Ship a pia-vpn skill with the VPN support toggle
The toggle grants CAP_NET_ADMIN and /dev/net/tun and stops there, which users reasonably read as "turn the VPN on" — the gap between the two is the reported bug that the default network does not route through a VPN. Close it by giving the container an agent-usable way to build the tunnel, rather than leaving each project to rediscover it. container/skills/ is baked to /opt/triple-c-skills and installed into ~/.claude/skills/ by entrypoint.sh from VPN_SUPPORT_ENABLED, mirroring how Mission Control installs its own. Staged under /opt because ~/.claude is a volume mount that would mask an image copy from first start. Three details that are not incidental: - The variable is sent as 0 rather than omitted when off, because ~/.claude persists: entrypoint has to be *told* to remove a skill left by an earlier run with the toggle on, and an absent variable cannot say that. A stale skill is worse than none, since it instructs an agent to use a capability the container no longer has. - It is reserved in RESERVED_ENV_EXACT alongside MISSION_CONTROL_ENABLED, or a custom env var of the same name could claim the skill without the capability behind it. Covered by a test. - The skill is re-copied on every start, rm -rf'd first, so fixes reach existing projects and files dropped from a later version do not linger. The skill itself carries the three things that are easy to get wrong: that a full tunnel captures the Docker resolver and takes DNS down with it, that an IP-literal health check cannot see a dead resolver, and that no tunnel survives a restart while /run state riding the snapshot makes it look as though one did. It also states what it deliberately does not do — no killswitch, no autostart — so an agent proposes those as decisions rather than improvising them. pia-wg.sh preflights CAP_NET_ADMIN by capability bit rather than letting the first `ip` call fail with a bare EPERM that points nowhere near the setting that needs changing. Credentials stay in a file (~/pia-creds, PIA_CREDS to override) rather than the environment, where docker inspect and every process in the container would see them. Tested: install/refresh/remove/no-op paths of install_feature_skill against the real function; preflight with and without the capability; and a full up --full / down round trip, confirming DNS via PIA's resolvers, api.anthropic.com reachable through the exit, and routes and resolv.conf restored on teardown. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -233,6 +233,7 @@ const RESERVED_ENV_EXACT: &[&str] = &[
|
||||
"MCP_SERVERS_JSON",
|
||||
"CLAUDE_CODE_SETTINGS_JSON",
|
||||
"MISSION_CONTROL_ENABLED",
|
||||
"VPN_SUPPORT_ENABLED",
|
||||
"TRIPLE_C_PERMISSION_MODE",
|
||||
CLAUDE_OAUTH_TOKEN_ENV,
|
||||
// The model-alias vars are already covered by the `ANTHROPIC_` prefix
|
||||
@@ -1275,6 +1276,15 @@ pub async fn create_container(
|
||||
env_vars.push("MISSION_CONTROL_ENABLED=1".to_string());
|
||||
}
|
||||
|
||||
// Drives the pia-vpn skill install in entrypoint.sh. Sent as 0 rather than
|
||||
// omitted when off, because ~/.claude is a persisted volume: entrypoint has
|
||||
// to be told to *remove* a skill left there by an earlier run with the
|
||||
// toggle on, and an absent variable cannot say that.
|
||||
env_vars.push(format!(
|
||||
"VPN_SUPPORT_ENABLED={}",
|
||||
u8::from(project.vpn_support_enabled)
|
||||
));
|
||||
|
||||
// Permission mode — read by triple-c-task-runner for scheduled (headless)
|
||||
// Claude Code runs. Interactive terminals get the flags directly instead.
|
||||
env_vars.push(format!(
|
||||
@@ -2789,6 +2799,23 @@ mod tests {
|
||||
assert_eq!(cap_add.unwrap(), vec!["NET_ADMIN"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_vpn_skill_flag_is_reserved_from_custom_env() {
|
||||
// entrypoint.sh installs and removes the pia-vpn skill from this
|
||||
// variable. A custom env var of the same name would let a project claim
|
||||
// the skill without the capability behind it — or keep it after the
|
||||
// toggle is off — so it has to be unsettable like the others.
|
||||
assert!(is_reserved_env_key("VPN_SUPPORT_ENABLED"));
|
||||
assert!(is_reserved_env_key("vpn_support_enabled"));
|
||||
assert_eq!(
|
||||
compute_env_fingerprint(&[EnvVar {
|
||||
key: "VPN_SUPPORT_ENABLED".to_string(),
|
||||
value: "1".to_string(),
|
||||
}]),
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
/// What bollard actually hands us when a tun-less host rejects the device.
|
||||
///
|
||||
/// Captured verbatim from Docker 29.7: `docker create` with a missing
|
||||
|
||||
Reference in New Issue
Block a user