Add permission modes and container introspection backend

Permission modes: replaces the binary full_permissions flag with a
PermissionMode enum (Plan/Default/AcceptEdits/Bypass). Flag mapping is
defined once in PermissionMode::cli_args() and used by the terminal, the
web terminal, and the scheduler:
  Plan        -> --permission-mode plan
  Default     -> (no flag)
  AcceptEdits -> --permission-mode acceptEdits
  Bypass      -> --dangerously-skip-permissions
Choices verified against `claude --permission-mode` on 2.1.226.

full_permissions is retained and effective_permission_mode() falls back
to it, so existing projects.json needs no migration.

Bug fix: triple-c-task-runner ran `claude -p ... --dangerously-skip-
permissions` unconditionally, ignoring the project's setting entirely.
It now reads TRIPLE_C_PERMISSION_MODE, which is injected into the
container, added to the reserved env blocklist, propagated through the
entrypoint's cron env filter, and tracked by a new
triple-c.permission-mode label so a change forces recreation.

Introspection: new commands/inspect_commands.rs exposes read-only views
into the container over docker exec — Claude sessions (parsed from
~/.claude/projects/<cwd>/<uuid>.jsonl), installed capabilities (skills,
agents, commands, hooks, plugins, natively-configured MCP servers), and
the triple-c-scheduler task list, logs and notifications.

Task/session ids are validated against a strict allowlist and every
parameterized call runs as a bare argv vector via bollard, so no shell
is involved. Stopped containers return empty results rather than errors.

No UI yet; that lands with the Project Home view.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-09 10:51:34 -07:00
co-authored by Claude Opus 5
parent d0bb631d4d
commit 0ac4e5030c
11 changed files with 1193 additions and 21 deletions
@@ -17,11 +17,11 @@ fn build_terminal_cmd(project: &Project, state: &AppState, session_name: Option<
.map(|b| b.auth_method == BedrockAuthMethod::Profile)
.unwrap_or(false);
let permission_args = project.effective_permission_mode().cli_args();
if !is_bedrock_profile {
let mut cmd = vec!["claude".to_string()];
if project.full_permissions {
cmd.push("--dangerously-skip-permissions".to_string());
}
cmd.extend(permission_args);
if let Some(name) = session_name {
if !name.is_empty() {
cmd.push("-n".to_string());
@@ -42,11 +42,13 @@ fn build_terminal_cmd(project: &Project, state: &AppState, session_name: Option<
.filter(|n| !n.is_empty())
.map(|n| format!(" -n '{}'", n.replace('\'', "'\\''")))
.unwrap_or_default();
let claude_cmd = if project.full_permissions {
format!("exec claude --dangerously-skip-permissions{}", name_flag)
} else {
format!("exec claude{}", name_flag)
};
// The args are interpolated into a shell script string, so single-quote
// each one (same escaping style as name_flag above).
let permission_flags: String = permission_args
.iter()
.map(|a| format!(" '{}'", a.replace('\'', "'\\''")))
.collect();
let claude_cmd = format!("exec claude{}{}", permission_flags, name_flag);
let script = format!(
r#"