mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-11 01:51:34 -10:00
Remove premature ShannonNet execution integration from 0.9.13
Founder requested deferring ShannonNet and Levee. Remove the backend, command, delegation/context hooks, configuration, translations, and release claims. Preserve OpenSandbox and backend receipts. Unsupported old backend settings refuse execution instead of silently selecting local execution. Independent project repositories and state remain untouched. Validation: focused runtime/commands/localization/work-surface tests 153 passed, 0 failed; web tests 407 passed, 0 failed. Web facts/docs/tokens/lint/TypeScript and locale parity pass (2 existing lint warnings); cargo fmt passes. TUI library check passed. Literal root npm test && npm run check:web remains unavailable because the root package lacks scripts. Hosted integration CI not yet run. Signed-off-by: CodeWhale Bot <[email protected]>
This commit is contained in:
1 parent
ea5358ab1b
commit
13c0f4ac29
35 files changed
+79
-1908
No files matched your search
@@ -237,29 +237,6 @@ with an accessibility-first pointer.
|
||||
three explicitly. `--use` saves the new secret as this machine's local
|
||||
`codewhale` provider credential in the same secret store `codewhale auth`
|
||||
uses; nothing is uploaded.
|
||||
- `sandbox_backend = "shannon"`: shell commands run as signed ShannonNet
|
||||
capability invocations (`cap://sandbox/exec`) on a worker that may live on
|
||||
another tailnet node. Codewhale opens a Task World per session for its
|
||||
durable `codewhale` Agent and every command leaves a receipt in
|
||||
`shannon trace`. New keys `sandbox_shannon_home` and
|
||||
`sandbox_shannon_capability`; tool metadata now reports the actual
|
||||
external backend kind instead of always `opensandbox`.
|
||||
- `/shannon [world|trace|children]` inspects the session's ShannonNet
|
||||
World: agent, projected capabilities, children, and receipts.
|
||||
- ShannonNet sub-agents get compiled context: the session's native-memory
|
||||
hits are imported with provenance and the child's projected World decides
|
||||
what it sees (confidential notes never cross); the session World is
|
||||
checkpointed and closed when the backend drops.
|
||||
- Sub-agents under delegated authority: with the ShannonNet backend the
|
||||
`agent` tool spawns a child identity with a World projected from the
|
||||
session World, the child's shell commands are signed as that child, and a
|
||||
join receipt is recorded when it finishes. `SandboxBackend::for_child` /
|
||||
`child_joined` default to sharing the parent backend for other backends.
|
||||
- Workspace sync for the ShannonNet backend (`sandbox_shannon_sync`, default
|
||||
on): the session's non-ignored files are shipped into the worker's
|
||||
per-World session container before each command — full tree first, then
|
||||
only changes and deletions — so remote builds and tests run on the files
|
||||
just edited locally and their outputs persist across commands.
|
||||
- `Git` grows a `commit_plan` action: a propose-only planner that splits the
|
||||
working tree into ordered atomic commits (#3999). It groups whole files —
|
||||
lock files ride with their manifest, tests ride with the source they name —
|
||||
|
||||
@@ -451,26 +451,6 @@ sandbox_mode = "workspace-write" # read-only | workspace-write | danger-full-acc
|
||||
# The backend uses a 30-second HTTP timeout. Background, interactive, and
|
||||
# TTY modes are not supported with external backends — all commands run
|
||||
# synchronously via HTTP.
|
||||
#
|
||||
# ShannonNet backend: each shell command becomes a signed capability
|
||||
# invocation on a ShannonNet worker (which may run on another tailnet node).
|
||||
# At session start Codewhale resolves its durable `codewhale` Agent, creates
|
||||
# a Task World named after the workspace, and attaches the capability; every
|
||||
# command then leaves a signed receipt (`shannon trace`). Requires the
|
||||
# `shannon` CLI on PATH (or `$SHANNON`) and an admitted provider for the
|
||||
# capability (`shannon cap advertise`).
|
||||
#
|
||||
# sandbox_backend = "shannon"
|
||||
# sandbox_shannon_home = "~/.shannon" # default: $SHANNON_HOME or ~/.shannon
|
||||
# sandbox_shannon_capability = "cap://sandbox/exec" # default
|
||||
# sandbox_shannon_sync = true # default: ship the workspace's
|
||||
# # non-ignored files into the worker's per-World session container before
|
||||
# # each command (a full archive first, then only changes), so remote builds
|
||||
# # and tests run on the files just edited here. false runs each command in
|
||||
# # a throwaway container against the worker's own read-only checkout.
|
||||
#
|
||||
# Env-var overrides: CODEWHALE_SANDBOX_SHANNON_HOME, CODEWHALE_SANDBOX_SHANNON_CAPABILITY,
|
||||
# CODEWHALE_SANDBOX_SHANNON_SYNC.
|
||||
# ─────────────────────────────────────────────────────────────────────────────────
|
||||
# Bubblewrap (Linux only, additional filesystem isolation)
|
||||
# ─────────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -237,29 +237,6 @@ with an accessibility-first pointer.
|
||||
three explicitly. `--use` saves the new secret as this machine's local
|
||||
`codewhale` provider credential in the same secret store `codewhale auth`
|
||||
uses; nothing is uploaded.
|
||||
- `sandbox_backend = "shannon"`: shell commands run as signed ShannonNet
|
||||
capability invocations (`cap://sandbox/exec`) on a worker that may live on
|
||||
another tailnet node. Codewhale opens a Task World per session for its
|
||||
durable `codewhale` Agent and every command leaves a receipt in
|
||||
`shannon trace`. New keys `sandbox_shannon_home` and
|
||||
`sandbox_shannon_capability`; tool metadata now reports the actual
|
||||
external backend kind instead of always `opensandbox`.
|
||||
- `/shannon [world|trace|children]` inspects the session's ShannonNet
|
||||
World: agent, projected capabilities, children, and receipts.
|
||||
- ShannonNet sub-agents get compiled context: the session's native-memory
|
||||
hits are imported with provenance and the child's projected World decides
|
||||
what it sees (confidential notes never cross); the session World is
|
||||
checkpointed and closed when the backend drops.
|
||||
- Sub-agents under delegated authority: with the ShannonNet backend the
|
||||
`agent` tool spawns a child identity with a World projected from the
|
||||
session World, the child's shell commands are signed as that child, and a
|
||||
join receipt is recorded when it finishes. `SandboxBackend::for_child` /
|
||||
`child_joined` default to sharing the parent backend for other backends.
|
||||
- Workspace sync for the ShannonNet backend (`sandbox_shannon_sync`, default
|
||||
on): the session's non-ignored files are shipped into the worker's
|
||||
per-World session container before each command — full tree first, then
|
||||
only changes and deletions — so remote builds and tests run on the files
|
||||
just edited locally and their outputs persist across commands.
|
||||
- `Git` grows a `commit_plan` action: a propose-only planner that splits the
|
||||
working tree into ordered atomic commits (#3999). It groups whole files —
|
||||
lock files ride with their manifest, tests ride with the source they name —
|
||||
|
||||
@@ -493,7 +493,6 @@
|
||||
"ModelsSourceFallback": "Models inclosos o configurats; disponibilitat no verificada.",
|
||||
"CmdModelDbDescription": "Explora la base de dades de models inclosa",
|
||||
"CmdNetworkDescription": "Gestiona les regles de xarxa de permís i denegació",
|
||||
"CmdShannonDescription": "Inspect this session's ShannonNet World, capabilities, children, and receipts",
|
||||
"CmdUpdateDescription": "Comprova i instal·la una nova versió de Codewhale",
|
||||
"CmdNoteDescription": "Afegeix, llista, edita o elimina notes de l'espai de treball",
|
||||
"CmdThemeDescription": "Canvia de tema o obre el selector de temes",
|
||||
|
||||
@@ -493,7 +493,6 @@
|
||||
"ModelsSourceFallback": "Mitgelieferte oder konfigurierte Modelle; Verfügbarkeit nicht geprüft.",
|
||||
"CmdModelDbDescription": "Mitgelieferte Modelldatenbank durchsuchen",
|
||||
"CmdNetworkDescription": "Netzwerk-Allow- und Deny-Regeln verwalten",
|
||||
"CmdShannonDescription": "Inspect this session's ShannonNet World, capabilities, children, and receipts",
|
||||
"CmdUpdateDescription": "Neue Codewhale-Version suchen und installieren",
|
||||
"CmdNoteDescription": "Workspace-Notizen hinzufügen, auflisten, bearbeiten oder entfernen",
|
||||
"CmdThemeDescription": "Theme wechseln oder Theme-Auswahl öffnen",
|
||||
|
||||
@@ -496,7 +496,6 @@
|
||||
"ModelsSourceFallback": "Bundled or configured models; availability not verified.",
|
||||
"CmdModelDbDescription": "Browse the bundled model database",
|
||||
"CmdNetworkDescription": "Manage network allow and deny rules",
|
||||
"CmdShannonDescription": "Inspect this session's ShannonNet World, capabilities, children, and receipts",
|
||||
"CmdUpdateDescription": "Check for and install a new Codewhale release",
|
||||
"CmdNoteDescription": "Add, list, edit, or remove workspace notes",
|
||||
"CmdThemeDescription": "Switch theme or open the theme picker",
|
||||
|
||||
@@ -496,7 +496,6 @@
|
||||
"ModelsSourceFallback": "Modelos incluidos o configurados; disponibilidad sin verificar.",
|
||||
"CmdModelDbDescription": "Explorar la base de datos de modelos integrada",
|
||||
"CmdNetworkDescription": "Gestionar reglas de red permitidas y bloqueadas",
|
||||
"CmdShannonDescription": "Inspect this session's ShannonNet World, capabilities, children, and receipts",
|
||||
"CmdUpdateDescription": "Buscar e instalar una nueva versión de Codewhale",
|
||||
"CmdNoteDescription": "Agregar nota al archivo persistente (.codewhale/notes.md)",
|
||||
"CmdThemeDescription": "Alternar entre tema claro y oscuro",
|
||||
|
||||
@@ -493,7 +493,6 @@
|
||||
"ModelsSourceFallback": "Modèles fournis ou configurés ; disponibilité non vérifiée.",
|
||||
"CmdModelDbDescription": "Parcourir la base de modèles intégrée",
|
||||
"CmdNetworkDescription": "Gérer les règles réseau d'autorisation et de refus",
|
||||
"CmdShannonDescription": "Inspect this session's ShannonNet World, capabilities, children, and receipts",
|
||||
"CmdUpdateDescription": "Rechercher et installer une nouvelle version de Codewhale",
|
||||
"CmdNoteDescription": "Ajouter, lister, modifier ou supprimer des notes du workspace",
|
||||
"CmdThemeDescription": "Changer de thème ou ouvrir le sélecteur de thème",
|
||||
|
||||
@@ -493,7 +493,6 @@
|
||||
"ModelsSourceFallback": "शामिल या कॉन्फ़िगर किए मॉडल; उपलब्धता सत्यापित नहीं है।",
|
||||
"CmdModelDbDescription": "बंडल किया मॉडल डेटाबेस ब्राउज़ करें",
|
||||
"CmdNetworkDescription": "नेटवर्क अनुमति और निषेध नियम प्रबंधित करें",
|
||||
"CmdShannonDescription": "Inspect this session's ShannonNet World, capabilities, children, and receipts",
|
||||
"CmdUpdateDescription": "नया Codewhale रिलीज़ जाँचें और इंस्टॉल करें",
|
||||
"CmdNoteDescription": "वर्कस्पेस नोट जोड़ें, सूचीबद्ध करें, संपादित करें या हटाएँ",
|
||||
"CmdThemeDescription": "थीम बदलें या थीम चयनकर्ता खोलें",
|
||||
|
||||
@@ -493,7 +493,6 @@
|
||||
"ModelsSourceFallback": "Model bawaan atau yang dikonfigurasi; ketersediaan belum diverifikasi.",
|
||||
"CmdModelDbDescription": "Telusuri database model bawaan",
|
||||
"CmdNetworkDescription": "Kelola aturan izinkan dan tolak jaringan",
|
||||
"CmdShannonDescription": "Inspect this session's ShannonNet World, capabilities, children, and receipts",
|
||||
"CmdUpdateDescription": "Periksa dan pasang rilis Codewhale baru",
|
||||
"CmdNoteDescription": "Tambah, tampilkan, edit, atau hapus catatan workspace",
|
||||
"CmdThemeDescription": "Ganti tema atau buka pemilih tema",
|
||||
|
||||
@@ -496,7 +496,6 @@
|
||||
"ModelsSourceFallback": "同梱または設定済みのモデル。利用可能かどうかは未確認です。",
|
||||
"CmdModelDbDescription": "内蔵のモデルデータベースを閲覧する",
|
||||
"CmdNetworkDescription": "ネットワーク許可・拒否ルールを管理",
|
||||
"CmdShannonDescription": "Inspect this session's ShannonNet World, capabilities, children, and receipts",
|
||||
"CmdUpdateDescription": "新しい Codewhale リリースを確認してインストールします",
|
||||
"CmdNoteDescription": "ワークスペースノートの追加、一覧、編集、削除",
|
||||
"CmdThemeDescription": "テーマを切り替え(ダーク/ライト/グレースケール/システム)",
|
||||
|
||||
@@ -496,7 +496,6 @@
|
||||
"ModelsSourceFallback": "기본 제공 또는 설정된 모델입니다. 사용 가능 여부는 확인되지 않았습니다.",
|
||||
"CmdModelDbDescription": "내장 모델 데이터베이스를 조회합니다",
|
||||
"CmdNetworkDescription": "네트워크 허용/차단 규칙을 관리합니다",
|
||||
"CmdShannonDescription": "Inspect this session's ShannonNet World, capabilities, children, and receipts",
|
||||
"CmdUpdateDescription": "새 Codewhale 릴리스를 확인하고 설치합니다",
|
||||
"CmdNoteDescription": "작업 공간 메모를 추가, 목록 조회, 편집, 삭제합니다",
|
||||
"CmdThemeDescription": "테마를 전환하거나 테마 선택 화면을 엽니다",
|
||||
|
||||
@@ -496,7 +496,6 @@
|
||||
"ModelsSourceFallback": "Modelos incluídos ou configurados; disponibilidade não verificada.",
|
||||
"CmdModelDbDescription": "Navegar pelo banco de dados de modelos integrado",
|
||||
"CmdNetworkDescription": "Gerenciar regras de rede permitidas e bloqueadas",
|
||||
"CmdShannonDescription": "Inspect this session's ShannonNet World, capabilities, children, and receipts",
|
||||
"CmdUpdateDescription": "Verificar e instalar uma nova versão do Codewhale",
|
||||
"CmdNoteDescription": "Adicionar, listar, editar ou remover notas do workspace",
|
||||
"CmdThemeDescription": "Alternar tema: escuro, claro, tons de cinza ou sistema",
|
||||
|
||||
@@ -493,7 +493,6 @@
|
||||
"ModelsSourceFallback": "Встроенные или настроенные модели; доступность не проверена.",
|
||||
"CmdModelDbDescription": "Просмотр встроенной базы моделей",
|
||||
"CmdNetworkDescription": "Управление правилами разрешения и запрета сети",
|
||||
"CmdShannonDescription": "Inspect this session's ShannonNet World, capabilities, children, and receipts",
|
||||
"CmdUpdateDescription": "Проверить и установить новый выпуск Codewhale",
|
||||
"CmdNoteDescription": "Добавить, показать, изменить или удалить заметки рабочей области",
|
||||
"CmdThemeDescription": "Сменить тему или открыть выбор темы",
|
||||
|
||||
@@ -493,7 +493,6 @@
|
||||
"ModelsSourceFallback": "Вбудовані або налаштовані моделі; доступність не перевірено.",
|
||||
"CmdModelDbDescription": "Переглянути вбудовану базу моделей",
|
||||
"CmdNetworkDescription": "Керувати мережевими правилами дозволу та заборони",
|
||||
"CmdShannonDescription": "Inspect this session's ShannonNet World, capabilities, children, and receipts",
|
||||
"CmdUpdateDescription": "Перевірити та встановити новий випуск Codewhale",
|
||||
"CmdNoteDescription": "Додати, показати, змінити або видалити нотатки робочої області",
|
||||
"CmdThemeDescription": "Змінити тему або відкрити вибір теми",
|
||||
|
||||
@@ -496,7 +496,6 @@
|
||||
"ModelsSourceFallback": "Mô hình đi kèm hoặc đã cấu hình; chưa xác minh tính khả dụng.",
|
||||
"CmdModelDbDescription": "Duyệt cơ sở dữ liệu mô hình tích hợp",
|
||||
"CmdNetworkDescription": "Quản lý các quy tắc cho phép và từ chối mạng",
|
||||
"CmdShannonDescription": "Inspect this session's ShannonNet World, capabilities, children, and receipts",
|
||||
"CmdUpdateDescription": "Kiểm tra và cài đặt bản phát hành Codewhale mới",
|
||||
"CmdNoteDescription": "Thêm, liệt kê, sửa hoặc xóa ghi chú trong không gian làm việc",
|
||||
"CmdThemeDescription": "Chuyển đổi giao diện hoặc mở bảng chọn giao diện",
|
||||
|
||||
@@ -496,7 +496,6 @@
|
||||
"ModelsSourceFallback": "内置或已配置的模型;尚未验证可用性。",
|
||||
"CmdModelDbDescription": "参考:浏览内置的模型数据库",
|
||||
"CmdNetworkDescription": "管理网络允许和拒绝规则",
|
||||
"CmdShannonDescription": "Inspect this session's ShannonNet World, capabilities, children, and receipts",
|
||||
"CmdUpdateDescription": "检查并安装新的 Codewhale 版本",
|
||||
"CmdNoteDescription": "添加、列出、编辑或删除工作区笔记",
|
||||
"CmdThemeDescription": "切换主题或打开主题选择器",
|
||||
|
||||
@@ -367,7 +367,6 @@
|
||||
"ModelsCodexHint": "ChatGPT 模型透過 Codex 載入。查詢時間並不代表供應商已重新整理模型目錄。",
|
||||
"ModelsSourceFallback": "內建或已設定的模型;尚未驗證可用性。",
|
||||
"CmdNetworkDescription": "管理網路允許和拒絕規則",
|
||||
"CmdShannonDescription": "Inspect this session's ShannonNet World, capabilities, children, and receipts",
|
||||
"CmdUpdateDescription": "檢查並安裝新的 Codewhale 版本",
|
||||
"CmdNewDescription": "開始一個新的已儲存工作階段",
|
||||
"CmdNoteDescription": "添加、列出、編輯或刪除工作區筆記",
|
||||
|
||||
@@ -2075,7 +2075,7 @@ fn build_acp_tool_registry(
|
||||
let kind = kind.trim();
|
||||
!kind.is_empty() && !kind.eq_ignore_ascii_case("none")
|
||||
});
|
||||
let sandbox_backend = match crate::sandbox::backend::create_backend(config, workspace) {
|
||||
let sandbox_backend = match crate::sandbox::backend::create_backend(config) {
|
||||
Ok(backend) => backend.map(std::sync::Arc::from),
|
||||
Err(error) => {
|
||||
tracing::warn!("Failed to create ACP sandbox backend: {error}");
|
||||
|
||||
@@ -211,7 +211,6 @@ pub(crate) fn key_to_message_id(key: &'static str) -> Option<MessageId> {
|
||||
"cmd_model_description" => MessageId::CmdModelDescription,
|
||||
"cmd_models_description" => MessageId::CmdModelsDescription,
|
||||
"cmd_network_description" => MessageId::CmdNetworkDescription,
|
||||
"cmd_shannon_description" => MessageId::CmdShannonDescription,
|
||||
"cmd_new_description" => MessageId::CmdNewDescription,
|
||||
"cmd_note_description" => MessageId::CmdNoteDescription,
|
||||
"cmd_permissions_description" => MessageId::CmdPermissionsDescription,
|
||||
@@ -1938,9 +1937,7 @@ pub(crate) struct MemoryAdapter<'a> {
|
||||
|
||||
/// Derive the authoritative native-memory store from the resolved user-memory
|
||||
/// file path, mirroring the pre-migration `/memory` handler exactly.
|
||||
pub(crate) fn native_store_from_memory_path(
|
||||
memory_path: &Path,
|
||||
) -> crate::native_memory::NativeMemoryStore {
|
||||
fn native_store_from_memory_path(memory_path: &Path) -> crate::native_memory::NativeMemoryStore {
|
||||
if let Some(store) = crate::native_memory::NativeMemoryStore::from_global_path(memory_path) {
|
||||
return store;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ mod dispatch;
|
||||
mod jobs;
|
||||
mod mcp;
|
||||
mod network;
|
||||
mod shannon;
|
||||
mod task;
|
||||
mod update;
|
||||
|
||||
@@ -45,10 +44,6 @@ impl CommandGroup for UtilityCommands {
|
||||
ContextualCommand::from_contract::<update::UpdateCmd>()
|
||||
.expect("update registration"),
|
||||
),
|
||||
Box::new(
|
||||
ContextualCommand::from_contract::<shannon::ShannonCmd>()
|
||||
.expect("shannon registration"),
|
||||
),
|
||||
])
|
||||
}
|
||||
}
|
||||
@@ -1,269 +0,0 @@
|
||||
//! `/shannon`: inspect this session's ShannonNet World — the durable Agent,
|
||||
//! the capabilities projected into the World, children spawned from it, and
|
||||
//! the receipts on its task. Read-only; the session backend
|
||||
//! (`sandbox_backend = "shannon"`) leaves a pointer file per workspace that
|
||||
//! names the ShannonNet home and World.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
use anyhow::{Context, bail};
|
||||
use serde_json::Value;
|
||||
|
||||
use codewhale_command_contract::handler::{CommandCapabilities, CommandContexts, CommandHandler};
|
||||
use codewhale_command_contract::metadata::{CommandInfo, RegisterCommand};
|
||||
|
||||
use crate::commands::CommandResult;
|
||||
|
||||
pub(in crate::commands) const COMMAND_INFO: CommandInfo = CommandInfo {
|
||||
name: "shannon",
|
||||
aliases: &[],
|
||||
usage: "/shannon [world|trace|children]",
|
||||
description_key: "cmd_shannon_description",
|
||||
};
|
||||
|
||||
pub(in crate::commands) struct ShannonCmd;
|
||||
|
||||
impl RegisterCommand<CommandResult> for ShannonCmd {
|
||||
fn info() -> &'static CommandInfo {
|
||||
&COMMAND_INFO
|
||||
}
|
||||
|
||||
fn handler() -> CommandHandler<CommandResult> {
|
||||
CommandHandler::Contextual {
|
||||
capabilities: CommandCapabilities::WORKSPACE,
|
||||
handler: shannon_contextual,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn shannon_contextual(contexts: CommandContexts<'_>, arg: Option<&str>) -> CommandResult {
|
||||
let parts = contexts.into_parts();
|
||||
let Some(workspace) = parts.workspace.as_deref() else {
|
||||
return CommandResult::error("Command capability unavailable: workspace");
|
||||
};
|
||||
match inspect(&workspace.workspace(), arg) {
|
||||
Ok(message) => CommandResult::message(message),
|
||||
Err(err) => CommandResult::error(err.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// What the session backend wrote at session start.
|
||||
#[derive(Debug)]
|
||||
struct SessionPointer {
|
||||
agent: String,
|
||||
world_id: String,
|
||||
binary: PathBuf,
|
||||
home: PathBuf,
|
||||
}
|
||||
|
||||
fn read_pointer(workspace: &Path) -> anyhow::Result<SessionPointer> {
|
||||
let path = crate::sandbox::shannon::session_pointer_path(workspace)
|
||||
.context("no Codewhale home directory")?;
|
||||
let raw = std::fs::read_to_string(&path).with_context(|| {
|
||||
format!(
|
||||
"no ShannonNet session for this workspace (set sandbox_backend = \"shannon\" and start a session); expected {}",
|
||||
path.display()
|
||||
)
|
||||
})?;
|
||||
let v: Value = serde_json::from_str(&raw).context("session pointer is not JSON")?;
|
||||
let field = |name: &str| {
|
||||
v.get(name)
|
||||
.and_then(Value::as_str)
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(str::to_string)
|
||||
.with_context(|| format!("session pointer lacks {name}"))
|
||||
};
|
||||
Ok(SessionPointer {
|
||||
agent: field("agent")?,
|
||||
world_id: field("world_id")?,
|
||||
binary: PathBuf::from(field("shannon_binary")?),
|
||||
home: PathBuf::from(field("shannon_home")?),
|
||||
})
|
||||
}
|
||||
|
||||
fn run_json(p: &SessionPointer, args: &[&str]) -> anyhow::Result<Value> {
|
||||
let output = Command::new(&p.binary)
|
||||
.arg("--home")
|
||||
.arg(&p.home)
|
||||
.arg("--json")
|
||||
.args(args)
|
||||
.stdin(Stdio::null())
|
||||
.output()
|
||||
.with_context(|| format!("failed to run {}", p.binary.display()))?;
|
||||
if !output.status.success() {
|
||||
bail!(
|
||||
"shannon {}: {}",
|
||||
args.join(" "),
|
||||
String::from_utf8_lossy(&output.stderr).trim()
|
||||
);
|
||||
}
|
||||
serde_json::from_slice(&output.stdout).context("shannon printed invalid JSON")
|
||||
}
|
||||
|
||||
fn run_text(p: &SessionPointer, args: &[&str]) -> anyhow::Result<String> {
|
||||
let output = Command::new(&p.binary)
|
||||
.arg("--home")
|
||||
.arg(&p.home)
|
||||
.args(args)
|
||||
.stdin(Stdio::null())
|
||||
.output()
|
||||
.with_context(|| format!("failed to run {}", p.binary.display()))?;
|
||||
if !output.status.success() {
|
||||
bail!(
|
||||
"shannon {}: {}",
|
||||
args.join(" "),
|
||||
String::from_utf8_lossy(&output.stderr).trim()
|
||||
);
|
||||
}
|
||||
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
|
||||
}
|
||||
|
||||
fn inspect(workspace: &Path, arg: Option<&str>) -> anyhow::Result<String> {
|
||||
let pointer = read_pointer(workspace)?;
|
||||
let section = arg.map(str::trim).unwrap_or("").to_ascii_lowercase();
|
||||
if !matches!(section.as_str(), "" | "world" | "trace" | "children") {
|
||||
bail!("Usage: /shannon [world|trace|children]");
|
||||
}
|
||||
let world = run_json(&pointer, &["world", "inspect", &pointer.world_id])?;
|
||||
let mut out = String::new();
|
||||
if section.is_empty() || section == "world" {
|
||||
out.push_str(&render_world(&pointer, &world));
|
||||
}
|
||||
if section.is_empty() || section == "children" {
|
||||
let tree = run_text(&pointer, &["agent", "tree", &pointer.agent]).unwrap_or_default();
|
||||
out.push_str("\nChildren (agent tree):\n");
|
||||
out.push_str(tree.trim_end());
|
||||
out.push('\n');
|
||||
}
|
||||
if (section.is_empty() || section == "trace")
|
||||
&& let Some(task) = world.get("task_id").and_then(Value::as_str)
|
||||
{
|
||||
let events = run_json(&pointer, &["trace", task])?;
|
||||
out.push_str(&render_trace(&events));
|
||||
}
|
||||
Ok(out.trim_end().to_string())
|
||||
}
|
||||
|
||||
fn render_world(p: &SessionPointer, world: &Value) -> String {
|
||||
let field = |name: &str| world.get(name).and_then(Value::as_str).unwrap_or("?");
|
||||
let mut out = format!(
|
||||
"ShannonNet session\n agent: {}\n world: {} ({}, state {})\n task: {}\n home: {}\n capabilities projected into this World:\n",
|
||||
p.agent,
|
||||
field("id"),
|
||||
field("name"),
|
||||
field("state"),
|
||||
field("task_id"),
|
||||
p.home.display()
|
||||
);
|
||||
let attachments = world
|
||||
.get("attachments")
|
||||
.and_then(Value::as_array)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
if attachments.is_empty() {
|
||||
out.push_str(" (none)\n");
|
||||
}
|
||||
for att in attachments {
|
||||
let uri = att.get("uri").and_then(Value::as_str).unwrap_or("?");
|
||||
let actions = att
|
||||
.get("actions")
|
||||
.and_then(Value::as_array)
|
||||
.map(|a| {
|
||||
a.iter()
|
||||
.filter_map(Value::as_str)
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let depth = att
|
||||
.get("grant_chain")
|
||||
.and_then(Value::as_array)
|
||||
.map(Vec::len)
|
||||
.unwrap_or(0);
|
||||
out.push_str(&format!(
|
||||
" {uri:<28} actions={actions:<22} grant links={depth}\n"
|
||||
));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn render_trace(events: &Value) -> String {
|
||||
let events = events.as_array().cloned().unwrap_or_default();
|
||||
let shown: Vec<&Value> = events.iter().rev().take(12).collect();
|
||||
let mut out = format!(
|
||||
"\nReceipts on this task (last {} of {}):\n",
|
||||
shown.len(),
|
||||
events.len()
|
||||
);
|
||||
for e in shown.into_iter().rev() {
|
||||
let kind = e.get("type").and_then(Value::as_str).unwrap_or("?");
|
||||
let at = e.get("created_at").and_then(Value::as_str).unwrap_or("");
|
||||
let provider = e.get("provider_id").and_then(Value::as_str).unwrap_or("");
|
||||
let capability = e.get("capability").and_then(Value::as_str).unwrap_or("");
|
||||
let evidence = e
|
||||
.pointer("/data/transport_evidence")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("");
|
||||
let mut line = format!(" {} {kind}", &at[..at.len().min(19)]);
|
||||
if !capability.is_empty() {
|
||||
line.push_str(&format!(" {capability}"));
|
||||
}
|
||||
if !provider.is_empty() {
|
||||
line.push_str(&format!(" via {provider}"));
|
||||
}
|
||||
if !evidence.is_empty() {
|
||||
line.push_str(&format!(" [{evidence}]"));
|
||||
}
|
||||
out.push_str(&line);
|
||||
out.push('\n');
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn renders_world_and_trace() {
|
||||
let p = SessionPointer {
|
||||
agent: "codewhale".into(),
|
||||
world_id: "w1".into(),
|
||||
binary: PathBuf::from("shannon"),
|
||||
home: PathBuf::from("/h"),
|
||||
};
|
||||
let world = json!({"id":"w1","name":"codewhale:proj","state":"active","task_id":"t1",
|
||||
"attachments":[{"uri":"cap://sandbox/exec","actions":["invoke","sync","destroy"],"grant_chain":[{}]}]});
|
||||
let text = render_world(&p, &world);
|
||||
assert!(
|
||||
text.contains("world: w1 (codewhale:proj, state active)"),
|
||||
"{text}"
|
||||
);
|
||||
let squashed = text.split_whitespace().collect::<Vec<_>>().join(" ");
|
||||
assert!(
|
||||
squashed.contains("cap://sandbox/exec actions=invoke,sync,destroy grant links=1"),
|
||||
"{text}"
|
||||
);
|
||||
let trace = render_trace(&json!([
|
||||
{"type":"capability.invoked","created_at":"2026-09-05T01:02:03.4Z","capability":"cap://sandbox/exec","provider_id":"prov-1","data":{"transport_evidence":"nCTRL tags=tag:shannon-controller"}},
|
||||
{"type":"agent.joined","created_at":"2026-09-05T01:03:03.4Z"}
|
||||
]));
|
||||
assert!(trace.contains("last 2 of 2"), "{trace}");
|
||||
assert!(
|
||||
trace.contains("2026-09-05T01:02:03 capability.invoked cap://sandbox/exec via prov-1 [nCTRL tags=tag:shannon-controller]"),
|
||||
"{trace}"
|
||||
);
|
||||
assert!(
|
||||
trace.contains("2026-09-05T01:03:03 agent.joined"),
|
||||
"{trace}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_pointer_explains_how_to_start_a_session() {
|
||||
let err = read_pointer(Path::new("/definitely/not/a/workspace/xyz")).unwrap_err();
|
||||
assert!(err.to_string().contains("no ShannonNet session"), "{err}");
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@
|
||||
//! fall-through behaviour.
|
||||
|
||||
mod contract;
|
||||
pub(crate) use contract::native_store_from_memory_path;
|
||||
pub mod discovery;
|
||||
mod groups;
|
||||
pub mod traits;
|
||||
@@ -2045,8 +2044,6 @@ mod tests {
|
||||
"network",
|
||||
"task",
|
||||
"update",
|
||||
// ShannonNet session inspection (utility group, portable handler).
|
||||
"shannon",
|
||||
// FEAT-021 project group.
|
||||
"init",
|
||||
"lsp",
|
||||
|
||||
@@ -3158,9 +3158,9 @@ pub struct Config {
|
||||
pub fallback_providers: Vec<codewhale_config::ProviderKind>,
|
||||
pub yolo: Option<bool>,
|
||||
pub verbosity: Option<String>,
|
||||
/// External sandbox backend: `"none"`, `"opensandbox"`, or `"shannon"`.
|
||||
/// When set, exec_shell routes commands through the backend instead of
|
||||
/// spawning a local process.
|
||||
/// External sandbox backend: `"none"` or `"opensandbox"`.
|
||||
/// When set, exec_shell routes commands through the backend's HTTP API
|
||||
/// instead of spawning a local process.
|
||||
#[serde(alias = "sandboxBackend")]
|
||||
pub sandbox_backend: Option<String>,
|
||||
/// Base URL for the external sandbox backend (default: `"http://localhost:8080"`).
|
||||
@@ -3169,19 +3169,6 @@ pub struct Config {
|
||||
/// Optional API key for the external sandbox backend (sent as Bearer token).
|
||||
#[serde(alias = "sandboxApiKey")]
|
||||
pub sandbox_api_key: Option<String>,
|
||||
/// ShannonNet state directory for `sandbox_backend = "shannon"`
|
||||
/// (default: `$SHANNON_HOME`, else `~/.shannon`).
|
||||
#[serde(alias = "sandboxShannonHome")]
|
||||
pub sandbox_shannon_home: Option<String>,
|
||||
/// Capability invoked per shell command for `sandbox_backend = "shannon"`
|
||||
/// (default: `cap://sandbox/exec`).
|
||||
#[serde(alias = "sandboxShannonCapability")]
|
||||
pub sandbox_shannon_capability: Option<String>,
|
||||
/// Ship the workspace's non-ignored files to the worker's per-World
|
||||
/// session before each command (default true). False runs commands
|
||||
/// against the worker's own checkout in a throwaway container.
|
||||
#[serde(alias = "sandboxShannonSync")]
|
||||
pub sandbox_shannon_sync: Option<bool>,
|
||||
/// When true and `/usr/bin/bwrap` is executable on Linux, route exec_shell
|
||||
/// through bubblewrap (#2184).
|
||||
/// Defaults to false. Requires the `bubblewrap` package to be installed
|
||||
@@ -8128,12 +8115,12 @@ fn root_deepseek_model_is_foreign_to_direct_provider(provider: ApiProvider, mode
|
||||
mod home;
|
||||
mod paths;
|
||||
use paths::{
|
||||
canonicalize_or_keep, default_config_path, default_managed_config_path,
|
||||
canonicalize_or_keep, codewhale_home_dir, default_config_path, default_managed_config_path,
|
||||
default_mcp_config_path, default_memory_path, default_notes_path, default_requirements_path,
|
||||
default_skills_dir, env_config_path, expand_pathbuf, home_config_path, try_default_config_path,
|
||||
workspace_config_key,
|
||||
};
|
||||
pub(crate) use paths::{codewhale_home_dir, effective_home_dir, expand_path};
|
||||
pub(crate) use paths::{effective_home_dir, expand_path};
|
||||
|
||||
pub(crate) fn workspace_trust_config_candidate_paths() -> Vec<PathBuf> {
|
||||
#[cfg(test)]
|
||||
@@ -9643,15 +9630,6 @@ fn apply_env_overrides_unlocked(config: &mut Config, policy: ConfigEnvironmentPo
|
||||
{
|
||||
config.sandbox_api_key = Some(value);
|
||||
}
|
||||
if let Ok(value) = std::env::var("CODEWHALE_SANDBOX_SHANNON_HOME") {
|
||||
config.sandbox_shannon_home = Some(value);
|
||||
}
|
||||
if let Ok(value) = std::env::var("CODEWHALE_SANDBOX_SHANNON_CAPABILITY") {
|
||||
config.sandbox_shannon_capability = Some(value);
|
||||
}
|
||||
if let Ok(value) = std::env::var("CODEWHALE_SANDBOX_SHANNON_SYNC") {
|
||||
config.sandbox_shannon_sync = Some(value == "1" || value.eq_ignore_ascii_case("true"));
|
||||
}
|
||||
if let Ok(value) = std::env::var("CODEWHALE_MANAGED_CONFIG_PATH")
|
||||
.or_else(|_| std::env::var("DEEPSEEK_MANAGED_CONFIG_PATH"))
|
||||
{
|
||||
@@ -10814,15 +10792,6 @@ fn merge_config(base: Config, override_cfg: Config) -> Config {
|
||||
sandbox_backend: override_cfg.sandbox_backend.or(base.sandbox_backend),
|
||||
sandbox_url: override_cfg.sandbox_url.or(base.sandbox_url),
|
||||
sandbox_api_key: override_cfg.sandbox_api_key.or(base.sandbox_api_key),
|
||||
sandbox_shannon_home: override_cfg
|
||||
.sandbox_shannon_home
|
||||
.or(base.sandbox_shannon_home),
|
||||
sandbox_shannon_capability: override_cfg
|
||||
.sandbox_shannon_capability
|
||||
.or(base.sandbox_shannon_capability),
|
||||
sandbox_shannon_sync: override_cfg
|
||||
.sandbox_shannon_sync
|
||||
.or(base.sandbox_shannon_sync),
|
||||
prefer_bwrap: override_cfg.prefer_bwrap.or(base.prefer_bwrap),
|
||||
bwrap_ro_roots: if override_cfg.bwrap_ro_roots.is_empty() {
|
||||
base.bwrap_ro_roots
|
||||
|
||||
@@ -1646,13 +1646,12 @@ impl Engine {
|
||||
// External sandbox backend (#516). Logged but non-fatal: if the
|
||||
// backend fails to construct, the engine continues with local
|
||||
// execution as the fallback.
|
||||
let sandbox_backend =
|
||||
crate::sandbox::backend::create_backend(api_config, &config.workspace)
|
||||
.unwrap_or_else(|e| {
|
||||
tracing::warn!("Failed to create sandbox backend: {e}");
|
||||
None
|
||||
})
|
||||
.map(std::sync::Arc::from);
|
||||
let sandbox_backend = crate::sandbox::backend::create_backend(api_config)
|
||||
.unwrap_or_else(|e| {
|
||||
tracing::warn!("Failed to create sandbox backend: {e}");
|
||||
None
|
||||
})
|
||||
.map(std::sync::Arc::from);
|
||||
let sandbox_enforcement = if sandbox_backend.is_some() {
|
||||
crate::sandbox::policy::SandboxEnforcement::ExternalBackend
|
||||
} else if crate::sandbox::get_platform_sandbox_with_bwrap_preference(config.prefer_bwrap)
|
||||
|
||||
@@ -11948,7 +11948,7 @@ async fn build_direct_workflow_tool(
|
||||
.search
|
||||
.as_ref()
|
||||
.and_then(|search| search.base_url.clone());
|
||||
if let Some(backend) = crate::sandbox::backend::create_backend(config, workspace)? {
|
||||
if let Some(backend) = crate::sandbox::backend::create_backend(config)? {
|
||||
context = context.with_sandbox_backend(Arc::from(backend));
|
||||
}
|
||||
|
||||
|
||||
@@ -621,7 +621,6 @@ pub enum MessageId {
|
||||
ModelsSourceFallback,
|
||||
CmdModelDbDescription,
|
||||
CmdNetworkDescription,
|
||||
CmdShannonDescription,
|
||||
CmdUpdateDescription,
|
||||
CmdNoteDescription,
|
||||
CmdThemeDescription,
|
||||
@@ -2822,7 +2821,6 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[
|
||||
MessageId::ModelsSourceFallback,
|
||||
MessageId::CmdModelDbDescription,
|
||||
MessageId::CmdNetworkDescription,
|
||||
MessageId::CmdShannonDescription,
|
||||
MessageId::CmdUpdateDescription,
|
||||
MessageId::CmdNoteDescription,
|
||||
MessageId::CmdProviderDescription,
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
//! Pluggable sandbox backend abstraction.
|
||||
//!
|
||||
//! External sandbox backends route shell command execution to a remote service
|
||||
//! (Alibaba OpenSandbox, or a ShannonNet worker reached by capability name)
|
||||
//! instead of spawning a local process. This is complementary to the OS-level
|
||||
//! sandbox module (Seatbelt / opt-in bubblewrap) — the external backend
|
||||
//! *replaces* local execution entirely when configured.
|
||||
//! (e.g. Alibaba OpenSandbox) instead of spawning a local process. This is
|
||||
//! complementary to the OS-level sandbox module (Seatbelt / opt-in bubblewrap)
|
||||
//! — the external backend *replaces* local execution entirely when configured.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
@@ -31,9 +28,8 @@ pub enum SandboxKind {
|
||||
None,
|
||||
/// Alibaba OpenSandbox remote execution.
|
||||
OpenSandbox,
|
||||
/// ShannonNet: a signed capability invocation on a worker that may live
|
||||
/// on another tailnet node (see `shannon.rs`).
|
||||
Shannon,
|
||||
/// Configured backend is unavailable; execution is refused.
|
||||
Unsupported,
|
||||
}
|
||||
|
||||
impl SandboxKind {
|
||||
@@ -43,7 +39,6 @@ impl SandboxKind {
|
||||
match value.trim().to_ascii_lowercase().as_str() {
|
||||
"none" | "" => Some(Self::None),
|
||||
"opensandbox" | "open-sandbox" | "open_sandbox" => Some(Self::OpenSandbox),
|
||||
"shannon" | "shannonnet" | "shannon-net" => Some(Self::Shannon),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -54,7 +49,7 @@ impl SandboxKind {
|
||||
match self {
|
||||
Self::None => "none",
|
||||
Self::OpenSandbox => "opensandbox",
|
||||
Self::Shannon => "shannon",
|
||||
Self::Unsupported => "unsupported",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,55 +61,13 @@ impl SandboxKind {
|
||||
/// `Arc` and shared across async tasks.
|
||||
#[async_trait]
|
||||
pub trait SandboxBackend: Send + Sync {
|
||||
/// Which backend this is, for tool metadata and receipts.
|
||||
/// Backend identity used by tool receipts.
|
||||
fn kind(&self) -> SandboxKind;
|
||||
|
||||
/// Execute a shell command and return its output.
|
||||
///
|
||||
/// `cmd` is the full shell command string (e.g. `"ls -la"`).
|
||||
/// `env` contains additional environment variables to set.
|
||||
async fn exec(&self, cmd: &str, env: &HashMap<String, String>) -> Result<SandboxOutput>;
|
||||
|
||||
/// A backend for a sub-agent about to run under this one. Backends with
|
||||
/// a notion of delegated authority (ShannonNet) return a child bound to
|
||||
/// its own identity and projected World; the default shares this
|
||||
/// backend unchanged (`None`), which is what a plain remote executor
|
||||
/// means by "sub-agent".
|
||||
fn for_child(&self, _role: &str, _objective: &str) -> Result<Option<Arc<dyn SandboxBackend>>> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Called on a backend returned by [`for_child`](Self::for_child) once
|
||||
/// its sub-agent has finished, with the child's final summary, token
|
||||
/// usage, and whether it completed. Records the join and retires the
|
||||
/// child's authority where the backend has such a notion.
|
||||
async fn child_joined(
|
||||
&self,
|
||||
_summary: Option<&str>,
|
||||
_tokens: u64,
|
||||
_succeeded: bool,
|
||||
) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Bounded context for a sub-agent's task, compiled from what its World
|
||||
/// may see. `notes` are the session's own memory hits for the task; a
|
||||
/// backend with a memory graph imports them with provenance and returns
|
||||
/// a text block to append to the child's prompt. The default has no
|
||||
/// such graph and returns `None` (the child gets the prompt alone).
|
||||
async fn child_context(&self, _task: &str, _notes: &[MemoryNote]) -> Result<Option<String>> {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// One note from the session's memory, offered to a child backend.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct MemoryNote {
|
||||
/// Stable key (source and line span) so re-imports do not duplicate.
|
||||
pub key: String,
|
||||
pub content: String,
|
||||
/// Provenance reference recorded with the memory.
|
||||
pub source: String,
|
||||
}
|
||||
|
||||
use crate::config::Config;
|
||||
@@ -124,21 +77,16 @@ use crate::config::Config;
|
||||
/// Returns `None` when no external sandbox backend is configured (i.e. the
|
||||
/// `sandbox_backend` key is absent, empty, or `"none"`). When `"opensandbox"`
|
||||
/// is set, constructs an [`OpenSandboxBackend`](super::opensandbox::OpenSandboxBackend) using `sandbox_url` and
|
||||
/// `sandbox_api_key`. When `"shannon"` is set, constructs a
|
||||
/// [`ShannonBackend`](super::shannon::ShannonBackend), which opens a Task
|
||||
/// World for `workspace` at construction (session start).
|
||||
pub fn create_backend(
|
||||
config: &Config,
|
||||
workspace: &Path,
|
||||
) -> Result<Option<Box<dyn SandboxBackend>>> {
|
||||
let kind = config
|
||||
.sandbox_backend
|
||||
.as_deref()
|
||||
.and_then(SandboxKind::parse)
|
||||
.unwrap_or(SandboxKind::None);
|
||||
/// `sandbox_api_key`.
|
||||
pub fn create_backend(config: &Config) -> Result<Option<Box<dyn SandboxBackend>>> {
|
||||
let Some(kind) = SandboxKind::parse(config.sandbox_backend.as_deref().unwrap_or("none")) else {
|
||||
// Old or misspelled remote settings must never select local execution.
|
||||
return Ok(Some(Box::new(UnsupportedBackend)));
|
||||
};
|
||||
|
||||
match kind {
|
||||
SandboxKind::None => Ok(None),
|
||||
SandboxKind::Unsupported => Ok(Some(Box::new(UnsupportedBackend))),
|
||||
SandboxKind::OpenSandbox => {
|
||||
let base_url = config
|
||||
.sandbox_url
|
||||
@@ -148,29 +96,51 @@ pub fn create_backend(
|
||||
let backend = super::opensandbox::OpenSandboxBackend::new(base_url, api_key, 30)?;
|
||||
Ok(Some(Box::new(backend)))
|
||||
}
|
||||
SandboxKind::Shannon => {
|
||||
let binary = std::env::var_os("SHANNON")
|
||||
.map(std::path::PathBuf::from)
|
||||
.unwrap_or_else(|| std::path::PathBuf::from("shannon"));
|
||||
let home = config
|
||||
.sandbox_shannon_home
|
||||
.as_deref()
|
||||
.map(crate::config::expand_path)
|
||||
.or_else(|| std::env::var_os("SHANNON_HOME").map(std::path::PathBuf::from))
|
||||
.unwrap_or_else(|| {
|
||||
dirs::home_dir()
|
||||
.unwrap_or_else(|| std::path::PathBuf::from("."))
|
||||
.join(".shannon")
|
||||
});
|
||||
let capability = config
|
||||
.sandbox_shannon_capability
|
||||
.clone()
|
||||
.filter(|cap| !cap.trim().is_empty())
|
||||
.unwrap_or_else(|| super::shannon::DEFAULT_CAPABILITY.to_string());
|
||||
let sync = config.sandbox_shannon_sync.unwrap_or(true);
|
||||
let backend =
|
||||
super::shannon::ShannonBackend::new(binary, home, capability, workspace, 30, sync)?;
|
||||
Ok(Some(Box::new(backend)))
|
||||
}
|
||||
}
|
||||
|
||||
/// A configured execution boundary that is no longer supported. Keep it present
|
||||
/// in the tool context so every shell call is refused instead of running locally.
|
||||
struct UnsupportedBackend;
|
||||
|
||||
#[async_trait]
|
||||
impl SandboxBackend for UnsupportedBackend {
|
||||
fn kind(&self) -> SandboxKind {
|
||||
SandboxKind::Unsupported
|
||||
}
|
||||
async fn exec(&self, _cmd: &str, _env: &HashMap<String, String>) -> Result<SandboxOutput> {
|
||||
anyhow::bail!(
|
||||
"Unsupported sandbox_backend setting. Choose opensandbox, or explicitly set none for local execution."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn unsupported_backend_refuses_execution_instead_of_falling_back_to_local() {
|
||||
for name in ["shannon", "shannonnet", "shannon-net", "levee", "unknown"] {
|
||||
let config = Config {
|
||||
sandbox_backend: Some(name.into()),
|
||||
..Config::default()
|
||||
};
|
||||
let backend = create_backend(&config)
|
||||
.unwrap()
|
||||
.expect("retain execution boundary");
|
||||
let error = backend
|
||||
.exec("echo must-not-run", &HashMap::new())
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(error.to_string().contains("Unsupported sandbox_backend"));
|
||||
}
|
||||
for name in [None, Some("none"), Some("")] {
|
||||
let config = Config {
|
||||
sandbox_backend: name.map(str::to_owned),
|
||||
..Config::default()
|
||||
};
|
||||
assert!(create_backend(&config).unwrap().is_none());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,6 @@ pub mod opensandbox;
|
||||
pub mod policy;
|
||||
pub mod process_hardening;
|
||||
pub mod read_guard;
|
||||
pub mod shannon;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod seatbelt;
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -11364,132 +11364,7 @@ async fn cancelled_subagent_result(
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
|
||||
/// Runs one sub-agent under delegated authority. When the session's
|
||||
/// sandbox backend can derive a child (ShannonNet: a spawned child identity
|
||||
/// with a World projected from the session World), the sub-agent's tools run
|
||||
/// through that child backend and never as the session principal; when it
|
||||
/// cannot (a plain remote executor, or no backend), the sub-agent shares the
|
||||
/// parent's backend as before. A failed delegation fails the spawn rather
|
||||
/// than silently running the child with the parent's authority. On
|
||||
/// completion the child is joined: a typed receipt on the task and the
|
||||
/// child's authority retired.
|
||||
async fn run_subagent(
|
||||
runtime: &SubAgentRuntime,
|
||||
agent_id: String,
|
||||
agent_type: FleetRole,
|
||||
prompt: String,
|
||||
assignment: SubAgentAssignment,
|
||||
allowed_tools: Option<Vec<String>>,
|
||||
fork_context: bool,
|
||||
started_at: Instant,
|
||||
max_steps: u32,
|
||||
token_budget: Option<u64>,
|
||||
turn_end_parking: Option<Arc<std::sync::atomic::AtomicBool>>,
|
||||
input_rx: mpsc::UnboundedReceiver<SubAgentInput>,
|
||||
) -> Result<SubAgentResult> {
|
||||
let child_backend = match runtime.context.sandbox_backend.as_ref() {
|
||||
Some(backend) => {
|
||||
let role = assignment
|
||||
.role
|
||||
.as_deref()
|
||||
.filter(|role| !role.trim().is_empty())
|
||||
.unwrap_or(agent_type.as_str());
|
||||
backend
|
||||
.for_child(role, &prompt)
|
||||
.map_err(|err| anyhow!("sub-agent authority could not be delegated: {err}"))?
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
let child_runtime = child_backend.as_ref().map(|child| {
|
||||
let mut child_runtime = runtime.clone();
|
||||
child_runtime.context.sandbox_backend = Some(Arc::clone(child));
|
||||
child_runtime
|
||||
});
|
||||
// Bounded context instead of a transcript: the session's memory hits
|
||||
// for this task go into the authority layer's memory graph, and the
|
||||
// child gets back only what its projected World may see.
|
||||
let prompt = match child_backend.as_ref() {
|
||||
Some(child) => {
|
||||
let notes = session_memory_notes(runtime, &prompt);
|
||||
match child.child_context(&prompt, ¬es).await {
|
||||
Ok(Some(brief)) => format!("{prompt}\n\n{brief}"),
|
||||
Ok(None) => prompt,
|
||||
Err(err) => {
|
||||
tracing::warn!(target: "subagent", ?err, agent_id, "sub-agent context was not compiled");
|
||||
prompt
|
||||
}
|
||||
}
|
||||
}
|
||||
None => prompt,
|
||||
};
|
||||
let result = run_subagent_in(
|
||||
child_runtime.as_ref().unwrap_or(runtime),
|
||||
agent_id.clone(),
|
||||
agent_type,
|
||||
prompt,
|
||||
assignment,
|
||||
allowed_tools,
|
||||
fork_context,
|
||||
started_at,
|
||||
max_steps,
|
||||
token_budget,
|
||||
turn_end_parking,
|
||||
input_rx,
|
||||
)
|
||||
.await;
|
||||
if let Some(child) = child_backend {
|
||||
let (summary, succeeded) = match &result {
|
||||
Ok(outcome) => (
|
||||
outcome.result.as_deref(),
|
||||
matches!(outcome.status, SubAgentStatus::Completed),
|
||||
),
|
||||
Err(_) => (None, false),
|
||||
};
|
||||
// Token accounting stays in Codewhale's own usage record; the join
|
||||
// carries the child's conclusion and outcome.
|
||||
if let Err(err) = child.child_joined(summary, 0, succeeded).await {
|
||||
tracing::warn!(target: "subagent", ?err, agent_id, "sub-agent join was not recorded");
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// The session's native-memory hits for a task, as notes a child backend
|
||||
/// may import. Empty when memory is off or the store has nothing.
|
||||
fn session_memory_notes(
|
||||
runtime: &SubAgentRuntime,
|
||||
task: &str,
|
||||
) -> Vec<crate::sandbox::backend::MemoryNote> {
|
||||
let Some(memory_path) = runtime.context.memory_path.as_deref() else {
|
||||
return Vec::new();
|
||||
};
|
||||
let store = crate::commands::native_store_from_memory_path(memory_path);
|
||||
match store.search_for_workspace(&runtime.context.workspace, task, 12) {
|
||||
Ok(hits) => hits
|
||||
.into_iter()
|
||||
.filter(|hit| !hit.stale)
|
||||
.map(|hit| crate::sandbox::backend::MemoryNote {
|
||||
key: format!(
|
||||
"{}:{}-{}",
|
||||
hit.source.display(),
|
||||
hit.line_start,
|
||||
hit.line_end
|
||||
),
|
||||
content: hit.text,
|
||||
source: format!(
|
||||
"codewhale-memory:{}:{}-{}",
|
||||
hit.source.display(),
|
||||
hit.line_start,
|
||||
hit.line_end
|
||||
),
|
||||
})
|
||||
.collect(),
|
||||
Err(_) => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn run_subagent_in(
|
||||
runtime: &SubAgentRuntime,
|
||||
agent_id: String,
|
||||
agent_type: FleetRole,
|
||||
|
||||
@@ -3526,7 +3526,7 @@ mod tests {
|
||||
app.work_surface.effective_placement = WorkSurfacePlacement::Top;
|
||||
app.task_panel.push(running_shell_entry(
|
||||
"shell_a1b2c3d4",
|
||||
"cd /Volumes/VIXinSSD/ShannonNet",
|
||||
"cd /workspace/example-project",
|
||||
));
|
||||
app.subagent_cache
|
||||
.push(running_agent("doc-scout-spec-arch"));
|
||||
@@ -3578,7 +3578,7 @@ mod tests {
|
||||
assert_eq!(facts.role_label, "shell");
|
||||
assert_eq!(facts.status, "running");
|
||||
assert!(
|
||||
facts.objective.contains("ShannonNet"),
|
||||
facts.objective.contains("example-project"),
|
||||
"{}",
|
||||
facts.objective
|
||||
);
|
||||
|
||||
@@ -197,8 +197,6 @@ drives turns through Chat Completions.
|
||||
execution to a remote service, e.g. Alibaba OpenSandbox)
|
||||
- `policy.rs` - Sandbox policy configuration
|
||||
- `opensandbox.rs` - Alibaba OpenSandbox HTTP backend adapter
|
||||
- `shannon.rs` - ShannonNet backend: signed capability invocations through
|
||||
the `shannon` CLI, one Task World per session
|
||||
- `seatbelt.rs` - macOS Seatbelt profile generation
|
||||
- `bwrap.rs` - opt-in Linux bubblewrap command wrapper
|
||||
- `seccomp.rs` - dormant Linux seccomp implementation; not wired into commands
|
||||
|
||||
+2
-65
@@ -18,7 +18,6 @@ run before execution reaches this boundary.
|
||||
| No OS wrapper | Linux without usable opt-in bwrap | Default | `none` |
|
||||
| No OS wrapper | Windows | Current implementation | `none` |
|
||||
| OpenSandbox-compatible service | Any supported host | `sandbox_backend = "opensandbox"` | External execution path |
|
||||
| ShannonNet worker (signed capability invocation, any tailnet node) | Any supported host with the `shannon` CLI | `sandbox_backend = "shannon"` | External execution path |
|
||||
|
||||
The repository contains a seccomp implementation module plus a future Windows
|
||||
helper contract. They are not wired into child-command launch, so Codewhale
|
||||
@@ -148,68 +147,8 @@ sandbox_api_key = "YOUR_API_KEY"
|
||||
```
|
||||
|
||||
`sandbox_backend = "none"` (or omitting the key) keeps local execution.
|
||||
|
||||
## External ShannonNet execution
|
||||
|
||||
When `sandbox_backend = "shannon"` is configured, each shell command becomes
|
||||
one signed `cap://sandbox/exec` invocation through the `shannon` CLI. The
|
||||
worker that executes it is whichever admitted provider the ShannonNet router
|
||||
selects — typically a `shannon-worker --kind docker` or a
|
||||
`shannon-tsnet-worker` on another tailnet node — and Codewhale never learns
|
||||
its address. At session start Codewhale resolves its durable `codewhale`
|
||||
Agent (creating it once), creates a Task World named after the workspace, and
|
||||
attaches the capability; the worker verifies that grant chain, executes in a
|
||||
network-less, resource-capped container, and returns stdout, stderr, and the
|
||||
exit code inside a signed receipt. `shannon trace <task>` lists every command
|
||||
with the provider and transport evidence that served it.
|
||||
|
||||
With `sandbox_shannon_sync = true` (the default) the worker keeps one
|
||||
writable session container per Task World and Codewhale ships the session's
|
||||
working tree into it before each command: the full non-ignored tree the
|
||||
first time (`.gitignore`, local and global excludes, and `.git` itself are
|
||||
honored; symlinks and files over 16 MiB are skipped), then only added or
|
||||
modified files and deletions, chunked at 6 MiB per request. Commands run in
|
||||
that `/work` with what the Engine just edited locally, and what they write
|
||||
persists for the next command, so remote builds and test suites work.
|
||||
The worker refuses path traversal, absolute paths, symlinks, and archives
|
||||
over its size budget, destroys the session when the backend drops or after
|
||||
an idle TTL, and never exposes its own checkout to the session. A failed
|
||||
sync fails the command rather than running it on a stale tree.
|
||||
|
||||
Sub-agents run under delegated authority. When the `agent` tool spawns a
|
||||
child, the backend spawns a ShannonNet child identity certified by the
|
||||
session's `codewhale` Agent, with a World projected from the session World
|
||||
that exposes only the sandbox capability (delegation depth attenuated); the
|
||||
child's shell commands are signed as that child in that World and ship into
|
||||
the child's own session container. When the child finishes, a join receipt
|
||||
(its final summary and outcome) is recorded on the task and the child's
|
||||
World is destroyed. A delegation that cannot be established fails the spawn
|
||||
rather than running the child as the session principal. Backends without
|
||||
delegated authority (OpenSandbox) share the parent's backend as before.
|
||||
|
||||
Children also get bounded context instead of a transcript: the session's
|
||||
native-memory hits for the task (when `[memory]` is enabled) are imported
|
||||
into the `codewhale` Agent's memory graph with provenance, and ShannonNet
|
||||
compiles what the child's projected World may see — confidential notes
|
||||
never cross into it — into a short block appended to the child's prompt,
|
||||
with the compiler's information-flow notes and context hash.
|
||||
|
||||
Session end closes the World: dropping the session backend runs a
|
||||
content-addressed checkpoint, destroys the World, and tears down the
|
||||
worker's session container, detached. `/shannon [world|trace|children]`
|
||||
inspects the live session: the Agent, the capabilities projected into the
|
||||
World with their grant depth, the agent tree, and the receipts on the task.
|
||||
|
||||
```toml
|
||||
sandbox_backend = "shannon"
|
||||
sandbox_shannon_home = "~/.shannon" # default: $SHANNON_HOME or ~/.shannon
|
||||
sandbox_shannon_capability = "cap://sandbox/exec" # default
|
||||
sandbox_shannon_sync = true # default
|
||||
```
|
||||
|
||||
The `shannon` binary comes from `$SHANNON` or `PATH`. Isolation belongs to
|
||||
the worker; Codewhale validates the receipt contract. Background, interactive,
|
||||
and TTY modes are unsupported, as with every external backend.
|
||||
Unsupported backend settings refuse shell execution; they never silently select
|
||||
local execution. Choose a supported backend or explicitly select `none`.
|
||||
|
||||
## Policies and fallbacks
|
||||
|
||||
@@ -236,8 +175,6 @@ backend:
|
||||
|
||||
- `CODEWHALE_SANDBOX_MODE`
|
||||
- `CODEWHALE_SANDBOX_BACKEND`
|
||||
- `CODEWHALE_SANDBOX_SHANNON_HOME`, `CODEWHALE_SANDBOX_SHANNON_CAPABILITY`,
|
||||
`CODEWHALE_SANDBOX_SHANNON_SYNC`
|
||||
- `CODEWHALE_SANDBOX_URL`
|
||||
- `CODEWHALE_SANDBOX_API_KEY`
|
||||
|
||||
|
||||
@@ -85,9 +85,9 @@ export const CHANGELOG: ChangelogRelease[] = [
|
||||
"/fleet presents its prioritized core (members, setup, teams, workers, help); every other verb stays dispatchable and is documented under explicit groups in /fleet help. The roster no longer shows the untouched built-in general alias next to worker (#5888).",
|
||||
"codewhale provider: account-backed model access over the provider keys a customer connected to their Codewhale account. One base URL (https://api.codewhale.net/v1, overridable with CODEWHALE_API_BASE; HTTPS required except on loopback), one cwc_key_… account API key with the models:infer scope (CODEWHALE_API_KEY), and a per-model wire chosen from the account's authenticated GET /v1/models: ids are provider/model and each row states chat-completions (/v1/chat/completions) or…",
|
||||
"codewhale account api-keys create --scope now accepts models:infer alongside account:read and agent:run, and an omitted --scope sends all three explicitly. --use saves the new secret as this machine's local codewhale provider credential in the same secret store codewhale auth uses; nothing is uploaded.",
|
||||
"sandbox_backend = \"shannon\": shell commands run as signed ShannonNet capability invocations (cap://sandbox/exec) on a worker that may live on another tailnet node. Codewhale opens a Task World per session for its durable codewhale Agent and every command leaves a receipt in shannon trace. New keys sandbox_shannon_home and sandbox_shannon_capability; tool metadata now reports the actual external backend kind instead of always opensandbox."
|
||||
"Git grows a commit_plan action: a propose-only planner that splits the working tree into ordered atomic commits (#3999). It groups whole files — lock files ride with their manifest, tests ride with the source they name — orders the groups so a commit that defines a symbol lands before the commit that uses it, and refuses the whole plan when that dependency graph has a cycle. It reads git diff HEAD plus the untracked-file list and writes nothing: no git add -N, no git apply…"
|
||||
],
|
||||
"itemCount": 17
|
||||
"itemCount": 12
|
||||
},
|
||||
{
|
||||
"heading": "Contributors",
|
||||
|
||||
Reference in new issue
Block a user