Add per-project sandbox mode and Bedrock service-tier
Some checks failed
Build App / compute-version (pull_request) Successful in 2s
Build App / build-macos (pull_request) Successful in 2m31s
Build App / build-windows (pull_request) Successful in 8m1s
Build Container / build-container (pull_request) Successful in 8m11s
Build App / build-linux (pull_request) Failing after 1m53s
Build App / create-tag (pull_request) Has been skipped
Build App / sync-to-github (pull_request) Has been skipped
Some checks failed
Build App / compute-version (pull_request) Successful in 2s
Build App / build-macos (pull_request) Successful in 2m31s
Build App / build-windows (pull_request) Successful in 8m1s
Build Container / build-container (pull_request) Successful in 8m11s
Build App / build-linux (pull_request) Failing after 1m53s
Build App / create-tag (pull_request) Has been skipped
Build App / sync-to-github (pull_request) Has been skipped
Sandbox mode: new per-project toggle that turns on Claude Code's bash sandbox inside the container. Adds `bubblewrap` and `socat` to the Dockerfile (the two Linux deps required by the sandbox), and emits a managed `sandbox` block into `~/.claude/settings.json` via the existing CLAUDE_CODE_SETTINGS_JSON entrypoint merge: - `enabled` mirrors the Triple-C toggle and is always emitted, so the entrypoint's recursive jq merge clears any prior on-state from the persisted named volume — Triple-C is authoritative. - `enableWeakerNestedSandbox: true` because we run inside Docker without privileged user namespaces. - `allowUnsandboxedCommands: false` to disable the `dangerouslyDisableSandbox` escape hatch — opting into the sandbox shouldn't come with a runtime bypass. When sandbox is on, a SANDBOX_INSTRUCTIONS section is appended to CLAUDE_INSTRUCTIONS so Claude can guide users through allowing extra paths/domains, excluding `docker *`/`watchman *` from the sandbox, and the rule that `sandbox.enabled` is owned by Triple-C. The Claude-Code settings fingerprint includes sandbox state (only when on, to avoid spuriously flagging existing containers for recreation on upgrade). Bedrock service tier: new optional field on the per-project Bedrock config. When set, exported as ANTHROPIC_BEDROCK_SERVICE_TIER (added in Claude Code 2.1.122) and included in the Bedrock fingerprint. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -88,6 +88,40 @@ This project uses **Flight Control** (bundled with Triple-C) for structured deve
|
|||||||
3. `.flightops/ARTIFACTS.md` — Where all artifacts are stored
|
3. `.flightops/ARTIFACTS.md` — Where all artifacts are stored
|
||||||
4. `.flightops/agent-crews/` — Project crew definitions for each phase (read the relevant crew file)"#;
|
4. `.flightops/agent-crews/` — Project crew definitions for each phase (read the relevant crew file)"#;
|
||||||
|
|
||||||
|
const SANDBOX_INSTRUCTIONS: &str = r#"## Sandbox Mode
|
||||||
|
|
||||||
|
This container has Claude Code's bash sandbox enabled, managed by Triple-C
|
||||||
|
(toggle it from the project's "Sandbox mode" switch in the Triple-C UI).
|
||||||
|
Bash commands run inside `bubblewrap` with filesystem and network isolation
|
||||||
|
(`enableWeakerNestedSandbox` is on because we are inside Docker).
|
||||||
|
|
||||||
|
### When a command fails because of sandbox restrictions
|
||||||
|
|
||||||
|
Triple-C disables the `dangerouslyDisableSandbox` escape hatch
|
||||||
|
(`allowUnsandboxedCommands: false`), so failing commands cannot bypass the
|
||||||
|
sandbox at runtime. To make a blocked command work, edit
|
||||||
|
`~/.claude/settings.json` and restart Claude Code:
|
||||||
|
|
||||||
|
| Need | Setting |
|
||||||
|
|---|---|
|
||||||
|
| Write to a path outside the project (e.g. `~/.kube`) | Add to `sandbox.filesystem.allowWrite` |
|
||||||
|
| Reach a new domain | Will prompt; or add permanently to `sandbox.allowedDomains` |
|
||||||
|
| Run a specific tool entirely outside the sandbox | Add a glob (e.g. `"docker *"`) to `sandbox.excludedCommands` |
|
||||||
|
|
||||||
|
### Docker commands
|
||||||
|
|
||||||
|
The `docker` CLI does not work inside the sandbox. If this project has
|
||||||
|
"Allow container spawning" enabled in Triple-C and you need to run
|
||||||
|
`docker` commands, add `"docker *"` to `sandbox.excludedCommands` in
|
||||||
|
`~/.claude/settings.json`. Other tools known to be sandbox-incompatible
|
||||||
|
include `watchman` — pass `--no-watchman` to `jest`.
|
||||||
|
|
||||||
|
### Disabling sandbox mode
|
||||||
|
|
||||||
|
Do not change `sandbox.enabled` in `settings.json` — Triple-C overwrites it
|
||||||
|
on every container start. To turn sandbox off, stop the container in
|
||||||
|
Triple-C, flip the "Sandbox mode" switch off, then start the container."#;
|
||||||
|
|
||||||
/// Build the full CLAUDE_INSTRUCTIONS value by merging global + project
|
/// Build the full CLAUDE_INSTRUCTIONS value by merging global + project
|
||||||
/// instructions, appending port mapping docs, and appending scheduler docs.
|
/// instructions, appending port mapping docs, and appending scheduler docs.
|
||||||
/// Used by both create_container() and container_needs_recreation() to ensure
|
/// Used by both create_container() and container_needs_recreation() to ensure
|
||||||
@@ -97,6 +131,7 @@ fn build_claude_instructions(
|
|||||||
project_instructions: Option<&str>,
|
project_instructions: Option<&str>,
|
||||||
port_mappings: &[PortMapping],
|
port_mappings: &[PortMapping],
|
||||||
mission_control_enabled: bool,
|
mission_control_enabled: bool,
|
||||||
|
sandbox_enabled: bool,
|
||||||
) -> Option<String> {
|
) -> Option<String> {
|
||||||
let mut combined = merge_claude_instructions(
|
let mut combined = merge_claude_instructions(
|
||||||
global_instructions,
|
global_instructions,
|
||||||
@@ -126,6 +161,13 @@ fn build_claude_instructions(
|
|||||||
None => SCHEDULER_INSTRUCTIONS.to_string(),
|
None => SCHEDULER_INSTRUCTIONS.to_string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if sandbox_enabled {
|
||||||
|
combined = Some(match combined {
|
||||||
|
Some(existing) => format!("{}\n\n{}", existing, SANDBOX_INSTRUCTIONS),
|
||||||
|
None => SANDBOX_INSTRUCTIONS.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
combined
|
combined
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,6 +269,7 @@ fn compute_bedrock_fingerprint(project: &Project) -> String {
|
|||||||
bedrock.aws_bearer_token.as_deref().unwrap_or("").to_string(),
|
bedrock.aws_bearer_token.as_deref().unwrap_or("").to_string(),
|
||||||
bedrock.model_id.as_deref().unwrap_or("").to_string(),
|
bedrock.model_id.as_deref().unwrap_or("").to_string(),
|
||||||
format!("{}", bedrock.disable_prompt_caching),
|
format!("{}", bedrock.disable_prompt_caching),
|
||||||
|
bedrock.service_tier.as_deref().unwrap_or("").to_string(),
|
||||||
];
|
];
|
||||||
sha256_hex(&parts.join("|"))
|
sha256_hex(&parts.join("|"))
|
||||||
} else {
|
} else {
|
||||||
@@ -312,8 +355,16 @@ fn merge_claude_code_settings(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Compute a fingerprint for the Claude Code settings so we can detect changes.
|
/// Compute a fingerprint for the Claude Code settings so we can detect changes.
|
||||||
fn compute_claude_code_settings_fingerprint(settings: Option<&ClaudeCodeSettings>) -> String {
|
/// The `sandbox_enabled` flag is included so that toggling sandbox mode forces
|
||||||
match settings {
|
/// a container recreation (re-injecting the merged settings.json). When
|
||||||
|
/// sandbox is off the historical fingerprint is preserved unchanged so that
|
||||||
|
/// upgrading triple-c does not spuriously flag every existing container for
|
||||||
|
/// recreation.
|
||||||
|
fn compute_claude_code_settings_fingerprint(
|
||||||
|
settings: Option<&ClaudeCodeSettings>,
|
||||||
|
sandbox_enabled: bool,
|
||||||
|
) -> String {
|
||||||
|
let base_fp = match settings {
|
||||||
None => String::new(),
|
None => String::new(),
|
||||||
Some(s) => {
|
Some(s) => {
|
||||||
let parts = vec![
|
let parts = vec![
|
||||||
@@ -328,30 +379,59 @@ fn compute_claude_code_settings_fingerprint(settings: Option<&ClaudeCodeSettings
|
|||||||
];
|
];
|
||||||
sha256_hex(&parts.join("|"))
|
sha256_hex(&parts.join("|"))
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
if sandbox_enabled {
|
||||||
|
sha256_hex(&format!("{}|sandbox=true", base_fp))
|
||||||
|
} else {
|
||||||
|
base_fp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build the settings.json content for Claude Code from ClaudeCodeSettings.
|
/// Build the settings.json content for Claude Code.
|
||||||
/// Returns a JSON string of the settings to be written to ~/.claude/settings.json.
|
/// Returns a JSON string of the settings to be written to ~/.claude/settings.json.
|
||||||
fn build_claude_code_settings_json(settings: &ClaudeCodeSettings) -> Option<String> {
|
/// Always emits a `sandbox.enabled` key reflecting the current per-project
|
||||||
|
/// toggle so that flipping it off in triple-c overrides any prior on-state
|
||||||
|
/// stored in the persisted settings.json (which lives in a named volume).
|
||||||
|
fn build_claude_code_settings_json(
|
||||||
|
settings: Option<&ClaudeCodeSettings>,
|
||||||
|
sandbox_enabled: bool,
|
||||||
|
) -> Option<String> {
|
||||||
let mut map = serde_json::Map::new();
|
let mut map = serde_json::Map::new();
|
||||||
|
|
||||||
if let Some(ref tui) = settings.tui_mode {
|
if let Some(s) = settings {
|
||||||
map.insert("tui".to_string(), serde_json::json!(tui));
|
if let Some(ref tui) = s.tui_mode {
|
||||||
}
|
map.insert("tui".to_string(), serde_json::json!(tui));
|
||||||
if let Some(ref effort) = settings.effort {
|
}
|
||||||
map.insert("effort".to_string(), serde_json::json!(effort));
|
if let Some(ref effort) = s.effort {
|
||||||
}
|
map.insert("effort".to_string(), serde_json::json!(effort));
|
||||||
if settings.auto_scroll_disabled {
|
}
|
||||||
map.insert("autoScrollEnabled".to_string(), serde_json::json!(false));
|
if s.auto_scroll_disabled {
|
||||||
}
|
map.insert("autoScrollEnabled".to_string(), serde_json::json!(false));
|
||||||
if settings.focus_mode {
|
}
|
||||||
map.insert("focusMode".to_string(), serde_json::json!(true));
|
if s.focus_mode {
|
||||||
}
|
map.insert("focusMode".to_string(), serde_json::json!(true));
|
||||||
if settings.show_thinking_summaries {
|
}
|
||||||
map.insert("showThinkingSummaries".to_string(), serde_json::json!(true));
|
if s.show_thinking_summaries {
|
||||||
|
map.insert("showThinkingSummaries".to_string(), serde_json::json!(true));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Always emit `sandbox.enabled` so that toggling the per-project sandbox
|
||||||
|
// off in triple-c clears any prior on-state in the persisted
|
||||||
|
// settings.json (which lives in a named volume that survives recreation).
|
||||||
|
// Inside a Docker container we can't rely on privileged user namespaces,
|
||||||
|
// so `enableWeakerNestedSandbox` is required when sandbox is on.
|
||||||
|
let sandbox_obj = if sandbox_enabled {
|
||||||
|
serde_json::json!({
|
||||||
|
"enabled": true,
|
||||||
|
"enableWeakerNestedSandbox": true,
|
||||||
|
"allowUnsandboxedCommands": false,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
serde_json::json!({ "enabled": false })
|
||||||
|
};
|
||||||
|
map.insert("sandbox".to_string(), sandbox_obj);
|
||||||
|
|
||||||
if map.is_empty() {
|
if map.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
@@ -586,6 +666,13 @@ pub async fn create_container(
|
|||||||
if bedrock.disable_prompt_caching {
|
if bedrock.disable_prompt_caching {
|
||||||
env_vars.push("DISABLE_PROMPT_CACHING=1".to_string());
|
env_vars.push("DISABLE_PROMPT_CACHING=1".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(ref tier) = bedrock.service_tier {
|
||||||
|
let trimmed = tier.trim();
|
||||||
|
if !trimmed.is_empty() {
|
||||||
|
env_vars.push(format!("ANTHROPIC_BEDROCK_SERVICE_TIER={}", trimmed));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -652,6 +739,7 @@ pub async fn create_container(
|
|||||||
project.claude_instructions.as_deref(),
|
project.claude_instructions.as_deref(),
|
||||||
&project.port_mappings,
|
&project.port_mappings,
|
||||||
project.mission_control_enabled,
|
project.mission_control_enabled,
|
||||||
|
project.sandbox_mode_enabled,
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Some(ref instructions) = combined_instructions {
|
if let Some(ref instructions) = combined_instructions {
|
||||||
@@ -683,11 +771,16 @@ pub async fn create_container(
|
|||||||
if cc.prompt_caching_1h {
|
if cc.prompt_caching_1h {
|
||||||
env_vars.push("ENABLE_PROMPT_CACHING_1H=1".to_string());
|
env_vars.push("ENABLE_PROMPT_CACHING_1H=1".to_string());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// settings.json-based settings (written by the entrypoint)
|
// settings.json-based settings (written by the entrypoint).
|
||||||
if let Some(settings_json) = build_claude_code_settings_json(cc) {
|
// Always invoked so per-project sandbox state is injected even when no
|
||||||
env_vars.push(format!("CLAUDE_CODE_SETTINGS_JSON={}", settings_json));
|
// ClaudeCodeSettings struct is present.
|
||||||
}
|
if let Some(settings_json) = build_claude_code_settings_json(
|
||||||
|
merged_cc_settings.as_ref(),
|
||||||
|
project.sandbox_mode_enabled,
|
||||||
|
) {
|
||||||
|
env_vars.push(format!("CLAUDE_CODE_SETTINGS_JSON={}", settings_json));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut mounts: Vec<Mount> = Vec::new();
|
let mut mounts: Vec<Mount> = Vec::new();
|
||||||
@@ -821,7 +914,7 @@ pub async fn create_container(
|
|||||||
labels.insert("triple-c.mission-control".to_string(), project.mission_control_enabled.to_string());
|
labels.insert("triple-c.mission-control".to_string(), project.mission_control_enabled.to_string());
|
||||||
labels.insert("triple-c.custom-env-fingerprint".to_string(), custom_env_fingerprint.clone());
|
labels.insert("triple-c.custom-env-fingerprint".to_string(), custom_env_fingerprint.clone());
|
||||||
labels.insert("triple-c.claude-code-settings-fingerprint".to_string(),
|
labels.insert("triple-c.claude-code-settings-fingerprint".to_string(),
|
||||||
compute_claude_code_settings_fingerprint(merged_cc_settings.as_ref()));
|
compute_claude_code_settings_fingerprint(merged_cc_settings.as_ref(), project.sandbox_mode_enabled));
|
||||||
labels.insert("triple-c.instructions-fingerprint".to_string(),
|
labels.insert("triple-c.instructions-fingerprint".to_string(),
|
||||||
combined_instructions.as_ref().map(|s| sha256_hex(s)).unwrap_or_default());
|
combined_instructions.as_ref().map(|s| sha256_hex(s)).unwrap_or_default());
|
||||||
labels.insert("triple-c.git-user-name".to_string(), effective_git_name.unwrap_or_default().to_string());
|
labels.insert("triple-c.git-user-name".to_string(), effective_git_name.unwrap_or_default().to_string());
|
||||||
@@ -1179,6 +1272,7 @@ pub async fn container_needs_recreation(
|
|||||||
project.claude_instructions.as_deref(),
|
project.claude_instructions.as_deref(),
|
||||||
&project.port_mappings,
|
&project.port_mappings,
|
||||||
project.mission_control_enabled,
|
project.mission_control_enabled,
|
||||||
|
project.sandbox_mode_enabled,
|
||||||
);
|
);
|
||||||
let expected_instructions_fp = expected_instructions.as_ref().map(|s| sha256_hex(s)).unwrap_or_default();
|
let expected_instructions_fp = expected_instructions.as_ref().map(|s| sha256_hex(s)).unwrap_or_default();
|
||||||
let container_instructions_fp = get_label("triple-c.instructions-fingerprint").unwrap_or_default();
|
let container_instructions_fp = get_label("triple-c.instructions-fingerprint").unwrap_or_default();
|
||||||
@@ -1192,7 +1286,7 @@ pub async fn container_needs_recreation(
|
|||||||
global_claude_code_settings,
|
global_claude_code_settings,
|
||||||
project.claude_code_settings.as_ref(),
|
project.claude_code_settings.as_ref(),
|
||||||
);
|
);
|
||||||
let expected_cc_fp = compute_claude_code_settings_fingerprint(merged_cc.as_ref());
|
let expected_cc_fp = compute_claude_code_settings_fingerprint(merged_cc.as_ref(), project.sandbox_mode_enabled);
|
||||||
let container_cc_fp = get_label("triple-c.claude-code-settings-fingerprint").unwrap_or_default();
|
let container_cc_fp = get_label("triple-c.claude-code-settings-fingerprint").unwrap_or_default();
|
||||||
if container_cc_fp != expected_cc_fp {
|
if container_cc_fp != expected_cc_fp {
|
||||||
log::info!("Claude Code settings mismatch (container={:?}, expected={:?})", container_cc_fp, expected_cc_fp);
|
log::info!("Claude Code settings mismatch (container={:?}, expected={:?})", container_cc_fp, expected_cc_fp);
|
||||||
|
|||||||
@@ -73,6 +73,8 @@ pub struct Project {
|
|||||||
pub openai_compatible_config: Option<OpenAiCompatibleConfig>,
|
pub openai_compatible_config: Option<OpenAiCompatibleConfig>,
|
||||||
pub allow_docker_access: bool,
|
pub allow_docker_access: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
pub sandbox_mode_enabled: bool,
|
||||||
|
#[serde(default)]
|
||||||
pub mission_control_enabled: bool,
|
pub mission_control_enabled: bool,
|
||||||
#[serde(default = "default_full_permissions")]
|
#[serde(default = "default_full_permissions")]
|
||||||
pub full_permissions: bool,
|
pub full_permissions: bool,
|
||||||
@@ -159,6 +161,10 @@ pub struct BedrockConfig {
|
|||||||
pub aws_bearer_token: Option<String>,
|
pub aws_bearer_token: Option<String>,
|
||||||
pub model_id: Option<String>,
|
pub model_id: Option<String>,
|
||||||
pub disable_prompt_caching: bool,
|
pub disable_prompt_caching: bool,
|
||||||
|
/// Optional value for the `ANTHROPIC_BEDROCK_SERVICE_TIER` env var
|
||||||
|
/// (e.g. "priority"). Empty/None means leave unset.
|
||||||
|
#[serde(default)]
|
||||||
|
pub service_tier: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ollama configuration for a project.
|
/// Ollama configuration for a project.
|
||||||
@@ -199,6 +205,7 @@ impl Project {
|
|||||||
ollama_config: None,
|
ollama_config: None,
|
||||||
openai_compatible_config: None,
|
openai_compatible_config: None,
|
||||||
allow_docker_access: false,
|
allow_docker_access: false,
|
||||||
|
sandbox_mode_enabled: false,
|
||||||
mission_control_enabled: false,
|
mission_control_enabled: false,
|
||||||
full_permissions: false,
|
full_permissions: false,
|
||||||
ssh_key_path: None,
|
ssh_key_path: None,
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
const [bedrockProfile, setBedrockProfile] = useState(project.bedrock_config?.aws_profile ?? "");
|
const [bedrockProfile, setBedrockProfile] = useState(project.bedrock_config?.aws_profile ?? "");
|
||||||
const [bedrockBearerToken, setBedrockBearerToken] = useState(project.bedrock_config?.aws_bearer_token ?? "");
|
const [bedrockBearerToken, setBedrockBearerToken] = useState(project.bedrock_config?.aws_bearer_token ?? "");
|
||||||
const [bedrockModelId, setBedrockModelId] = useState(project.bedrock_config?.model_id ?? "");
|
const [bedrockModelId, setBedrockModelId] = useState(project.bedrock_config?.model_id ?? "");
|
||||||
|
const [bedrockServiceTier, setBedrockServiceTier] = useState(project.bedrock_config?.service_tier ?? "");
|
||||||
|
|
||||||
// Ollama local state
|
// Ollama local state
|
||||||
const [ollamaBaseUrl, setOllamaBaseUrl] = useState(project.ollama_config?.base_url ?? "http://host.docker.internal:11434");
|
const [ollamaBaseUrl, setOllamaBaseUrl] = useState(project.ollama_config?.base_url ?? "http://host.docker.internal:11434");
|
||||||
@@ -88,6 +89,7 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
setBedrockProfile(project.bedrock_config?.aws_profile ?? "");
|
setBedrockProfile(project.bedrock_config?.aws_profile ?? "");
|
||||||
setBedrockBearerToken(project.bedrock_config?.aws_bearer_token ?? "");
|
setBedrockBearerToken(project.bedrock_config?.aws_bearer_token ?? "");
|
||||||
setBedrockModelId(project.bedrock_config?.model_id ?? "");
|
setBedrockModelId(project.bedrock_config?.model_id ?? "");
|
||||||
|
setBedrockServiceTier(project.bedrock_config?.service_tier ?? "");
|
||||||
setOllamaBaseUrl(project.ollama_config?.base_url ?? "http://host.docker.internal:11434");
|
setOllamaBaseUrl(project.ollama_config?.base_url ?? "http://host.docker.internal:11434");
|
||||||
setOllamaModelId(project.ollama_config?.model_id ?? "");
|
setOllamaModelId(project.ollama_config?.model_id ?? "");
|
||||||
setOpenaiCompatibleBaseUrl(project.openai_compatible_config?.base_url ?? "http://host.docker.internal:4000");
|
setOpenaiCompatibleBaseUrl(project.openai_compatible_config?.base_url ?? "http://host.docker.internal:4000");
|
||||||
@@ -192,6 +194,7 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
aws_bearer_token: null,
|
aws_bearer_token: null,
|
||||||
model_id: null,
|
model_id: null,
|
||||||
disable_prompt_caching: false,
|
disable_prompt_caching: false,
|
||||||
|
service_tier: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultOllamaConfig: OllamaConfig = {
|
const defaultOllamaConfig: OllamaConfig = {
|
||||||
@@ -339,6 +342,16 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleBedrockServiceTierBlur = async () => {
|
||||||
|
try {
|
||||||
|
const current = project.bedrock_config ?? defaultBedrockConfig;
|
||||||
|
const trimmed = bedrockServiceTier.trim();
|
||||||
|
await update({ ...project, bedrock_config: { ...current, service_tier: trimmed || null } });
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to update Bedrock service tier:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleOllamaBaseUrlBlur = async () => {
|
const handleOllamaBaseUrlBlur = async () => {
|
||||||
try {
|
try {
|
||||||
const current = project.ollama_config ?? defaultOllamaConfig;
|
const current = project.ollama_config ?? defaultOllamaConfig;
|
||||||
@@ -692,6 +705,28 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Sandbox mode toggle */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<label className="text-xs text-[var(--text-secondary)]">Sandbox mode<Tooltip text="Enables Claude Code's bash sandbox (bubblewrap-based filesystem and network isolation). Triple-C is the source of truth for the on/off state — toggling this overrides any manual /sandbox configuration in the container's settings.json on next start. Uses enableWeakerNestedSandbox since the container runs without privileged user namespaces." /></label>
|
||||||
|
<button
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
await update({ ...project, sandbox_mode_enabled: !project.sandbox_mode_enabled });
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to update sandbox mode setting:", err);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={!isStopped}
|
||||||
|
className={`px-2 py-0.5 text-xs rounded transition-colors disabled:opacity-50 ${
|
||||||
|
project.sandbox_mode_enabled
|
||||||
|
? "bg-[var(--success)] text-white"
|
||||||
|
: "bg-[var(--bg-primary)] border border-[var(--border-color)] text-[var(--text-secondary)]"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{project.sandbox_mode_enabled ? "ON" : "OFF"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Mission Control toggle */}
|
{/* Mission Control toggle */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<label className="text-xs text-[var(--text-secondary)]">Mission Control<Tooltip text="Enables a web dashboard for monitoring and managing Claude sessions remotely." /></label>
|
<label className="text-xs text-[var(--text-secondary)]">Mission Control<Tooltip text="Enables a web dashboard for monitoring and managing Claude sessions remotely." /></label>
|
||||||
@@ -953,6 +988,19 @@ export default function ProjectCard({ project }: Props) {
|
|||||||
className={inputCls}
|
className={inputCls}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Service tier */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs text-[var(--text-secondary)] mb-0.5">Service Tier (optional)<Tooltip text="Sets ANTHROPIC_BEDROCK_SERVICE_TIER. Valid values are determined by AWS Bedrock (e.g. 'priority'). Leave blank for the account default." /></label>
|
||||||
|
<input
|
||||||
|
value={bedrockServiceTier}
|
||||||
|
onChange={(e) => setBedrockServiceTier(e.target.value)}
|
||||||
|
onBlur={handleBedrockServiceTierBlur}
|
||||||
|
placeholder="(default)"
|
||||||
|
disabled={!isStopped}
|
||||||
|
className={inputCls}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export interface Project {
|
|||||||
ollama_config: OllamaConfig | null;
|
ollama_config: OllamaConfig | null;
|
||||||
openai_compatible_config: OpenAiCompatibleConfig | null;
|
openai_compatible_config: OpenAiCompatibleConfig | null;
|
||||||
allow_docker_access: boolean;
|
allow_docker_access: boolean;
|
||||||
|
sandbox_mode_enabled: boolean;
|
||||||
mission_control_enabled: boolean;
|
mission_control_enabled: boolean;
|
||||||
full_permissions: boolean;
|
full_permissions: boolean;
|
||||||
ssh_key_path: string | null;
|
ssh_key_path: string | null;
|
||||||
@@ -61,6 +62,7 @@ export interface BedrockConfig {
|
|||||||
aws_bearer_token: string | null;
|
aws_bearer_token: string | null;
|
||||||
model_id: string | null;
|
model_id: string | null;
|
||||||
disable_prompt_caching: boolean;
|
disable_prompt_caching: boolean;
|
||||||
|
service_tier: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OllamaConfig {
|
export interface OllamaConfig {
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ RUN for i in 1 2 3 4 5; do \
|
|||||||
pkg-config \
|
pkg-config \
|
||||||
libssl-dev \
|
libssl-dev \
|
||||||
cron \
|
cron \
|
||||||
|
bubblewrap \
|
||||||
|
socat \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Remove default ubuntu user to free UID 1000 for host-user remapping
|
# Remove default ubuntu user to free UID 1000 for host-user remapping
|
||||||
|
|||||||
Reference in New Issue
Block a user