fix(tui): restore global skills under Full Access

Project the saved and cycled Full Access posture into tool trust as documented. Keep OS sandbox posture separate, and route native global skills through load_skill so ordinary file boundaries remain intact.

Co-authored-by: Wenhao Hu <120066335+AnonymousUser443@users.noreply.github.com>
Signed-off-by: Hunter B <hmbown@gmail.com>
This commit is contained in:
Hunter B
2026-07-22 06:40:29 -07:00
parent 2206daba94
commit 4d197626d7
4 changed files with 66 additions and 23 deletions
+3 -1
View File
@@ -1165,7 +1165,7 @@ reviewed plugin snapshots must be opened with `load_skill`.\n\n",
out.push_str(
"\n### How to use skills\n\
- Native skill bodies live on disk at the listed paths. For a reviewed plugin snapshot, use `load_skill` and do not read its mutable source path directly.\n\
- Use `load_skill` to open any skill body by name. This is required for reviewed plugin snapshots and is the preferred path for native skills, including global skills outside the workspace. Direct file reads retain the normal workspace/trust boundary.\n\
- Trigger rules: use a skill when the user names it (`$SkillName`, `/skill <name>`, or plain text) or the task clearly matches its description. Do not carry skills across turns unless re-mentioned.\n\
- Missing/blocked: if a named skill is missing or cannot be read, say so briefly and continue with the best fallback.\n\
- Safety: do not execute scripts from a community skill unless the user explicitly asks or the skill has been trusted for script use.\n",
@@ -1231,6 +1231,8 @@ mod tests {
assert!(rendered.contains("## Skills"));
assert!(rendered.contains("- test-skill: A test skill"));
assert!(rendered.contains("Use `load_skill` to open any skill body by name"));
assert!(rendered.contains("Direct file reads retain the normal workspace/trust boundary"));
assert!(
rendered.contains(&expected_path),
"expected path {expected_path:?} not in rendered output"
+46 -13
View File
@@ -10,19 +10,12 @@
//! would blow the prompt budget the moment a user has half a dozen
//! skills installed.
//!
//! Two paths exist for the model to actually read a native skill:
//!
//! 1. The existing progressive-disclosure pattern: model spots a
//! skill in the catalogue, calls `read_file <path>` from the
//! listing.
//! 2. (this tool) `load_skill name=<id>` — single call, name-based
//! lookup, also enumerates the sibling files in the skill's
//! directory so the model sees the companion resources without
//! a separate `list_dir`.
//!
//! Both are valid for native skills. Reviewed plugin skills are exposed only
//! through this tool's content-bound in-memory snapshot; their mutable source
//! paths and companion files are deliberately not returned.
//! `load_skill name=<id>` is the canonical progressive-disclosure path. It
//! performs a name-based host lookup, so native global skills work without
//! widening the model's workspace file authority, and it enumerates companion
//! files without a separate `list_dir`. Reviewed plugin skills are exposed
//! only through this tool's content-bound in-memory snapshot; their mutable
//! source paths and companion files are deliberately not returned.
use async_trait::async_trait;
use serde_json::{Value, json};
@@ -561,6 +554,46 @@ mod tests {
);
}
#[tokio::test]
#[allow(clippy::await_holding_lock)]
async fn execute_loads_global_codewhale_skill_without_workspace_trust() {
let _env_lock = crate::test_support::lock_test_env();
let tmp = tempdir().unwrap();
let workspace = tmp.path().join("workspace");
let home = tmp.path().join("home");
let global_skills = home.join(".codewhale/skills");
fs::create_dir_all(&workspace).unwrap();
write_skill(
&global_skills,
"global-helper",
"Global helper",
"Global body marker.",
);
let _home = crate::test_support::EnvVarGuard::set("HOME", &home);
let _user_profile = crate::test_support::EnvVarGuard::set("USERPROFILE", &home);
let context = ToolContext::new(&workspace);
assert!(!context.trust_mode);
assert!(
context
.resolve_path(
global_skills
.join("global-helper/SKILL.md")
.to_str()
.unwrap()
)
.is_err(),
"ordinary file tools must retain the workspace boundary"
);
let result = LoadSkillTool
.execute(json!({"name": "global-helper"}), &context)
.await
.expect("load_skill host lookup should open ~/.codewhale/skills");
assert!(result.success);
assert!(result.content.contains("Global body marker."));
}
#[tokio::test]
async fn execute_returns_helpful_error_for_unknown_skill() {
let tmp = tempdir().unwrap();
+15 -9
View File
@@ -3329,13 +3329,14 @@ impl App {
let configured_approval_mode = explicit_approval_mode
.or(saved_permission_posture)
.unwrap_or_default();
let configured_trust_mode = configured_approval_mode == ApprovalMode::Bypass;
let mode_prefs = ModeSessionPrefs {
agent_allow_shell: if yolo_compat || matches!(initial_mode, AppMode::Yolo) {
config.interactive_allow_shell()
} else {
allow_shell
},
agent_trust_mode: false,
agent_trust_mode: configured_trust_mode,
// The YOLO-compat launch elevates the *live* approval mirror to
// Bypass below; the durable Agent baseline keeps the configured
// policy so a YOLO -> Agent downshift restores it.
@@ -3613,7 +3614,7 @@ impl App {
last_known_work_state: None,
current_session_metadata: None,
session_artifacts: Vec::new(),
trust_mode: yolo_compat || initial_mode == AppMode::Yolo,
trust_mode: yolo_compat || initial_mode == AppMode::Yolo || configured_trust_mode,
translation_enabled: false,
status_items: config
.tui
@@ -4206,14 +4207,19 @@ impl App {
);
}
/// Update the durable Act approval choice without changing its saved shell
/// or trust choices. Plan remains read-only.
/// Update the durable Act approval choice. Entering Full Access enables
/// trust mode; leaving it removes that implicit elevation while preserving
/// an independently enabled trust baseline in other posture transitions.
/// Plan remains read-only.
pub fn set_agent_approval_posture(&mut self, next: ApprovalMode) {
self.set_agent_runtime_baseline(
self.mode_prefs.agent_allow_shell,
self.mode_prefs.agent_trust_mode,
next,
);
let trust_mode = if next == ApprovalMode::Bypass {
true
} else if self.mode_prefs.agent_approval_mode == ApprovalMode::Bypass {
false
} else {
self.mode_prefs.agent_trust_mode
};
self.set_agent_runtime_baseline(self.mode_prefs.agent_allow_shell, trust_mode, next);
}
#[must_use]
+2
View File
@@ -2441,10 +2441,12 @@ fn permission_postures_persist_across_restart() {
assert!(app.cycle_approval_posture());
}
assert_eq!(app.approval_mode, expected);
assert_eq!(app.trust_mode, expected == ApprovalMode::Bypass);
let restarted = App::new(options, &Config::default());
assert_eq!(restarted.approval_mode, expected);
assert_eq!(restarted.mode_prefs.agent_approval_mode, expected);
assert_eq!(restarted.trust_mode, expected == ApprovalMode::Bypass);
drop(config_env);
}
}