Remove orphaned k8s artifacts (Docker-only now)
ci / gates (push) Successful in 7s
ci / rust (push) Failing after 20s
ci / frontend (push) Successful in 24s
ci / e2e (push) Has been skipped

CI no longer references k8s; delete the dead k8s surface:
- deploy/helm/ (the chart), ci/check-helm.sh, scripts/netpol-cluster.sh
- cm-sandbox: the feature-gated K8sDriver (src/k8s.rs) + k8s_security test +
  the `k8s`/`k8s-tests` features + the optional kube/k8s-openapi/rustls deps
  (Cargo.lock drops the kube-rs tree). Nothing outside cm-sandbox referenced it.

Docker (bollard) DockerDriver is the sole sandbox driver. cm-sandbox + cm-runtime
compile, fmt + clippy clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
Omar Sobh
2026-06-26 18:55:17 -07:00
co-authored by Claude Opus 4.8
parent 3554a3aaf2
commit ce7a5d9aed
18 changed files with 10 additions and 2236 deletions
Generated
+9 -309
View File
@@ -470,7 +470,7 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
"rand 0.9.4", "rand 0.9.4",
"rustls", "rustls",
"rustls-native-certs 0.8.4", "rustls-native-certs",
"rustls-pemfile", "rustls-pemfile",
"rustls-pki-types", "rustls-pki-types",
"serde", "serde",
@@ -1156,10 +1156,7 @@ dependencies = [
"async-trait", "async-trait",
"bollard", "bollard",
"futures", "futures",
"k8s-openapi",
"kube",
"reqwest 0.12.28", "reqwest 0.12.28",
"rustls",
"serde", "serde",
"serde_json", "serde_json",
"thiserror 2.0.18", "thiserror 2.0.18",
@@ -1309,16 +1306,6 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"
[[package]]
name = "core-foundation"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]] [[package]]
name = "core-foundation" name = "core-foundation"
version = "0.10.1" version = "0.10.1"
@@ -1574,27 +1561,6 @@ dependencies = [
"serde_core", "serde_core",
] ]
[[package]]
name = "derive_more"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134"
dependencies = [
"derive_more-impl",
]
[[package]]
name = "derive_more-impl"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
dependencies = [
"proc-macro2",
"quote",
"rustc_version",
"syn",
]
[[package]] [[package]]
name = "digest" name = "digest"
version = "0.10.7" version = "0.10.7"
@@ -2209,30 +2175,6 @@ dependencies = [
"hashbrown 0.15.5", "hashbrown 0.15.5",
] ]
[[package]]
name = "headers"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb"
dependencies = [
"base64",
"bytes",
"headers-core",
"http",
"httpdate",
"mime",
"sha1",
]
[[package]]
name = "headers-core"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4"
dependencies = [
"http",
]
[[package]] [[package]]
name = "heck" name = "heck"
version = "0.5.0" version = "0.5.0"
@@ -2345,26 +2287,6 @@ dependencies = [
"want", "want",
] ]
[[package]]
name = "hyper-http-proxy"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ad4b0a1e37510028bc4ba81d0e38d239c39671b0f0ce9e02dfa93a8133f7c08"
dependencies = [
"bytes",
"futures-util",
"headers",
"http",
"hyper",
"hyper-rustls",
"hyper-util",
"pin-project-lite",
"rustls-native-certs 0.7.3",
"tokio",
"tokio-rustls",
"tower-service",
]
[[package]] [[package]]
name = "hyper-named-pipe" name = "hyper-named-pipe"
version = "0.1.0" version = "0.1.0"
@@ -2389,9 +2311,8 @@ dependencies = [
"http", "http",
"hyper", "hyper",
"hyper-util", "hyper-util",
"log",
"rustls", "rustls",
"rustls-native-certs 0.8.4", "rustls-native-certs",
"tokio", "tokio",
"tokio-rustls", "tokio-rustls",
"tower-service", "tower-service",
@@ -2743,19 +2664,6 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "jsonpath-rust"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c00ae348f9f8fd2d09f82a98ca381c60df9e0820d8d79fce43e649b4dc3128b"
dependencies = [
"pest",
"pest_derive",
"regex",
"serde_json",
"thiserror 2.0.18",
]
[[package]] [[package]]
name = "jsonwebtoken" name = "jsonwebtoken"
version = "9.3.1" version = "9.3.1"
@@ -2771,84 +2679,6 @@ dependencies = [
"simple_asn1", "simple_asn1",
] ]
[[package]]
name = "k8s-openapi"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa60a41b57ae1a0a071af77dbcf89fc9819cfe66edaf2beeb204c34459dcf0b2"
dependencies = [
"base64",
"chrono",
"serde",
"serde_json",
]
[[package]]
name = "kube"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "778f98664beaf4c3c11372721e14310d1ae00f5e2d9aabcf8906c881aa4e9f51"
dependencies = [
"k8s-openapi",
"kube-client",
"kube-core",
]
[[package]]
name = "kube-client"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cb276b85b6e94ded00ac8ea2c68fcf4697ea0553cb25fddc35d4a0ab718db8d"
dependencies = [
"base64",
"bytes",
"chrono",
"either",
"futures",
"home",
"http",
"http-body",
"http-body-util",
"hyper",
"hyper-http-proxy",
"hyper-rustls",
"hyper-timeout",
"hyper-util",
"jsonpath-rust",
"k8s-openapi",
"kube-core",
"pem",
"rustls",
"secrecy",
"serde",
"serde_json",
"serde_yaml",
"thiserror 2.0.18",
"tokio",
"tokio-tungstenite 0.26.2",
"tokio-util",
"tower",
"tower-http",
"tracing",
]
[[package]]
name = "kube-core"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3c56ff45deb0031f2a476017eed60c06872251f271b8387ad8020b8fef60960"
dependencies = [
"chrono",
"derive_more",
"form_urlencoded",
"http",
"k8s-openapi",
"serde",
"serde-value",
"serde_json",
"thiserror 2.0.18",
]
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.5.0" version = "1.5.0"
@@ -3324,12 +3154,6 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]]
name = "openssl-probe"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]] [[package]]
name = "openssl-probe" name = "openssl-probe"
version = "0.2.1" version = "0.2.1"
@@ -3406,15 +3230,6 @@ dependencies = [
"thiserror 2.0.18", "thiserror 2.0.18",
] ]
[[package]]
name = "ordered-float"
version = "2.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c"
dependencies = [
"num-traits",
]
[[package]] [[package]]
name = "p256" name = "p256"
version = "0.13.2" version = "0.13.2"
@@ -3552,49 +3367,6 @@ version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "pest"
version = "2.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662"
dependencies = [
"memchr",
"ucd-trie",
]
[[package]]
name = "pest_derive"
version = "2.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77"
dependencies = [
"pest",
"pest_generator",
]
[[package]]
name = "pest_generator"
version = "2.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f"
dependencies = [
"pest",
"pest_meta",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "pest_meta"
version = "2.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220"
dependencies = [
"pest",
"sha2",
]
[[package]] [[package]]
name = "pin-project" name = "pin-project"
version = "1.1.13" version = "1.1.13"
@@ -4173,7 +3945,7 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
"quinn", "quinn",
"rustls", "rustls",
"rustls-native-certs 0.8.4", "rustls-native-certs",
"rustls-pki-types", "rustls-pki-types",
"serde", "serde",
"serde_json", "serde_json",
@@ -4377,29 +4149,16 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "rustls-native-certs"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5"
dependencies = [
"openssl-probe 0.1.6",
"rustls-pemfile",
"rustls-pki-types",
"schannel",
"security-framework 2.11.1",
]
[[package]] [[package]]
name = "rustls-native-certs" name = "rustls-native-certs"
version = "0.8.4" version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d"
dependencies = [ dependencies = [
"openssl-probe 0.2.1", "openssl-probe",
"rustls-pki-types", "rustls-pki-types",
"schannel", "schannel",
"security-framework 3.7.0", "security-framework",
] ]
[[package]] [[package]]
@@ -4427,16 +4186,16 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784"
dependencies = [ dependencies = [
"core-foundation 0.10.1", "core-foundation",
"core-foundation-sys", "core-foundation-sys",
"jni", "jni",
"log", "log",
"once_cell", "once_cell",
"rustls", "rustls",
"rustls-native-certs 0.8.4", "rustls-native-certs",
"rustls-platform-verifier-android", "rustls-platform-verifier-android",
"rustls-webpki", "rustls-webpki",
"security-framework 3.7.0", "security-framework",
"security-framework-sys", "security-framework-sys",
"webpki-root-certs", "webpki-root-certs",
"windows-sys 0.61.2", "windows-sys 0.61.2",
@@ -4558,28 +4317,6 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "secrecy"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a"
dependencies = [
"zeroize",
]
[[package]]
name = "security-framework"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [
"bitflags 2.13.0",
"core-foundation 0.9.4",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]] [[package]]
name = "security-framework" name = "security-framework"
version = "3.7.0" version = "3.7.0"
@@ -4587,7 +4324,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
dependencies = [ dependencies = [
"bitflags 2.13.0", "bitflags 2.13.0",
"core-foundation 0.10.1", "core-foundation",
"core-foundation-sys", "core-foundation-sys",
"libc", "libc",
"security-framework-sys", "security-framework-sys",
@@ -4619,16 +4356,6 @@ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]]
name = "serde-value"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c"
dependencies = [
"ordered-float",
"serde",
]
[[package]] [[package]]
name = "serde_core" name = "serde_core"
version = "1.0.228" version = "1.0.228"
@@ -4737,19 +4464,6 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "serde_yaml"
version = "0.9.34+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
dependencies = [
"indexmap 2.14.0",
"itoa",
"ryu",
"serde",
"unsafe-libyaml",
]
[[package]] [[package]]
name = "serial" name = "serial"
version = "0.4.0" version = "0.4.0"
@@ -5658,13 +5372,11 @@ version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
dependencies = [ dependencies = [
"base64",
"bitflags 2.13.0", "bitflags 2.13.0",
"bytes", "bytes",
"futures-util", "futures-util",
"http", "http",
"http-body", "http-body",
"mime",
"pin-project-lite", "pin-project-lite",
"tower", "tower",
"tower-layer", "tower-layer",
@@ -5837,12 +5549,6 @@ version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
[[package]]
name = "ucd-trie"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
[[package]] [[package]]
name = "ulid" name = "ulid"
version = "1.2.1" version = "1.2.1"
@@ -5917,12 +5623,6 @@ dependencies = [
"subtle", "subtle",
] ]
[[package]]
name = "unsafe-libyaml"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]] [[package]]
name = "untrusted" name = "untrusted"
version = "0.9.0" version = "0.9.0"
-61
View File
@@ -1,61 +0,0 @@
#!/usr/bin/env bash
# Lints the chart and asserts the load-bearing rendered properties: the
# broker rides as a server sidecar sharing the socket volume, SSE is
# unbuffered at the ingress, and pods run hardened.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
CHART="$ROOT/deploy/helm/clawmates"
helm lint "$CHART" \
--set auth.issuerUrl=https://idp.example.com \
--set oauth.redirectBase=https://app.clawmates.work
RENDERED=$(helm template clawmates "$CHART" \
--set auth.issuerUrl=https://idp.example.com \
--set oauth.redirectBase=https://app.clawmates.work)
require() {
if ! grep -qF -- "$1" <<<"$RENDERED"; then
echo "FAIL: rendered chart is missing: $1"
exit 1
fi
}
# Broker sidecar shares the unix-socket emptyDir with the server.
require 'name: broker'
require 'value: /run/clawmates/broker.sock'
require 'mountPath: /run/clawmates'
# SSE must not buffer at the ingress.
require 'nginx.ingress.kubernetes.io/proxy-buffering: "off"'
# Hardened pods.
require 'runAsNonRoot: true'
require 'drop: ["ALL"]'
require 'readOnlyRootFilesystem: true'
# Config wired through the ConfigMap.
require 'socket_path = "/run/clawmates/broker.sock"'
# The chart-shipped seccomp profile must BE the Docker driver's profile.
if ! diff -q "$ROOT/images/seccomp/agent-profile.json" \
"$CHART/files/agent-profile.json" >/dev/null; then
echo "FAIL: chart seccomp profile diverged from images/seccomp"
exit 1
fi
HARDENED=$(helm template clawmates "$CHART" \
--set auth.issuerUrl=https://idp.example.com \
--set oauth.redirectBase=https://app.clawmates.work \
--set sandbox.seccomp=localhost \
--set server.autoscaling.enabled=true)
for needle in \
'kind: DaemonSet' \
'clawmates-agent-profile.json' \
'kind: HorizontalPodAutoscaler' \
'averageUtilization: 70'; do
if ! grep -qF -- "$needle" <<<"$HARDENED"; then
echo "FAIL: hardened render is missing: $needle"
exit 1
fi
done
echo "helm chart OK"
-10
View File
@@ -10,21 +10,11 @@ publish.workspace = true
async-trait = "0.1" async-trait = "0.1"
bollard = "0.19" bollard = "0.19"
futures = "0.3" futures = "0.3"
k8s-openapi = { version = "0.25", features = ["latest"], optional = true }
kube = { version = "1", features = ["client", "rustls-tls", "ws"], default-features = false, optional = true }
rustls = { version = "0.23", features = ["ring"], default-features = false, optional = true }
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
[features]
# The Kubernetes driver (kube-rs is a heavy dependency tree; the Docker
# driver alone serves dev and the air-gapped target).
k8s = ["dep:kube", "dep:k8s-openapi", "dep:rustls"]
# Live kind-cluster tests for the K8s driver (dedicated CI job).
k8s-tests = ["k8s"]
[dev-dependencies] [dev-dependencies]
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
-330
View File
@@ -1,330 +0,0 @@
//! Kubernetes implementation of the sandbox driver (cloud target): one
//! hardened pod per sandbox in a dedicated namespace with a default-deny
//! NetworkPolicy.
//!
//! Hardening parity with the Docker driver: uid 10001, cap-drop ALL,
//! no-new-privileges, read-only rootfs with emptyDir /tmp and /home/agent,
//! resource limits. Differences, stated honestly: seccomp uses the
//! runtime's `RuntimeDefault` profile (installing our strict allowlist as
//! a `Localhost` profile is per-node provisioning, documented for managed
//! clusters), and kernel-level egress enforcement of the NetworkPolicy
//! requires a NetworkPolicy-capable CNI (kind's default kindnet does not
//! enforce; production clusters with Calico/Cilium do).
use k8s_openapi::api::core::v1::{Namespace, Pod};
use k8s_openapi::api::networking::v1::NetworkPolicy;
use kube::api::{AttachParams, DeleteParams, ObjectMeta, Patch, PatchParams, PostParams};
use kube::Api;
use serde_json::json;
use tokio::io::AsyncReadExt;
use crate::spec::{ExecResult, ManagedSandbox, SandboxHandle, SandboxSpec};
use crate::{SandboxDriver, SandboxError};
fn engine_err(e: impl std::fmt::Display) -> SandboxError {
SandboxError::Engine(e.to_string())
}
pub struct K8sDriver {
client: kube::Client,
namespace: String,
/// Kubelet-relative path of the strict allowlist profile when the
/// nodes carry it (installed by the Helm DaemonSet); RuntimeDefault
/// otherwise.
localhost_seccomp: Option<String>,
}
impl K8sDriver {
/// Connects via the ambient kubeconfig and ensures the sandbox
/// namespace exists with its default-deny NetworkPolicy.
pub async fn connect(namespace: &str) -> Result<K8sDriver, SandboxError> {
// rustls 0.23 needs a process-level crypto provider when several
// are linked; first caller wins, repeats are harmless.
let _ = rustls::crypto::ring::default_provider().install_default();
let client = kube::Client::try_default().await.map_err(engine_err)?;
K8sDriver::with_client(client, namespace).await
}
/// Connects to a SPECIFIC kubeconfig context (e.g. the
/// NetworkPolicy-enforcing test cluster) instead of the current one.
pub async fn connect_with_context(
namespace: &str,
context: &str,
) -> Result<K8sDriver, SandboxError> {
let _ = rustls::crypto::ring::default_provider().install_default();
let options = kube::config::KubeConfigOptions {
context: Some(context.to_owned()),
..Default::default()
};
let config = kube::Config::from_kubeconfig(&options)
.await
.map_err(engine_err)?;
let client = kube::Client::try_from(config).map_err(engine_err)?;
K8sDriver::with_client(client, namespace).await
}
async fn with_client(client: kube::Client, namespace: &str) -> Result<K8sDriver, SandboxError> {
let driver = K8sDriver {
client,
namespace: namespace.to_owned(),
localhost_seccomp: None,
};
driver.ensure_namespace().await?;
Ok(driver)
}
async fn ensure_namespace(&self) -> Result<(), SandboxError> {
let namespaces: Api<Namespace> = Api::all(self.client.clone());
let ns = Namespace {
metadata: ObjectMeta {
name: Some(self.namespace.clone()),
labels: Some(
[
// Pod Security Standards: refuse anything that
// tries to relax the hardening below.
(
"pod-security.kubernetes.io/enforce".to_owned(),
"restricted".to_owned(),
),
("app.kubernetes.io/name".to_owned(), "clawmates".to_owned()),
]
.into(),
),
..Default::default()
},
..Default::default()
};
namespaces
.patch(
&self.namespace,
&PatchParams::apply("clawmates-sandbox").force(),
&Patch::Apply(&ns),
)
.await
.map_err(engine_err)?;
// Default-deny everything for sandbox pods; the orchestrator talks
// to them via the API server's exec channel, not the pod network.
let policies: Api<NetworkPolicy> = Api::namespaced(self.client.clone(), &self.namespace);
let deny: NetworkPolicy = serde_json::from_value(json!({
"apiVersion": "networking.k8s.io/v1",
"kind": "NetworkPolicy",
"metadata": { "name": "sandbox-default-deny", "namespace": self.namespace },
"spec": {
"podSelector": {},
"policyTypes": ["Ingress", "Egress"]
}
}))
.expect("static policy");
policies
.patch(
"sandbox-default-deny",
&PatchParams::apply("clawmates-sandbox").force(),
&Patch::Apply(&deny),
)
.await
.map_err(engine_err)?;
Ok(())
}
/// Uses the node-installed strict allowlist profile instead of the
/// runtime default (see deploy/helm templates/seccomp-installer).
pub fn with_localhost_seccomp(mut self, profile: &str) -> K8sDriver {
self.localhost_seccomp = Some(profile.to_owned());
self
}
fn pod_spec(&self, spec: &SandboxSpec) -> Pod {
let seccomp = match &self.localhost_seccomp {
Some(profile) => json!({ "type": "Localhost", "localhostProfile": profile }),
None => json!({ "type": "RuntimeDefault" }),
};
serde_json::from_value(json!({
"apiVersion": "v1",
"kind": "Pod",
"metadata": {
"name": spec.name,
"namespace": self.namespace,
"labels": {
"app.kubernetes.io/name": "clawmates-sandbox",
crate::SANDBOX_LABEL: crate::sandbox_kind(spec.egress)
}
},
"spec": {
"restartPolicy": "Never",
"automountServiceAccountToken": false,
"securityContext": {
"runAsNonRoot": true,
"runAsUser": 10001,
"runAsGroup": 10001,
"seccompProfile": seccomp
},
"containers": [{
"name": "sandbox",
"image": spec.image,
"imagePullPolicy": "Never",
"command": ["sleep", "infinity"],
"securityContext": {
"allowPrivilegeEscalation": false,
"capabilities": { "drop": ["ALL"] },
"readOnlyRootFilesystem": true
},
"resources": {
"limits": {
"memory": format!("{}", spec.memory_bytes),
"cpu": format!("{}m", spec.nano_cpus / 1_000_000)
}
},
"volumeMounts": [
{ "name": "tmp", "mountPath": "/tmp" },
{ "name": "home", "mountPath": "/home/agent" }
]
}],
"volumes": [
{ "name": "tmp", "emptyDir": { "sizeLimit": "64Mi" } },
{ "name": "home", "emptyDir": { "sizeLimit": "256Mi" } }
]
}
}))
.expect("static pod spec")
}
fn pods(&self) -> Api<Pod> {
Api::namespaced(self.client.clone(), &self.namespace)
}
}
#[async_trait::async_trait]
impl SandboxDriver for K8sDriver {
async fn provision(&self, spec: &SandboxSpec) -> Result<SandboxHandle, SandboxError> {
let pods = self.pods();
pods.create(&PostParams::default(), &self.pod_spec(spec))
.await
.map_err(engine_err)?;
// Wait until Running (image is preloaded, so this is fast).
for _ in 0..120 {
let pod = pods.get(&spec.name).await.map_err(engine_err)?;
let phase = pod.status.as_ref().and_then(|s| s.phase.clone());
match phase.as_deref() {
Some("Running") => {
return Ok(SandboxHandle {
id: spec.name.clone(),
name: spec.name.clone(),
})
}
Some("Failed") => return Err(engine_err("sandbox pod failed to start")),
_ => tokio::time::sleep(std::time::Duration::from_millis(500)).await,
}
}
Err(engine_err("sandbox pod never reached Running"))
}
async fn exec(&self, handle: &SandboxHandle, cmd: &[&str]) -> Result<ExecResult, SandboxError> {
let pods = self.pods();
let params = AttachParams::default().stdout(true).stderr(true);
let mut attached = pods
.exec(&handle.name, cmd.to_vec(), &params)
.await
.map_err(engine_err)?;
let mut stdout = String::new();
let mut stderr = String::new();
if let Some(mut out) = attached.stdout() {
out.read_to_string(&mut stdout).await.map_err(engine_err)?;
}
if let Some(mut err) = attached.stderr() {
err.read_to_string(&mut stderr).await.map_err(engine_err)?;
}
let status = attached.take_status();
attached.join().await.map_err(engine_err)?;
// Exit code travels in the v1.Status the API server sends on close.
let exit_code = match status {
Some(rx) => match rx.await {
Some(s) if s.status.as_deref() == Some("Success") => 0,
Some(s) => s
.details
.and_then(|d| {
d.causes.unwrap_or_default().into_iter().find_map(|c| {
(c.reason.as_deref() == Some("ExitCode"))
.then(|| c.message.and_then(|m| m.parse().ok()))
.flatten()
})
})
.unwrap_or(1),
None => 0,
},
None => 0,
};
Ok(ExecResult {
exit_code,
stdout,
stderr,
})
}
async fn attach_pty(
&self,
_handle: &SandboxHandle,
_cmd: &[&str],
_cols: u16,
_rows: u16,
_env: &[String],
) -> Result<crate::PtySession, SandboxError> {
// The interactive Terminal runs on the Docker target today; the k8s
// attach (kube exec with tty+stdin streams) is a later addition.
Err(SandboxError::Engine(
"interactive PTY is not yet supported on the Kubernetes driver".into(),
))
}
async fn resize_pty(&self, _exec_id: &str, _cols: u16, _rows: u16) -> Result<(), SandboxError> {
Err(SandboxError::Engine(
"interactive PTY is not yet supported on the Kubernetes driver".into(),
))
}
async fn destroy(&self, handle: &SandboxHandle) -> Result<(), SandboxError> {
self.pods()
.delete(&handle.name, &DeleteParams::default().grace_period(0))
.await
.map_err(engine_err)?;
Ok(())
}
async fn health(&self, handle: &SandboxHandle) -> Result<bool, SandboxError> {
match self
.pods()
.get_opt(&handle.name)
.await
.map_err(engine_err)?
{
Some(pod) => Ok(pod
.status
.and_then(|s| s.phase)
.is_some_and(|phase| phase == "Running")),
None => Ok(false),
}
}
async fn list_managed(&self, kind: &str) -> Result<Vec<ManagedSandbox>, SandboxError> {
let lp =
kube::api::ListParams::default().labels(&format!("{}={}", crate::SANDBOX_LABEL, kind));
let list = self.pods().list(&lp).await.map_err(engine_err)?;
Ok(list
.into_iter()
.filter_map(|p| {
let name = p.metadata.name?;
let created = p
.metadata
.creation_timestamp
.map(|t| t.0.timestamp())
.unwrap_or(0);
Some(ManagedSandbox {
id: name,
created_unix: created,
})
})
.collect())
}
}
+1 -6
View File
@@ -1,17 +1,12 @@
//! Per-agent sandbox orchestration (spec §15): containers with no root, no //! Per-agent sandbox orchestration (spec §15): containers with no root, no
//! capabilities, a seccomp deny profile, read-only rootfs, and no network. //! capabilities, a seccomp deny profile, read-only rootfs, and no network.
//! One `SandboxDriver` trait; the Docker implementation serves dev and the //! One `SandboxDriver` trait; the Docker implementation serves dev and the
//! air-gapped compose target; the Kubernetes driver (feature `k8s`) runs //! air-gapped compose target.
//! hardened pods in a PSS-restricted, default-deny namespace.
mod docker; mod docker;
#[cfg(feature = "k8s")]
mod k8s;
mod spec; mod spec;
pub use docker::DockerDriver; pub use docker::DockerDriver;
#[cfg(feature = "k8s")]
pub use k8s::K8sDriver;
pub use spec::{ pub use spec::{
DriveMount, ExecResult, ManagedSandbox, PtySession, SandboxHandle, SandboxKind, SandboxSpec, DriveMount, ExecResult, ManagedSandbox, PtySession, SandboxHandle, SandboxKind, SandboxSpec,
}; };
-283
View File
@@ -1,283 +0,0 @@
//! The K8s sandbox driver against a REAL kind cluster: the same kernel
//! probes as the Docker suite (uid, capabilities, no-new-privs, rootfs),
//! plus namespace policy objects. Run with `--features k8s-tests` after
//! `kind create cluster --name clawmates-test` and a `kind load` of the
//! agent image (the harness does both image steps itself).
#![cfg(feature = "k8s-tests")]
use std::process::Command;
use cm_sandbox::{K8sDriver, SandboxDriver, SandboxSpec};
const IMAGE: &str = "clawmates/agent-base:dev";
const CLUSTER: &str = "clawmates-test";
const CONTEXT: &str = "kind-clawmates-test";
const NAMESPACE: &str = "clawmates-sandboxes-test";
fn ensure_image_in_kind() {
let exists = Command::new("docker")
.args(["image", "inspect", IMAGE])
.output()
.expect("docker available")
.status
.success();
if !exists {
let root = env!("CARGO_MANIFEST_DIR");
let status = Command::new("docker")
.args([
"build",
"-t",
IMAGE,
"-f",
&format!("{root}/../../images/agent-base/Dockerfile"),
&format!("{root}/../../images/agent-base"),
])
.status()
.expect("docker build runs");
assert!(status.success(), "agent-base image build failed");
}
let status = Command::new("kind")
.args(["load", "docker-image", IMAGE, "--name", CLUSTER])
.status()
.expect("kind available");
assert!(status.success(), "kind load failed");
}
fn ensure_image_in_cluster(cluster: &str) {
ensure_image_in_kind();
let status = Command::new("kind")
.args(["load", "docker-image", IMAGE, "--name", cluster])
.status()
.expect("kind available");
assert!(status.success(), "kind load into {cluster} failed");
}
async fn spawn(suffix: &str) -> (K8sDriver, cm_sandbox::SandboxHandle) {
ensure_image_in_kind();
let driver = K8sDriver::connect_with_context(NAMESPACE, CONTEXT)
.await
.expect("cluster reachable");
let spec = SandboxSpec {
name: format!("tc-k8s-{suffix}-{}", std::process::id()),
image: IMAGE.into(),
memory_bytes: 256 * 1024 * 1024,
nano_cpus: 1_000_000_000,
pids_limit: 128,
egress: false,
kind: cm_sandbox::SandboxKind::Agent,
mounts: Vec::new(),
};
let handle = driver.provision(&spec).await.expect("pod provisions");
(driver, handle)
}
#[tokio::test]
async fn pod_runs_hardened_with_writable_scratch_only() {
let (driver, handle) = spawn("hard").await;
let uid = driver.exec(&handle, &["id", "-u"]).await.unwrap();
assert_eq!(uid.stdout.trim(), "10001", "stderr: {}", uid.stderr);
let caps = driver
.exec(&handle, &["grep", "CapEff", "/proc/self/status"])
.await
.unwrap();
let value = caps.stdout.split_whitespace().last().unwrap_or("");
assert_eq!(u64::from_str_radix(value, 16).unwrap(), 0);
let nnp = driver
.exec(&handle, &["grep", "NoNewPrivs", "/proc/self/status"])
.await
.unwrap();
assert!(nnp.stdout.trim().ends_with('1'), "got: {}", nnp.stdout);
let write_root = driver
.exec(&handle, &["touch", "/etc/owned"])
.await
.unwrap();
assert_ne!(write_root.exit_code, 0, "rootfs must reject writes");
let write_tmp = driver
.exec(&handle, &["touch", "/tmp/scratch"])
.await
.unwrap();
assert_eq!(write_tmp.exit_code, 0, "stderr: {}", write_tmp.stderr);
let write_home = driver
.exec(&handle, &["touch", "/home/agent/file"])
.await
.unwrap();
assert_eq!(write_home.exit_code, 0, "stderr: {}", write_home.stderr);
driver.destroy(&handle).await.unwrap();
}
#[tokio::test]
async fn namespace_carries_pss_restricted_and_default_deny_policy() {
let (driver, handle) = spawn("policy").await;
// Assert through the API: PSS label + the deny-all NetworkPolicy.
// (Kernel-level egress enforcement needs a NetworkPolicy-capable CNI;
// kind's default kindnet does not enforce — production clusters do.)
let options = kube::config::KubeConfigOptions {
context: Some(CONTEXT.to_owned()),
..Default::default()
};
let config = kube::Config::from_kubeconfig(&options).await.unwrap();
let client = kube::Client::try_from(config).unwrap();
let namespaces: kube::Api<k8s_openapi::api::core::v1::Namespace> =
kube::Api::all(client.clone());
let ns = namespaces.get(NAMESPACE).await.unwrap();
assert_eq!(
ns.metadata
.labels
.unwrap()
.get("pod-security.kubernetes.io/enforce")
.map(String::as_str),
Some("restricted")
);
let policies: kube::Api<k8s_openapi::api::networking::v1::NetworkPolicy> =
kube::Api::namespaced(client, NAMESPACE);
let deny = policies.get("sandbox-default-deny").await.unwrap();
let spec = deny.spec.unwrap();
assert_eq!(
spec.policy_types.unwrap(),
vec!["Ingress".to_owned(), "Egress".to_owned()]
);
assert!(spec.ingress.is_none() && spec.egress.is_none(), "deny-all");
driver.destroy(&handle).await.unwrap();
}
#[tokio::test]
async fn destroy_removes_the_pod_and_health_reflects_it() {
let (driver, handle) = spawn("life").await;
assert!(driver.health(&handle).await.unwrap());
driver.destroy(&handle).await.unwrap();
for _ in 0..60 {
if !driver.health(&handle).await.unwrap() {
return;
}
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
}
panic!("pod never disappeared");
}
/// With the strict allowlist installed on the node (the Helm DaemonSet's
/// job; the harness drops it into the kind node), pods run under
/// `Localhost` seccomp and the kernel refuses what the profile removed.
#[tokio::test]
async fn localhost_seccomp_profile_denies_unshare_inside_pods() {
ensure_image_in_kind();
let status = Command::new("docker")
.args([
"exec",
"clawmates-test-control-plane",
"mkdir",
"-p",
"/var/lib/kubelet/seccomp",
])
.status()
.expect("kind node reachable");
assert!(status.success());
let root = env!("CARGO_MANIFEST_DIR");
let status = Command::new("docker")
.args([
"cp",
&format!("{root}/../../images/seccomp/agent-profile.json"),
"clawmates-test-control-plane:/var/lib/kubelet/seccomp/clawmates-agent-profile.json",
])
.status()
.expect("docker cp");
assert!(status.success());
let driver = K8sDriver::connect_with_context(NAMESPACE, CONTEXT)
.await
.expect("cluster reachable")
.with_localhost_seccomp("clawmates-agent-profile.json");
let spec = SandboxSpec {
name: format!("tc-k8s-seccomp-{}", std::process::id()),
image: IMAGE.into(),
memory_bytes: 256 * 1024 * 1024,
nano_cpus: 1_000_000_000,
pids_limit: 128,
egress: false,
kind: cm_sandbox::SandboxKind::Agent,
mounts: Vec::new(),
};
let handle = driver.provision(&spec).await.expect("pod provisions");
// Ordinary work runs...
let ok = driver.exec(&handle, &["id", "-u"]).await.unwrap();
assert_eq!(ok.stdout.trim(), "10001");
// ...but the syscalls stripped from the allowlist are gone — the
// same kernel probe the Docker suite uses.
let unshare = driver
.exec(&handle, &["unshare", "--user", "true"])
.await
.unwrap();
assert_ne!(unshare.exit_code, 0, "unshare must be denied: {unshare:?}");
driver.destroy(&handle).await.unwrap();
}
/// The §15 egress claim, ENFORCED: on a NetworkPolicy-capable CNI
/// (Calico via scripts/netpol-cluster.sh) the namespace's default-deny
/// actually drops packets in the kernel — outbound connects and DNS both
/// fail inside the pod, while API-server exec still works (it is not pod
/// network). kindnet (the default suite's cluster) accepts the policy
/// object but never enforces it; this is the cluster where it bites.
#[tokio::test]
async fn calico_enforces_the_default_deny_egress() {
const NETPOL_CONTEXT: &str = "kind-clawmates-netpol-test";
const NETPOL_CLUSTER: &str = "clawmates-netpol-test";
let have_cluster = Command::new("kind")
.args(["get", "clusters"])
.output()
.map(|out| String::from_utf8_lossy(&out.stdout).contains(NETPOL_CLUSTER))
.unwrap_or(false);
if !have_cluster {
eprintln!("skipped: run scripts/netpol-cluster.sh up first");
return;
}
ensure_image_in_cluster(NETPOL_CLUSTER);
let driver = K8sDriver::connect_with_context(NAMESPACE, NETPOL_CONTEXT)
.await
.expect("calico cluster reachable");
let spec = SandboxSpec {
name: format!("tc-netpol-{}", std::process::id()),
image: IMAGE.into(),
memory_bytes: 256 * 1024 * 1024,
nano_cpus: 1_000_000_000,
pids_limit: 128,
egress: false,
kind: cm_sandbox::SandboxKind::Agent,
mounts: Vec::new(),
};
let handle = driver.provision(&spec).await.expect("pod provisions");
// Exec works (API-server channel, not pod network).
let ok = driver.exec(&handle, &["id", "-u"]).await.unwrap();
assert_eq!(ok.stdout.trim(), "10001");
// Raw outbound connect: dropped by Calico, not merely unconfigured.
let direct = driver
.exec(
&handle,
&["wget", "-T", "3", "-q", "-O", "-", "http://1.1.1.1"],
)
.await
.unwrap();
assert_ne!(direct.exit_code, 0, "egress to 1.1.1.1 must be dropped");
// DNS (UDP egress to cluster DNS) is denied too.
let dns = driver
.exec(&handle, &["nslookup", "anthropic.com"])
.await
.unwrap();
assert_ne!(dns.exit_code, 0, "DNS egress must be dropped");
driver.destroy(&handle).await.unwrap();
}
-9
View File
@@ -1,9 +0,0 @@
apiVersion: v2
name: clawmates
description: >-
Clawmates — collaborative AI agents with a human-in-the-loop safety layer.
One chart deploys the server (API + gateway + runtime), the secret broker,
and the frontend; Postgres is either managed (external URL) or in-cluster.
type: application
version: 0.1.0
appVersion: "0.1.0"
@@ -1,705 +0,0 @@
{
"_comment": "Clawmates agent-sandbox seccomp profile: Docker's default ALLOWLIST (vendored from moby v27.5.1) minus syscalls an agent workload never needs \u2014 namespace/mount/trace/key/module/perf surface removed even where capabilities would otherwise permit them. defaultAction ERRNO.",
"defaultAction": "SCMP_ACT_ERRNO",
"defaultErrnoRet": 1,
"archMap": [
{
"architecture": "SCMP_ARCH_X86_64",
"subArchitectures": [
"SCMP_ARCH_X86",
"SCMP_ARCH_X32"
]
},
{
"architecture": "SCMP_ARCH_AARCH64",
"subArchitectures": [
"SCMP_ARCH_ARM"
]
}
],
"syscalls": [
{
"names": [
"accept",
"accept4",
"access",
"adjtimex",
"alarm",
"bind",
"brk",
"cachestat",
"capget",
"capset",
"chdir",
"chmod",
"chown",
"chown32",
"clock_adjtime",
"clock_adjtime64",
"clock_getres",
"clock_getres_time64",
"clock_gettime",
"clock_gettime64",
"clock_nanosleep",
"clock_nanosleep_time64",
"close",
"close_range",
"connect",
"copy_file_range",
"creat",
"dup",
"dup2",
"dup3",
"epoll_create",
"epoll_create1",
"epoll_ctl",
"epoll_ctl_old",
"epoll_pwait",
"epoll_pwait2",
"epoll_wait",
"epoll_wait_old",
"eventfd",
"eventfd2",
"execve",
"execveat",
"exit",
"exit_group",
"faccessat",
"faccessat2",
"fadvise64",
"fadvise64_64",
"fallocate",
"fanotify_mark",
"fchdir",
"fchmod",
"fchmodat",
"fchmodat2",
"fchown",
"fchown32",
"fchownat",
"fcntl",
"fcntl64",
"fdatasync",
"fgetxattr",
"flistxattr",
"flock",
"fork",
"fremovexattr",
"fsetxattr",
"fstat",
"fstat64",
"fstatat64",
"fstatfs",
"fstatfs64",
"fsync",
"ftruncate",
"ftruncate64",
"futex",
"futex_requeue",
"futex_time64",
"futex_wait",
"futex_waitv",
"futex_wake",
"futimesat",
"getcpu",
"getcwd",
"getdents",
"getdents64",
"getegid",
"getegid32",
"geteuid",
"geteuid32",
"getgid",
"getgid32",
"getgroups",
"getgroups32",
"getitimer",
"getpeername",
"getpgid",
"getpgrp",
"getpid",
"getppid",
"getpriority",
"getrandom",
"getresgid",
"getresgid32",
"getresuid",
"getresuid32",
"getrlimit",
"get_robust_list",
"getrusage",
"getsid",
"getsockname",
"getsockopt",
"get_thread_area",
"gettid",
"gettimeofday",
"getuid",
"getuid32",
"getxattr",
"inotify_add_watch",
"inotify_init",
"inotify_init1",
"inotify_rm_watch",
"io_cancel",
"ioctl",
"io_destroy",
"io_getevents",
"io_pgetevents",
"io_pgetevents_time64",
"ioprio_get",
"ioprio_set",
"io_setup",
"io_submit",
"ipc",
"kill",
"landlock_add_rule",
"landlock_create_ruleset",
"landlock_restrict_self",
"lchown",
"lchown32",
"lgetxattr",
"link",
"linkat",
"listen",
"listxattr",
"llistxattr",
"_llseek",
"lremovexattr",
"lseek",
"lsetxattr",
"lstat",
"lstat64",
"madvise",
"map_shadow_stack",
"membarrier",
"memfd_create",
"memfd_secret",
"mincore",
"mkdir",
"mkdirat",
"mknod",
"mknodat",
"mlock",
"mlock2",
"mlockall",
"mmap",
"mmap2",
"mprotect",
"mq_getsetattr",
"mq_notify",
"mq_open",
"mq_timedreceive",
"mq_timedreceive_time64",
"mq_timedsend",
"mq_timedsend_time64",
"mq_unlink",
"mremap",
"msgctl",
"msgget",
"msgrcv",
"msgsnd",
"msync",
"munlock",
"munlockall",
"munmap",
"name_to_handle_at",
"nanosleep",
"newfstatat",
"_newselect",
"open",
"openat",
"openat2",
"pause",
"pidfd_open",
"pidfd_send_signal",
"pipe",
"pipe2",
"pkey_alloc",
"pkey_free",
"pkey_mprotect",
"poll",
"ppoll",
"ppoll_time64",
"prctl",
"pread64",
"preadv",
"preadv2",
"prlimit64",
"process_mrelease",
"pselect6",
"pselect6_time64",
"pwrite64",
"pwritev",
"pwritev2",
"read",
"readahead",
"readlink",
"readlinkat",
"readv",
"recv",
"recvfrom",
"recvmmsg",
"recvmmsg_time64",
"recvmsg",
"remap_file_pages",
"removexattr",
"rename",
"renameat",
"renameat2",
"restart_syscall",
"rmdir",
"rseq",
"rt_sigaction",
"rt_sigpending",
"rt_sigprocmask",
"rt_sigqueueinfo",
"rt_sigreturn",
"rt_sigsuspend",
"rt_sigtimedwait",
"rt_sigtimedwait_time64",
"rt_tgsigqueueinfo",
"sched_getaffinity",
"sched_getattr",
"sched_getparam",
"sched_get_priority_max",
"sched_get_priority_min",
"sched_getscheduler",
"sched_rr_get_interval",
"sched_rr_get_interval_time64",
"sched_setaffinity",
"sched_setattr",
"sched_setparam",
"sched_setscheduler",
"sched_yield",
"seccomp",
"select",
"semctl",
"semget",
"semop",
"semtimedop",
"semtimedop_time64",
"send",
"sendfile",
"sendfile64",
"sendmmsg",
"sendmsg",
"sendto",
"setfsgid",
"setfsgid32",
"setfsuid",
"setfsuid32",
"setgid",
"setgid32",
"setgroups",
"setgroups32",
"setitimer",
"setpgid",
"setpriority",
"setregid",
"setregid32",
"setresgid",
"setresgid32",
"setresuid",
"setresuid32",
"setreuid",
"setreuid32",
"setrlimit",
"set_robust_list",
"setsid",
"setsockopt",
"set_thread_area",
"set_tid_address",
"setuid",
"setuid32",
"setxattr",
"shmat",
"shmctl",
"shmdt",
"shmget",
"shutdown",
"sigaltstack",
"signalfd",
"signalfd4",
"sigprocmask",
"sigreturn",
"socketcall",
"socketpair",
"splice",
"stat",
"stat64",
"statfs",
"statfs64",
"statx",
"symlink",
"symlinkat",
"sync",
"sync_file_range",
"syncfs",
"sysinfo",
"tee",
"tgkill",
"time",
"timer_create",
"timer_delete",
"timer_getoverrun",
"timer_gettime",
"timer_gettime64",
"timer_settime",
"timer_settime64",
"timerfd_create",
"timerfd_gettime",
"timerfd_gettime64",
"timerfd_settime",
"timerfd_settime64",
"times",
"tkill",
"truncate",
"truncate64",
"ugetrlimit",
"umask",
"uname",
"unlink",
"unlinkat",
"utime",
"utimensat",
"utimensat_time64",
"utimes",
"vfork",
"vmsplice",
"wait4",
"waitid",
"waitpid",
"write",
"writev"
],
"action": "SCMP_ACT_ALLOW"
},
{
"names": [
"socket"
],
"action": "SCMP_ACT_ALLOW",
"args": [
{
"index": 0,
"value": 40,
"op": "SCMP_CMP_NE"
}
]
},
{
"names": [
"personality"
],
"action": "SCMP_ACT_ALLOW",
"args": [
{
"index": 0,
"value": 0,
"op": "SCMP_CMP_EQ"
}
]
},
{
"names": [
"personality"
],
"action": "SCMP_ACT_ALLOW",
"args": [
{
"index": 0,
"value": 8,
"op": "SCMP_CMP_EQ"
}
]
},
{
"names": [
"personality"
],
"action": "SCMP_ACT_ALLOW",
"args": [
{
"index": 0,
"value": 131072,
"op": "SCMP_CMP_EQ"
}
]
},
{
"names": [
"personality"
],
"action": "SCMP_ACT_ALLOW",
"args": [
{
"index": 0,
"value": 131080,
"op": "SCMP_CMP_EQ"
}
]
},
{
"names": [
"personality"
],
"action": "SCMP_ACT_ALLOW",
"args": [
{
"index": 0,
"value": 4294967295,
"op": "SCMP_CMP_EQ"
}
]
},
{
"names": [
"sync_file_range2",
"swapcontext"
],
"action": "SCMP_ACT_ALLOW",
"includes": {
"arches": [
"ppc64le"
]
}
},
{
"names": [
"arm_fadvise64_64",
"arm_sync_file_range",
"sync_file_range2",
"breakpoint",
"cacheflush",
"set_tls"
],
"action": "SCMP_ACT_ALLOW",
"includes": {
"arches": [
"arm",
"arm64"
]
}
},
{
"names": [
"arch_prctl"
],
"action": "SCMP_ACT_ALLOW",
"includes": {
"arches": [
"amd64",
"x32"
]
}
},
{
"names": [
"modify_ldt"
],
"action": "SCMP_ACT_ALLOW",
"includes": {
"arches": [
"amd64",
"x32",
"x86"
]
}
},
{
"names": [
"s390_pci_mmio_read",
"s390_pci_mmio_write",
"s390_runtime_instr"
],
"action": "SCMP_ACT_ALLOW",
"includes": {
"arches": [
"s390",
"s390x"
]
}
},
{
"names": [
"riscv_flush_icache"
],
"action": "SCMP_ACT_ALLOW",
"includes": {
"arches": [
"riscv64"
]
}
},
{
"names": [
"clone",
"fanotify_init",
"fsconfig",
"fsmount",
"fsopen",
"fspick",
"lookup_dcookie",
"mount_setattr",
"open_tree",
"quotactl_fd",
"setdomainname",
"sethostname",
"syslog",
"umount"
],
"action": "SCMP_ACT_ALLOW",
"includes": {
"caps": [
"CAP_SYS_ADMIN"
]
}
},
{
"names": [
"clone"
],
"action": "SCMP_ACT_ALLOW",
"args": [
{
"index": 0,
"value": 2114060288,
"op": "SCMP_CMP_MASKED_EQ"
}
],
"excludes": {
"caps": [
"CAP_SYS_ADMIN"
],
"arches": [
"s390",
"s390x"
]
}
},
{
"names": [
"clone"
],
"action": "SCMP_ACT_ALLOW",
"args": [
{
"index": 1,
"value": 2114060288,
"op": "SCMP_CMP_MASKED_EQ"
}
],
"comment": "s390 parameter ordering for clone is different",
"includes": {
"arches": [
"s390",
"s390x"
]
},
"excludes": {
"caps": [
"CAP_SYS_ADMIN"
]
}
},
{
"names": [
"clone3"
],
"action": "SCMP_ACT_ERRNO",
"errnoRet": 38,
"excludes": {
"caps": [
"CAP_SYS_ADMIN"
]
}
},
{
"names": [
"chroot"
],
"action": "SCMP_ACT_ALLOW",
"includes": {
"caps": [
"CAP_SYS_CHROOT"
]
}
},
{
"names": [
"kcmp",
"pidfd_getfd",
"process_madvise"
],
"action": "SCMP_ACT_ALLOW",
"includes": {
"caps": [
"CAP_SYS_PTRACE"
]
}
},
{
"names": [
"iopl",
"ioperm"
],
"action": "SCMP_ACT_ALLOW",
"includes": {
"caps": [
"CAP_SYS_RAWIO"
]
}
},
{
"names": [
"settimeofday",
"stime",
"clock_settime",
"clock_settime64"
],
"action": "SCMP_ACT_ALLOW",
"includes": {
"caps": [
"CAP_SYS_TIME"
]
}
},
{
"names": [
"vhangup"
],
"action": "SCMP_ACT_ALLOW",
"includes": {
"caps": [
"CAP_SYS_TTY_CONFIG"
]
}
},
{
"names": [
"get_mempolicy",
"mbind",
"set_mempolicy",
"set_mempolicy_home_node"
],
"action": "SCMP_ACT_ALLOW",
"includes": {
"caps": [
"CAP_SYS_NICE"
]
}
},
{
"names": [
"syslog"
],
"action": "SCMP_ACT_ALLOW",
"includes": {
"caps": [
"CAP_SYSLOG"
]
}
}
]
}
@@ -1,20 +0,0 @@
{{- define "clawmates.labels" -}}
app.kubernetes.io/name: clawmates
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{- define "clawmates.serverSelector" -}}
app.kubernetes.io/name: clawmates
app.kubernetes.io/component: server
{{- end }}
{{- define "clawmates.brokerSelector" -}}
app.kubernetes.io/name: clawmates
app.kubernetes.io/component: broker
{{- end }}
{{- define "clawmates.frontendSelector" -}}
app.kubernetes.io/name: clawmates
app.kubernetes.io/component: frontend
{{- end }}
@@ -1,49 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: clawmates-config
labels: {{- include "clawmates.labels" . | nindent 4 }}
data:
clawmates.toml: |
deploy_target = "cloud"
listen_addr = "0.0.0.0:8080"
[database]
# Overlaid by CLAWMATES_DATABASE__URL from the Secret.
url = "postgres://overridden-by-env"
[llm]
provider = "{{ .Values.llm.provider }}"
model = "{{ .Values.llm.model }}"
{{- if .Values.llm.baseUrl }}
base_url = "{{ .Values.llm.baseUrl }}"
{{- end }}
[auth]
mode = "{{ .Values.auth.mode }}"
{{- if eq .Values.auth.mode "oidc" }}
issuer_url = "{{ required "auth.issuerUrl is required for oidc" .Values.auth.issuerUrl }}"
client_id = "{{ .Values.auth.clientId }}"
{{- end }}
{{- if eq .Values.auth.mode "clerk" }}
issuer_url = "{{ required "auth.issuerUrl is required for clerk (https://<slug>.clerk.accounts.dev)" .Values.auth.issuerUrl }}"
{{- end }}
[storage]
data_dir = "{{ .Values.storage.dataDir }}"
backend = "{{ .Values.storage.backend }}"
{{- if eq .Values.storage.backend "s3" }}
s3_endpoint = "{{ required "storage.s3.endpoint required" .Values.storage.s3.endpoint }}"
s3_bucket = "{{ required "storage.s3.bucket required" .Values.storage.s3.bucket }}"
{{- end }}
[broker]
socket_path = "/run/clawmates/broker.sock"
{{- if .Values.oauth.issuerUrl }}
[oauth]
issuer_url = "{{ .Values.oauth.issuerUrl }}"
client_id = "{{ .Values.oauth.clientId }}"
redirect_base = "{{ .Values.oauth.redirectBase }}"
{{- end }}
@@ -1,55 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: clawmates-frontend
labels: {{- include "clawmates.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.frontend.replicas }}
selector:
matchLabels: {{- include "clawmates.frontendSelector" . | nindent 6 }}
template:
metadata:
labels:
{{- include "clawmates.frontendSelector" . | nindent 8 }}
app.kubernetes.io/instance: {{ .Release.Name }}
spec:
securityContext:
runAsNonRoot: true
seccompProfile: { type: RuntimeDefault }
containers:
- name: frontend
image: "{{ .Values.image.registry }}/frontend:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- { containerPort: 3000, name: http }
env:
- name: API_ORIGIN
value: http://clawmates-server:8080
{{- if eq .Values.auth.mode "clerk" }}
- name: AUTH_MODE
value: clerk
- name: CLERK_PUBLISHABLE_KEY
valueFrom:
secretKeyRef:
name: {{ .Values.auth.clerkSecretName }}
key: publishable-key
- name: CLERK_SECRET_KEY
valueFrom:
secretKeyRef:
name: {{ .Values.auth.clerkSecretName }}
key: secret-key
{{- end }}
resources: {{- toYaml .Values.frontend.resources | nindent 12 }}
securityContext:
allowPrivilegeEscalation: false
capabilities: { drop: ["ALL"] }
---
apiVersion: v1
kind: Service
metadata:
name: clawmates-frontend
labels: {{- include "clawmates.labels" . | nindent 4 }}
spec:
selector: {{- include "clawmates.frontendSelector" . | nindent 4 }}
ports:
- { name: http, port: 3000, targetPort: http }
-21
View File
@@ -1,21 +0,0 @@
{{- if .Values.server.autoscaling.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: clawmates-server
labels: {{- include "clawmates.labels" . | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: clawmates-server
minReplicas: {{ .Values.server.autoscaling.min }}
maxReplicas: {{ .Values.server.autoscaling.max }}
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .Values.server.autoscaling.targetCPU }}
{{- end }}
@@ -1,37 +0,0 @@
{{- if .Values.ingress.enabled }}
# SSE streaming (POST /api/gateway) requires unbuffered proxying with long
# read timeouts; without these annotations approvals and live transcripts
# stall behind nginx buffering.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: clawmates
labels: {{- include "clawmates.labels" . | nindent 4 }}
annotations:
nginx.ingress.kubernetes.io/proxy-buffering: "off"
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
spec:
ingressClassName: {{ .Values.ingress.className }}
{{- if .Values.ingress.tlsSecretName }}
tls:
- hosts: [{{ .Values.ingress.host | quote }}]
secretName: {{ .Values.ingress.tlsSecretName }}
{{- end }}
rules:
- host: {{ .Values.ingress.host | quote }}
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: clawmates-server
port: { name: http }
- path: /
pathType: Prefix
backend:
service:
name: clawmates-frontend
port: { name: http }
{{- end }}
@@ -1,44 +0,0 @@
# The broker has no Service and shares the server pod, so its socket is
# already unreachable over the network. These policies enforce the rest of
# the §15 topology: the database secret-holders are the only egress-capable
# pods, and the frontend can reach only the server.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: clawmates-frontend-egress
labels: {{- include "clawmates.labels" . | nindent 4 }}
spec:
podSelector:
matchLabels: {{- include "clawmates.frontendSelector" . | nindent 6 }}
policyTypes: [Egress]
egress:
# DNS, then only the server.
- to: []
ports:
- { protocol: UDP, port: 53 }
- { protocol: TCP, port: 53 }
- to:
- podSelector:
matchLabels: {{- include "clawmates.serverSelector" . | nindent 14 }}
ports:
- { protocol: TCP, port: 8080 }
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: clawmates-server-ingress
labels: {{- include "clawmates.labels" . | nindent 4 }}
spec:
podSelector:
matchLabels: {{- include "clawmates.serverSelector" . | nindent 6 }}
policyTypes: [Ingress]
ingress:
- from:
- podSelector:
matchLabels: {{- include "clawmates.frontendSelector" . | nindent 14 }}
# Ingress controller namespaces vary; admit via namespace selector.
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
ports:
- { protocol: TCP, port: 8080 }
@@ -1,53 +0,0 @@
{{- if eq .Values.sandbox.seccomp "localhost" }}
# Installs the strict allowlist seccomp profile onto every node so
# sandbox pods can run with seccompProfile type Localhost. The profile is
# the SAME file the Docker driver embeds (ci/check-helm.sh enforces the
# copies stay identical).
apiVersion: v1
kind: ConfigMap
metadata:
name: clawmates-seccomp-profile
labels: {{- include "clawmates.labels" . | nindent 4 }}
data:
clawmates-agent-profile.json: |-
{{ .Files.Get "files/agent-profile.json" | indent 4 }}
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: clawmates-seccomp-installer
labels: {{- include "clawmates.labels" . | nindent 4 }}
spec:
selector:
matchLabels:
app.kubernetes.io/name: clawmates-seccomp-installer
template:
metadata:
labels:
app.kubernetes.io/name: clawmates-seccomp-installer
spec:
initContainers:
- name: install
image: busybox:1.36
command:
- sh
- -c
- cp /profile/clawmates-agent-profile.json /host-seccomp/
volumeMounts:
- { name: profile, mountPath: /profile, readOnly: true }
- { name: host-seccomp, mountPath: /host-seccomp }
containers:
- name: hold
image: busybox:1.36
command: ["sleep", "infinity"]
resources:
requests: { cpu: 5m, memory: 8Mi }
limits: { cpu: 10m, memory: 16Mi }
volumes:
- name: profile
configMap: { name: clawmates-seccomp-profile }
- name: host-seccomp
hostPath:
path: /var/lib/kubelet/seccomp
type: DirectoryOrCreate
{{- end }}
-124
View File
@@ -1,124 +0,0 @@
# The server pod runs clawmates-server with the secret broker as a sidecar
# sharing a private emptyDir for the unix socket — the §15 topology: no
# network hop carries credentials, and nothing else can reach the socket.
apiVersion: apps/v1
kind: Deployment
metadata:
name: clawmates-server
labels: {{- include "clawmates.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.server.replicas }}
selector:
matchLabels: {{- include "clawmates.serverSelector" . | nindent 6 }}
template:
metadata:
labels:
{{- include "clawmates.serverSelector" . | nindent 8 }}
app.kubernetes.io/instance: {{ .Release.Name }}
spec:
securityContext:
runAsNonRoot: true
runAsUser: 10001
seccompProfile: { type: RuntimeDefault }
containers:
- name: server
image: "{{ .Values.image.registry }}/server:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- { containerPort: 8080, name: http }
env:
- name: CLAWMATES_CONFIG
value: /etc/clawmates/clawmates.toml
- name: CLAWMATES_DATABASE__URL
valueFrom:
secretKeyRef:
name: {{ .Values.database.urlSecretName }}
key: url
{{- if eq .Values.llm.provider "anthropic" }}
- name: ANTHROPIC_API_KEY
valueFrom:
secretKeyRef:
name: {{ .Values.llm.apiKeySecretName }}
key: api-key
{{- end }}
{{- if eq .Values.storage.backend "s3" }}
- name: CLAWMATES_STORAGE__S3_ACCESS_KEY
valueFrom:
secretKeyRef:
name: {{ .Values.storage.s3.credentialsSecretName }}
key: access-key
- name: CLAWMATES_STORAGE__S3_SECRET_KEY
valueFrom:
secretKeyRef:
name: {{ .Values.storage.s3.credentialsSecretName }}
key: secret-key
{{- end }}
volumeMounts:
- { name: config, mountPath: /etc/clawmates, readOnly: true }
- { name: broker-socket, mountPath: /run/clawmates }
{{- if eq .Values.storage.backend "local" }}
- { name: data, mountPath: {{ .Values.storage.dataDir }} }
{{- end }}
readinessProbe:
httpGet: { path: /healthz, port: http }
initialDelaySeconds: 3
resources: {{- toYaml .Values.server.resources | nindent 12 }}
securityContext:
allowPrivilegeEscalation: false
capabilities: { drop: ["ALL"] }
readOnlyRootFilesystem: true
- name: broker
image: "{{ .Values.image.registry }}/broker:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
env:
- name: CLAWMATES_BROKER_SOCKET
value: /run/clawmates/broker.sock
- name: CLAWMATES_BROKER_KEY_FILE
value: /etc/clawmates-broker/broker.key
- name: CLAWMATES_DATABASE__URL
valueFrom:
secretKeyRef:
name: {{ .Values.database.urlSecretName }}
key: url
volumeMounts:
- { name: broker-socket, mountPath: /run/clawmates }
- { name: broker-key, mountPath: /etc/clawmates-broker, readOnly: true }
resources: {{- toYaml .Values.broker.resources | nindent 12 }}
securityContext:
allowPrivilegeEscalation: false
capabilities: { drop: ["ALL"] }
readOnlyRootFilesystem: true
volumes:
- name: config
configMap: { name: clawmates-config }
- name: broker-socket
emptyDir: {}
- name: broker-key
secret: { secretName: {{ .Values.broker.keySecretName }} }
{{- if eq .Values.storage.backend "local" }}
- name: data
persistentVolumeClaim: { claimName: clawmates-data }
{{- end }}
---
apiVersion: v1
kind: Service
metadata:
name: clawmates-server
labels: {{- include "clawmates.labels" . | nindent 4 }}
spec:
selector: {{- include "clawmates.serverSelector" . | nindent 4 }}
ports:
- { name: http, port: 8080, targetPort: http }
{{- if eq .Values.storage.backend "local" }}
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: clawmates-data
labels: {{- include "clawmates.labels" . | nindent 4 }}
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: {{ .Values.storage.pvcSize }}
{{- end }}
-79
View File
@@ -1,79 +0,0 @@
# Clawmates cloud deployment values.
image:
registry: ghcr.io/clawmates
tag: latest
pullPolicy: IfNotPresent
server:
replicas: 1
autoscaling:
enabled: false
min: 1
max: 5
targetCPU: 70
resources:
requests: { cpu: 250m, memory: 256Mi }
limits: { cpu: "1", memory: 512Mi }
broker:
resources:
requests: { cpu: 50m, memory: 64Mi }
limits: { cpu: 250m, memory: 128Mi }
# Master key Secret (key: broker.key); create it before install and BACK
# IT UP — secrets are unrecoverable without it.
keySecretName: clawmates-broker-key
frontend:
replicas: 1
resources:
requests: { cpu: 100m, memory: 128Mi }
limits: { cpu: 500m, memory: 256Mi }
database:
# External/managed Postgres connection string Secret (key: url).
urlSecretName: clawmates-database
llm:
# anthropic | openai_compat | scripted
provider: anthropic
model: claude-sonnet-4-6
baseUrl: ""
# Secret holding ANTHROPIC_API_KEY (key: api-key) for the anthropic provider.
apiKeySecretName: clawmates-llm
storage:
# local (PVC) | s3
backend: local
dataDir: /var/lib/clawmates
pvcSize: 10Gi
s3:
endpoint: ""
bucket: ""
credentialsSecretName: clawmates-s3
auth:
# local | oidc | clerk (clerk: issuerUrl is the instance Frontend API,
# https://<slug>.clerk.accounts.dev)
mode: oidc
issuerUrl: ""
clientId: clawmates
# clerk mode: Secret with keys publishable-key + secret-key.
clerkSecretName: clawmates-clerk
oauth:
issuerUrl: ""
clientId: clawmates
clientSecretName: clawmates-oauth
redirectBase: ""
sandbox:
# runtimeDefault | localhost (localhost installs the strict allowlist
# profile onto every node via a DaemonSet and runs sandbox pods under it)
seccomp: runtimeDefault
ingress:
enabled: true
className: nginx
host: app.clawmates.work
tlsSecretName: ""
-41
View File
@@ -1,41 +0,0 @@
#!/usr/bin/env bash
# A kind cluster with a NetworkPolicy-ENFORCING CNI (Calico) for the
# egress-denial e2e: kind's default kindnet accepts NetworkPolicy objects
# but never enforces them, so only this cluster can prove the §15
# default-deny actually drops packets in the kernel.
#
# Usage: scripts/netpol-cluster.sh {up|down}
set -euo pipefail
CLUSTER="clawmates-netpol-test"
CALICO_VERSION="v3.29.1"
case "${1:-up}" in
up)
if kind get clusters 2>/dev/null | grep -qx "$CLUSTER"; then
echo "$CLUSTER already exists"
else
kind create cluster --name "$CLUSTER" --wait 120s --config - <<'EOF'
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
networking:
disableDefaultCNI: true
podSubnet: 192.168.0.0/16
EOF
kubectl --context "kind-$CLUSTER" apply -f \
"https://raw.githubusercontent.com/projectcalico/calico/$CALICO_VERSION/manifests/calico.yaml"
fi
echo "waiting for calico + node readiness..."
kubectl --context "kind-$CLUSTER" -n kube-system rollout status \
daemonset/calico-node --timeout=300s
kubectl --context "kind-$CLUSTER" wait --for=condition=Ready node --all --timeout=120s
echo "$CLUSTER ready (calico enforcing)"
;;
down)
kind delete cluster --name "$CLUSTER"
;;
*)
echo "usage: $0 {up|down}" >&2
exit 1
;;
esac