Build App / compute-version (pull_request) Successful in 6s
Build App / build-macos (pull_request) Successful in 2m30s
Build App / build-windows (pull_request) Successful in 5m16s
Build Container / build-container (pull_request) Successful in 10m15s
Build App / build-linux (pull_request) Successful in 6m35s
Build App / create-tag (pull_request) Skipped
Build App / sync-to-github (pull_request) Skipped
Behind a TLS-terminating corporate proxy every HTTPS call inside a container fails — npm, pip, git, curl, the browser-view pane, and Claude Code's own API requests. There was no mechanism at all: installing the certificate by hand inside a container is lost on Reset and had to be repeated per project. A global CA path in AppSettings with a per-project override on Project, taking either a single certificate file or a directory. It is bind-mounted read-only at /tmp/.host-ca (mirroring /tmp/.host-ssh and /tmp/.host-aws) and applied by entrypoint.sh on every start, so it survives recreation, migration and Reset. Four things this gets right that are easy to get wrong: * update-ca-certificates globs *.crt case-sensitively, so a .pem that is merely copied in is ignored in silence. Certificates are renamed, by container_cert_name() in Rust and a mirrored few lines of shell. * The system store only serves curl/git/apt. Node — and so Claude Code itself — needs NODE_EXTRA_CA_CERTS, Python needs REQUESTS_CA_BUNDLE/SSL_CERT_FILE, and Chromium reads neither: it wants ~/.pki/nssdb, seeded with certutil (libnss3-tools, added to the image). * Those vars are set from Rust at creation, never exported by the entrypoint — a terminal is a docker exec and sees nothing the entrypoint exported. They are emitted empty when no CA is configured, since docker commit bakes env into the snapshot image. * triple-c.ca-fingerprint hashes the certificate bytes as well as the path, so a CA rotated in at the same location still forces a recreation. Verified end to end against a real container and a self-signed CA: curl, node, python and git all complete a TLS handshake against a server signed by it and all three fail in the same container without it; the env vars are visible from a docker exec session; the store is cleaned when the setting is cleared. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KSP2KNPhuWKQ4DL5TZEn3k
172 lines
5.8 KiB
TypeScript
172 lines
5.8 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { open } from "@tauri-apps/plugin-dialog";
|
|
import type { Project } from "../../../../lib/types";
|
|
import Button from "../../../ui/Button";
|
|
import Field, { ConfigGroup, inputClass } from "../../../ui/Field";
|
|
import CaCertPathInput from "../../../settings/CaCertPathInput";
|
|
import EnvVarsEditor from "../../EnvVarsEditor";
|
|
import PortMappingsEditor from "../../PortMappingsEditor";
|
|
|
|
interface Props {
|
|
project: Project;
|
|
save: (patch: Partial<Project>) => Promise<boolean>;
|
|
disabled: boolean;
|
|
disabledReason?: string;
|
|
}
|
|
|
|
export default function AccessSection({
|
|
project,
|
|
save,
|
|
disabled,
|
|
disabledReason,
|
|
}: Props) {
|
|
const [sshKeyPath, setSshKeyPath] = useState(project.ssh_key_path ?? "");
|
|
const [caCertPath, setCaCertPath] = useState(project.ca_cert_path ?? "");
|
|
const [gitName, setGitName] = useState(project.git_user_name ?? "");
|
|
const [gitEmail, setGitEmail] = useState(project.git_user_email ?? "");
|
|
const [gitToken, setGitToken] = useState(project.git_token ?? "");
|
|
|
|
useEffect(() => {
|
|
setSshKeyPath(project.ssh_key_path ?? "");
|
|
setCaCertPath(project.ca_cert_path ?? "");
|
|
setGitName(project.git_user_name ?? "");
|
|
setGitEmail(project.git_user_email ?? "");
|
|
setGitToken(project.git_token ?? "");
|
|
}, [project]);
|
|
|
|
return (
|
|
<ConfigGroup
|
|
title="Access"
|
|
description="Credentials, environment, and networking the container is given."
|
|
>
|
|
<Field
|
|
label="SSH key directory"
|
|
hint="Mounted into the container so Claude can authenticate with Git remotes over SSH."
|
|
>
|
|
{(id) => (
|
|
<div className="flex gap-1.5">
|
|
<input
|
|
id={id}
|
|
value={sshKeyPath}
|
|
onChange={(e) => setSshKeyPath(e.target.value)}
|
|
onBlur={() => save({ ssh_key_path: sshKeyPath || null })}
|
|
placeholder="~/.ssh"
|
|
disabled={disabled}
|
|
className={inputClass}
|
|
/>
|
|
<Button
|
|
size="md"
|
|
disabled={disabled}
|
|
onClick={async () => {
|
|
const selected = await open({ directory: true, multiple: false });
|
|
if (typeof selected === "string") {
|
|
setSshKeyPath(selected);
|
|
save({ ssh_key_path: selected });
|
|
}
|
|
}}
|
|
>
|
|
Browse
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</Field>
|
|
|
|
<Field label="Git name" hint="Sets git user.name inside the container for commit authorship.">
|
|
{(id) => (
|
|
<input
|
|
id={id}
|
|
value={gitName}
|
|
onChange={(e) => setGitName(e.target.value)}
|
|
onBlur={() => save({ git_user_name: gitName || null })}
|
|
placeholder="Your Name"
|
|
disabled={disabled}
|
|
className={inputClass}
|
|
/>
|
|
)}
|
|
</Field>
|
|
|
|
<Field label="Git email" hint="Sets git user.email inside the container for commit authorship.">
|
|
{(id) => (
|
|
<input
|
|
id={id}
|
|
value={gitEmail}
|
|
onChange={(e) => setGitEmail(e.target.value)}
|
|
onBlur={() => save({ git_user_email: gitEmail || null })}
|
|
placeholder="you@example.com"
|
|
disabled={disabled}
|
|
className={inputClass}
|
|
/>
|
|
)}
|
|
</Field>
|
|
|
|
<Field
|
|
label="Git HTTPS token"
|
|
hint="A personal access token (e.g. a GitHub PAT) for HTTPS git operations inside the container."
|
|
>
|
|
{(id) => (
|
|
<input
|
|
id={id}
|
|
type="password"
|
|
value={gitToken}
|
|
onChange={(e) => setGitToken(e.target.value)}
|
|
onBlur={() => save({ git_token: gitToken || null })}
|
|
placeholder="ghp_…"
|
|
disabled={disabled}
|
|
className={inputClass}
|
|
/>
|
|
)}
|
|
</Field>
|
|
|
|
<Field
|
|
label="Corporate CA certificate"
|
|
hint="Overrides the global certificate for this project only. A certificate file, or a folder of them, trusted inside the container by curl, git, npm, pip, Chromium and Claude Code."
|
|
>
|
|
{(id) => (
|
|
<CaCertPathInput
|
|
id={id}
|
|
value={caCertPath}
|
|
onChange={setCaCertPath}
|
|
onCommit={(value) => save({ ca_cert_path: value.trim() || null })}
|
|
disabled={disabled}
|
|
placeholder="/etc/ssl/certs/corp-root.pem"
|
|
emptyHint="Using the global certificate from Settings → Certificates."
|
|
inputClassName={`${inputClass} min-w-0`}
|
|
/>
|
|
)}
|
|
</Field>
|
|
|
|
<div className="pt-2 border-t border-[var(--border-color)]">
|
|
<span className="block text-[13px] font-medium text-[var(--text-primary)]">
|
|
Environment variables
|
|
</span>
|
|
<p className="mt-0.5 mb-2 text-xs text-[var(--text-secondary)] leading-snug">
|
|
Injected into this project’s container. These override global variables
|
|
with the same key.
|
|
</p>
|
|
<EnvVarsEditor
|
|
envVars={project.custom_env_vars ?? []}
|
|
disabled={disabled}
|
|
disabledReason={disabledReason}
|
|
onSave={(vars) => save({ custom_env_vars: vars })}
|
|
/>
|
|
</div>
|
|
|
|
<div className="pt-2 border-t border-[var(--border-color)]">
|
|
<span className="block text-[13px] font-medium text-[var(--text-primary)]">
|
|
Port mappings
|
|
</span>
|
|
<p className="mt-0.5 mb-2 text-xs text-[var(--text-secondary)] leading-snug">
|
|
Expose container ports on the host so you can reach dev servers running inside
|
|
the sandbox.
|
|
</p>
|
|
<PortMappingsEditor
|
|
portMappings={project.port_mappings ?? []}
|
|
disabled={disabled}
|
|
disabledReason={disabledReason}
|
|
onSave={(mappings) => save({ port_mappings: mappings })}
|
|
/>
|
|
</div>
|
|
</ConfigGroup>
|
|
);
|
|
}
|