Post-1.0: OTLP tracing, broker image + compose service, install rehearsal
- tc-telemetry: fmt subscriber always; with [telemetry] otlp_endpoint set, spans batch-export over OTLP/HTTP. Tested against a REAL OTLP receiver decoding the actual protobuf (official proto types): the emitted span and service.name arrive on the wire. No endpoint = no export = no network (air-gap stance). tower-http TraceLayer gives every API request a span - The broker finally has its own image (images/broker.Dockerfile, 9.5MB from scratch) — the Helm chart referenced one that never existed — and the compose deployment now RUNS the broker, sharing a socket volume with the server (the unix-socket equivalent of the K8s sidecar). Compose secret flows were silently dead before this - server.Dockerfile fixes surfaced by the rehearsal: the workspace build needs tools/ (bundler joined the workspace) and images/seccomp/ (include_str! profile) in the build context - scripts/rehearse-install.sh (plan: clean-VM rehearsal): assembles a REAL signed bundle from the built images (server/frontend/broker/ postgres/socket-proxy), runs the customer path — offline verify, docker load, compose up — and asserts /healthz plus the served login page before teardown. Passing locally; wired as a release.yml step, which also builds/ships the broker + socket-proxy images now 161 Rust tests + 29 journeys; clean-room rehearsal green. Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Fable 5
parent
05e9612688
commit
8046853feb
@@ -24,6 +24,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
docker build -t "teamclaw/server:$VERSION" -f images/server.Dockerfile .
|
docker build -t "teamclaw/server:$VERSION" -f images/server.Dockerfile .
|
||||||
docker build -t "teamclaw/frontend:$VERSION" -f images/frontend.Dockerfile .
|
docker build -t "teamclaw/frontend:$VERSION" -f images/frontend.Dockerfile .
|
||||||
|
docker build -t "teamclaw/broker:$VERSION" -f images/broker.Dockerfile .
|
||||||
docker build -t "teamclaw/agent-base:$VERSION" images/agent-base
|
docker build -t "teamclaw/agent-base:$VERSION" images/agent-base
|
||||||
docker build -t "teamclaw/agent-browser:$VERSION" images/agent-browser
|
docker build -t "teamclaw/agent-browser:$VERSION" images/agent-browser
|
||||||
docker pull postgres:16-alpine
|
docker pull postgres:16-alpine
|
||||||
@@ -33,7 +34,7 @@ jobs:
|
|||||||
mkdir -p dist/sboms
|
mkdir -p dist/sboms
|
||||||
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh \
|
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh \
|
||||||
| sh -s -- -b /usr/local/bin
|
| sh -s -- -b /usr/local/bin
|
||||||
for image in server frontend agent-base agent-browser; do
|
for image in server frontend broker agent-base agent-browser; do
|
||||||
syft "teamclaw/$image:$VERSION" -o spdx-json \
|
syft "teamclaw/$image:$VERSION" -o spdx-json \
|
||||||
> "dist/sboms/$image.spdx.json"
|
> "dist/sboms/$image.spdx.json"
|
||||||
done
|
done
|
||||||
@@ -43,6 +44,9 @@ jobs:
|
|||||||
mkdir -p dist/images
|
mkdir -p dist/images
|
||||||
docker save "teamclaw/server:$VERSION" -o dist/images/server.tar
|
docker save "teamclaw/server:$VERSION" -o dist/images/server.tar
|
||||||
docker save "teamclaw/frontend:$VERSION" -o dist/images/frontend.tar
|
docker save "teamclaw/frontend:$VERSION" -o dist/images/frontend.tar
|
||||||
|
docker save "teamclaw/broker:$VERSION" -o dist/images/broker.tar
|
||||||
|
docker pull tecnativa/docker-socket-proxy:0.3
|
||||||
|
docker save tecnativa/docker-socket-proxy:0.3 -o dist/images/socket-proxy.tar
|
||||||
docker save "teamclaw/agent-base:$VERSION" -o dist/images/agent-base.tar
|
docker save "teamclaw/agent-base:$VERSION" -o dist/images/agent-base.tar
|
||||||
docker save "teamclaw/agent-browser:$VERSION" -o dist/images/agent-browser.tar
|
docker save "teamclaw/agent-browser:$VERSION" -o dist/images/agent-browser.tar
|
||||||
docker save postgres:16-alpine -o dist/images/postgres.tar
|
docker save postgres:16-alpine -o dist/images/postgres.tar
|
||||||
@@ -98,6 +102,13 @@ jobs:
|
|||||||
- name: Tarball
|
- name: Tarball
|
||||||
run: tar -C dist -czf "teamclaw-bundle-$VERSION.tgz" bundle
|
run: tar -C dist -czf "teamclaw-bundle-$VERSION.tgz" bundle
|
||||||
|
|
||||||
|
- name: Clean-room install rehearsal
|
||||||
|
run: |
|
||||||
|
docker tag "teamclaw/server:$VERSION" teamclaw/server:latest
|
||||||
|
docker tag "teamclaw/frontend:$VERSION" teamclaw/frontend:latest
|
||||||
|
docker tag "teamclaw/broker:$VERSION" teamclaw/broker:latest
|
||||||
|
./scripts/rehearse-install.sh
|
||||||
|
|
||||||
- name: Attach to release
|
- name: Attach to release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
|
|||||||
Generated
+214
-6
@@ -1710,6 +1710,15 @@ version = "0.1.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "matchers"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
|
||||||
|
dependencies = [
|
||||||
|
"regex-automata",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matchit"
|
name = "matchit"
|
||||||
version = "0.8.4"
|
version = "0.8.4"
|
||||||
@@ -1765,6 +1774,15 @@ dependencies = [
|
|||||||
"minimal-lexical",
|
"minimal-lexical",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nu-ansi-term"
|
||||||
|
version = "0.50.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num"
|
name = "num"
|
||||||
version = "0.4.3"
|
version = "0.4.3"
|
||||||
@@ -1883,7 +1901,7 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"quick-xml",
|
"quick-xml",
|
||||||
"rand 0.9.4",
|
"rand 0.9.4",
|
||||||
"reqwest",
|
"reqwest 0.12.28",
|
||||||
"ring",
|
"ring",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -1921,6 +1939,76 @@ version = "0.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "opentelemetry"
|
||||||
|
version = "0.32.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b0142c63252a9e054e68a4c61a5778f7b14f576274d593f8ce883d191a099682"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
|
"js-sys",
|
||||||
|
"pin-project-lite",
|
||||||
|
"thiserror",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "opentelemetry-http"
|
||||||
|
version = "0.32.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5683015d09e2df236ef005b17f6f196f0d5f6313c4fa43a7b6a53b52776e4331"
|
||||||
|
dependencies = [
|
||||||
|
"async-trait",
|
||||||
|
"bytes",
|
||||||
|
"http",
|
||||||
|
"opentelemetry",
|
||||||
|
"reqwest 0.13.4",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "opentelemetry-otlp"
|
||||||
|
version = "0.32.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9966929966d17620d7c316c643ba62631826e10021409357772d5eea84f62c35"
|
||||||
|
dependencies = [
|
||||||
|
"http",
|
||||||
|
"opentelemetry",
|
||||||
|
"opentelemetry-http",
|
||||||
|
"opentelemetry-proto",
|
||||||
|
"opentelemetry_sdk",
|
||||||
|
"prost",
|
||||||
|
"reqwest 0.13.4",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "opentelemetry-proto"
|
||||||
|
version = "0.32.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "56d658ba1faf63f7b9c492cfbe6e0ec365440a16132d3270c1065f7b33f1b638"
|
||||||
|
dependencies = [
|
||||||
|
"opentelemetry",
|
||||||
|
"opentelemetry_sdk",
|
||||||
|
"prost",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "opentelemetry_sdk"
|
||||||
|
version = "0.32.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9b59f80e1ac4d5ff7a2db8fb6c80badb7f0f3f858211fba08dd9aaec750894f9"
|
||||||
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
|
"futures-executor",
|
||||||
|
"futures-util",
|
||||||
|
"opentelemetry",
|
||||||
|
"percent-encoding",
|
||||||
|
"portable-atomic",
|
||||||
|
"rand 0.9.4",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ordered-float"
|
name = "ordered-float"
|
||||||
version = "2.10.1"
|
version = "2.10.1"
|
||||||
@@ -2539,6 +2627,37 @@ dependencies = [
|
|||||||
"webpki-roots 1.0.7",
|
"webpki-roots 1.0.7",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "reqwest"
|
||||||
|
version = "0.13.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"bytes",
|
||||||
|
"futures-channel",
|
||||||
|
"futures-core",
|
||||||
|
"futures-util",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"hyper",
|
||||||
|
"hyper-util",
|
||||||
|
"js-sys",
|
||||||
|
"log",
|
||||||
|
"percent-encoding",
|
||||||
|
"pin-project-lite",
|
||||||
|
"sync_wrapper",
|
||||||
|
"tokio",
|
||||||
|
"tower",
|
||||||
|
"tower-http",
|
||||||
|
"tower-service",
|
||||||
|
"url",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ring"
|
name = "ring"
|
||||||
version = "0.17.14"
|
version = "0.17.14"
|
||||||
@@ -2957,6 +3076,15 @@ dependencies = [
|
|||||||
"digest",
|
"digest",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sharded-slab"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
|
||||||
|
dependencies = [
|
||||||
|
"lazy_static",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shlex"
|
name = "shlex"
|
||||||
version = "2.0.1"
|
version = "2.0.1"
|
||||||
@@ -3347,7 +3475,7 @@ dependencies = [
|
|||||||
"hmac",
|
"hmac",
|
||||||
"jsonwebtoken",
|
"jsonwebtoken",
|
||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
"reqwest",
|
"reqwest 0.12.28",
|
||||||
"rsa",
|
"rsa",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -3367,6 +3495,7 @@ dependencies = [
|
|||||||
"thiserror",
|
"thiserror",
|
||||||
"time",
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tower-http",
|
||||||
"urlencoding",
|
"urlencoding",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
@@ -3380,7 +3509,7 @@ dependencies = [
|
|||||||
"base64",
|
"base64",
|
||||||
"jsonwebtoken",
|
"jsonwebtoken",
|
||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
"reqwest",
|
"reqwest 0.12.28",
|
||||||
"rsa",
|
"rsa",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -3464,7 +3593,7 @@ dependencies = [
|
|||||||
"async-trait",
|
"async-trait",
|
||||||
"eventsource-stream",
|
"eventsource-stream",
|
||||||
"futures",
|
"futures",
|
||||||
"reqwest",
|
"reqwest 0.12.28",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
@@ -3525,7 +3654,7 @@ dependencies = [
|
|||||||
"futures",
|
"futures",
|
||||||
"k8s-openapi",
|
"k8s-openapi",
|
||||||
"kube",
|
"kube",
|
||||||
"reqwest",
|
"reqwest 0.12.28",
|
||||||
"rustls",
|
"rustls",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -3558,7 +3687,7 @@ dependencies = [
|
|||||||
"chacha20poly1305",
|
"chacha20poly1305",
|
||||||
"hex",
|
"hex",
|
||||||
"hmac",
|
"hmac",
|
||||||
"reqwest",
|
"reqwest 0.12.28",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2",
|
||||||
@@ -3573,6 +3702,23 @@ dependencies = [
|
|||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tc-telemetry"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"axum",
|
||||||
|
"opentelemetry",
|
||||||
|
"opentelemetry-otlp",
|
||||||
|
"opentelemetry-proto",
|
||||||
|
"opentelemetry_sdk",
|
||||||
|
"prost",
|
||||||
|
"thiserror",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"tracing-opentelemetry",
|
||||||
|
"tracing-subscriber",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tc-testkit"
|
name = "tc-testkit"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -3635,6 +3781,7 @@ dependencies = [
|
|||||||
"tc-runtime",
|
"tc-runtime",
|
||||||
"tc-sandbox",
|
"tc-sandbox",
|
||||||
"tc-scheduler",
|
"tc-scheduler",
|
||||||
|
"tc-telemetry",
|
||||||
"time",
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
@@ -3712,6 +3859,15 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thread_local"
|
||||||
|
version = "1.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time"
|
name = "time"
|
||||||
version = "0.3.47"
|
version = "0.3.47"
|
||||||
@@ -4004,6 +4160,52 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"valuable",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-log"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"tracing-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-opentelemetry"
|
||||||
|
version = "0.33.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "adbc64cba7137545b8044cb1fe9814f7aacf3c6b5f9b45be8bb5db538befdb26"
|
||||||
|
dependencies = [
|
||||||
|
"js-sys",
|
||||||
|
"opentelemetry",
|
||||||
|
"smallvec",
|
||||||
|
"tracing",
|
||||||
|
"tracing-core",
|
||||||
|
"tracing-log",
|
||||||
|
"tracing-subscriber",
|
||||||
|
"web-time",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-subscriber"
|
||||||
|
version = "0.3.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
|
||||||
|
dependencies = [
|
||||||
|
"matchers",
|
||||||
|
"nu-ansi-term",
|
||||||
|
"once_cell",
|
||||||
|
"regex-automata",
|
||||||
|
"sharded-slab",
|
||||||
|
"smallvec",
|
||||||
|
"thread_local",
|
||||||
|
"tracing",
|
||||||
|
"tracing-core",
|
||||||
|
"tracing-log",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4197,6 +4399,12 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "valuable"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vcpkg"
|
name = "vcpkg"
|
||||||
version = "0.2.15"
|
version = "0.2.15"
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ members = [
|
|||||||
"crates/tc-files",
|
"crates/tc-files",
|
||||||
"crates/tc-scheduler",
|
"crates/tc-scheduler",
|
||||||
"crates/tc-billing",
|
"crates/tc-billing",
|
||||||
|
"crates/tc-telemetry",
|
||||||
"crates/tc-testkit",
|
"crates/tc-testkit",
|
||||||
"crates/tc-auth",
|
"crates/tc-auth",
|
||||||
"crates/tc-api",
|
"crates/tc-api",
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ tc-db = { path = "../../tc-db" }
|
|||||||
tc-files = { path = "../../tc-files" }
|
tc-files = { path = "../../tc-files" }
|
||||||
tc-llm = { path = "../../tc-llm" }
|
tc-llm = { path = "../../tc-llm" }
|
||||||
tc-runtime = { path = "../../tc-runtime" }
|
tc-runtime = { path = "../../tc-runtime" }
|
||||||
|
tc-telemetry = { path = "../../tc-telemetry" }
|
||||||
tc-sandbox = { path = "../../tc-sandbox" }
|
tc-sandbox = { path = "../../tc-sandbox" }
|
||||||
tc-scheduler = { path = "../../tc-scheduler" }
|
tc-scheduler = { path = "../../tc-scheduler" }
|
||||||
tc-domain = { path = "../../tc-domain" }
|
tc-domain = { path = "../../tc-domain" }
|
||||||
|
|||||||
@@ -55,6 +55,9 @@ async fn run() -> Result<(), String> {
|
|||||||
let config_path =
|
let config_path =
|
||||||
PathBuf::from(std::env::var("TEAMCLAW_CONFIG").unwrap_or_else(|_| "teamclaw.toml".into()));
|
PathBuf::from(std::env::var("TEAMCLAW_CONFIG").unwrap_or_else(|_| "teamclaw.toml".into()));
|
||||||
let config = AppConfig::load_from(&config_path).map_err(|e| e.to_string())?;
|
let config = AppConfig::load_from(&config_path).map_err(|e| e.to_string())?;
|
||||||
|
let _telemetry =
|
||||||
|
tc_telemetry::init("teamclaw-server", config.telemetry.otlp_endpoint.as_deref())
|
||||||
|
.map_err(|e| format!("telemetry: {e}"))?;
|
||||||
|
|
||||||
let pool = tc_db::connect(&config.database.url, config.database.max_connections)
|
let pool = tc_db::connect(&config.database.url, config.database.max_connections)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ tc-safety = { path = "../tc-safety" }
|
|||||||
tc-scheduler = { path = "../tc-scheduler" }
|
tc-scheduler = { path = "../tc-scheduler" }
|
||||||
tc-secrets = { path = "../tc-secrets" }
|
tc-secrets = { path = "../tc-secrets" }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
|
tower-http = { version = "0.6", features = ["trace"] }
|
||||||
time = { workspace = true }
|
time = { workspace = true }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
urlencoding = "2"
|
urlencoding = "2"
|
||||||
|
|||||||
@@ -123,5 +123,6 @@ pub fn router(state: AppState) -> Router {
|
|||||||
.route("/api/team/usage", get(routes::billing::usage))
|
.route("/api/team/usage", get(routes::billing::usage))
|
||||||
.route("/api/credits/redeem", post(routes::billing::redeem))
|
.route("/api/credits/redeem", post(routes::billing::redeem))
|
||||||
.route("/api/team/permissions", get(routes::team::permissions))
|
.route("/api/team/permissions", get(routes::team::permissions))
|
||||||
|
.layer(tower_http::trace::TraceLayer::new_for_http())
|
||||||
.with_state(state)
|
.with_state(state)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -159,6 +159,13 @@ impl Default for SandboxConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, Deserialize)]
|
||||||
|
pub struct TelemetryConfig {
|
||||||
|
/// OTLP/HTTP collector base (e.g. http://otel-collector:4318).
|
||||||
|
/// Unset = no export; logs only. Nothing ever phones home uninvited.
|
||||||
|
pub otlp_endpoint: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, Deserialize)]
|
#[derive(Debug, Clone, Default, Deserialize)]
|
||||||
pub struct OAuthConfig {
|
pub struct OAuthConfig {
|
||||||
/// Default identity provider for directory-app OAuth connects.
|
/// Default identity provider for directory-app OAuth connects.
|
||||||
@@ -186,6 +193,8 @@ pub struct AppConfig {
|
|||||||
pub oauth: OAuthConfig,
|
pub oauth: OAuthConfig,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub sandbox: SandboxConfig,
|
pub sandbox: SandboxConfig,
|
||||||
|
#[serde(default)]
|
||||||
|
pub telemetry: TelemetryConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
[package]
|
||||||
|
name = "tc-telemetry"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
opentelemetry = "0.32"
|
||||||
|
opentelemetry-otlp = { version = "0.32", default-features = false, features = ["trace", "http-proto", "reqwest-blocking-client"] }
|
||||||
|
opentelemetry_sdk = "0.32"
|
||||||
|
thiserror = { workspace = true }
|
||||||
|
tracing = "0.1"
|
||||||
|
tracing-opentelemetry = "0.33"
|
||||||
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
axum = "0.8"
|
||||||
|
opentelemetry-proto = { version = "0.32", default-features = false, features = ["trace", "gen-tonic-messages"] }
|
||||||
|
prost = "0.14"
|
||||||
|
tokio = { workspace = true }
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
//! Tracing setup (§16 NFR: observability). Always installs a fmt
|
||||||
|
//! subscriber honoring `RUST_LOG`; with an OTLP endpoint configured,
|
||||||
|
//! spans also export over OTLP/HTTP — to a collector shipped in the
|
||||||
|
//! air-gapped bundle or any cloud backend. No endpoint, no export, no
|
||||||
|
//! network: telemetry never phones home uninvited.
|
||||||
|
|
||||||
|
use opentelemetry::trace::TracerProvider as _;
|
||||||
|
use opentelemetry::KeyValue;
|
||||||
|
use opentelemetry_otlp::WithExportConfig;
|
||||||
|
use opentelemetry_sdk::trace::SdkTracerProvider;
|
||||||
|
use opentelemetry_sdk::Resource;
|
||||||
|
use tracing_subscriber::layer::SubscriberExt;
|
||||||
|
use tracing_subscriber::util::SubscriberInitExt;
|
||||||
|
use tracing_subscriber::{EnvFilter, Layer};
|
||||||
|
|
||||||
|
/// Keeps the exporter alive; dropping it flushes pending spans.
|
||||||
|
pub struct TelemetryGuard {
|
||||||
|
provider: Option<SdkTracerProvider>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TelemetryGuard {
|
||||||
|
/// Forces all batched spans out — call before process exit (tests
|
||||||
|
/// call it to assert delivery).
|
||||||
|
pub fn flush(&self) {
|
||||||
|
if let Some(provider) = &self.provider {
|
||||||
|
let _ = provider.force_flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for TelemetryGuard {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let Some(provider) = self.provider.take() {
|
||||||
|
let _ = provider.force_flush();
|
||||||
|
let _ = provider.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum TelemetryError {
|
||||||
|
#[error("otlp exporter: {0}")]
|
||||||
|
Exporter(String),
|
||||||
|
#[error("subscriber already installed")]
|
||||||
|
AlreadyInstalled,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Installs the global subscriber. `otlp_endpoint` is the collector's
|
||||||
|
/// OTLP/HTTP base (e.g. `http://otel-collector:4318`).
|
||||||
|
pub fn init(
|
||||||
|
service_name: &str,
|
||||||
|
otlp_endpoint: Option<&str>,
|
||||||
|
) -> Result<TelemetryGuard, TelemetryError> {
|
||||||
|
let fmt = tracing_subscriber::fmt::layer().with_target(false);
|
||||||
|
let filter = EnvFilter::try_from_default_env()
|
||||||
|
.unwrap_or_else(|_| EnvFilter::new("info,sqlx=warn,hyper=warn"));
|
||||||
|
|
||||||
|
match otlp_endpoint {
|
||||||
|
Some(endpoint) => {
|
||||||
|
let exporter = opentelemetry_otlp::SpanExporter::builder()
|
||||||
|
.with_http()
|
||||||
|
.with_endpoint(format!("{}/v1/traces", endpoint.trim_end_matches('/')))
|
||||||
|
.build()
|
||||||
|
.map_err(|e| TelemetryError::Exporter(e.to_string()))?;
|
||||||
|
let provider = SdkTracerProvider::builder()
|
||||||
|
.with_batch_exporter(exporter)
|
||||||
|
.with_resource(
|
||||||
|
Resource::builder()
|
||||||
|
.with_attributes([KeyValue::new("service.name", service_name.to_owned())])
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.build();
|
||||||
|
let tracer = provider.tracer(service_name.to_owned());
|
||||||
|
let otel = tracing_opentelemetry::layer().with_tracer(tracer);
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(fmt.with_filter(filter))
|
||||||
|
.with(otel)
|
||||||
|
.try_init()
|
||||||
|
.map_err(|_| TelemetryError::AlreadyInstalled)?;
|
||||||
|
Ok(TelemetryGuard {
|
||||||
|
provider: Some(provider),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(fmt.with_filter(filter))
|
||||||
|
.try_init()
|
||||||
|
.map_err(|_| TelemetryError::AlreadyInstalled)?;
|
||||||
|
Ok(TelemetryGuard { provider: None })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
//! Spans reach a REAL OTLP/HTTP receiver: a live server accepting
|
||||||
|
//! protobuf at /v1/traces, decoded with the official proto types — the
|
||||||
|
//! exact wire contract any collector speaks.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use opentelemetry_proto::tonic::collector::trace::v1::ExportTraceServiceRequest;
|
||||||
|
use prost::Message;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
type Received = Arc<Mutex<Vec<ExportTraceServiceRequest>>>;
|
||||||
|
|
||||||
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
async fn spans_export_to_a_real_otlp_receiver() {
|
||||||
|
let received: Received = Arc::new(Mutex::new(Vec::new()));
|
||||||
|
let state = received.clone();
|
||||||
|
let app = axum::Router::new()
|
||||||
|
.route(
|
||||||
|
"/v1/traces",
|
||||||
|
axum::routing::post(
|
||||||
|
|axum::extract::State(state): axum::extract::State<Received>,
|
||||||
|
body: axum::body::Bytes| async move {
|
||||||
|
let request =
|
||||||
|
ExportTraceServiceRequest::decode(body.as_ref()).expect("otlp protobuf");
|
||||||
|
state.lock().await.push(request);
|
||||||
|
// An empty ExportTraceServiceResponse.
|
||||||
|
(
|
||||||
|
[("content-type", "application/x-protobuf")],
|
||||||
|
Vec::<u8>::new(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.with_state(state);
|
||||||
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||||
|
let endpoint = format!("http://{}", listener.local_addr().unwrap());
|
||||||
|
tokio::spawn(async move {
|
||||||
|
axum::serve(listener, app).await.unwrap();
|
||||||
|
});
|
||||||
|
|
||||||
|
let guard = tc_telemetry::init("teamclaw-test", Some(&endpoint)).expect("init");
|
||||||
|
|
||||||
|
{
|
||||||
|
let span = tracing::info_span!("approval_decision", approval_id = "ap_123");
|
||||||
|
let _entered = span.enter();
|
||||||
|
tracing::info!("decided");
|
||||||
|
}
|
||||||
|
guard.flush();
|
||||||
|
|
||||||
|
// Find our span among everything exported.
|
||||||
|
let requests = received.lock().await;
|
||||||
|
assert!(!requests.is_empty(), "no OTLP export arrived");
|
||||||
|
let mut span_names = Vec::new();
|
||||||
|
let mut service_names = Vec::new();
|
||||||
|
for request in requests.iter() {
|
||||||
|
for resource in &request.resource_spans {
|
||||||
|
if let Some(res) = &resource.resource {
|
||||||
|
for attr in &res.attributes {
|
||||||
|
if attr.key == "service.name" {
|
||||||
|
if let Some(value) = &attr.value {
|
||||||
|
service_names.push(format!("{value:?}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for scope in &resource.scope_spans {
|
||||||
|
for span in &scope.spans {
|
||||||
|
span_names.push(span.name.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
span_names.iter().any(|name| name == "approval_decision"),
|
||||||
|
"spans: {span_names:?}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
service_names.iter().any(|s| s.contains("teamclaw-test")),
|
||||||
|
"services: {service_names:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
Q2 revenue is up 14%.
|
||||||
@@ -24,6 +24,10 @@ networks:
|
|||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
pgdata: {}
|
pgdata: {}
|
||||||
|
# Unix socket + master key shared ONLY between server and broker —
|
||||||
|
# the unix-socket equivalent of the K8s sidecar topology.
|
||||||
|
broker_run: {}
|
||||||
|
broker_key: {}
|
||||||
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
@@ -41,6 +45,26 @@ services:
|
|||||||
timeout: 3s
|
timeout: 3s
|
||||||
retries: 12
|
retries: 12
|
||||||
|
|
||||||
|
# The secret broker: separate process, separate image; credentials
|
||||||
|
# never leave it. Reachable only via the shared unix socket volume.
|
||||||
|
broker:
|
||||||
|
image: teamclaw/broker:${TEAMCLAW_VERSION:-latest}
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: images/broker.Dockerfile
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
TEAMCLAW_BROKER_SOCKET: /run/teamclaw/broker.sock
|
||||||
|
TEAMCLAW_BROKER_KEY_FILE: /etc/teamclaw-broker/broker.key
|
||||||
|
TEAMCLAW_DATABASE__URL: postgres://postgres:${POSTGRES_PASSWORD:?set in .env}@postgres:5432/teamclaw
|
||||||
|
volumes:
|
||||||
|
- broker_run:/run/teamclaw
|
||||||
|
- broker_key:/etc/teamclaw-broker
|
||||||
|
networks: [core]
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
# Allow-listed Docker API (§15 blast-radius cap): the server can
|
# Allow-listed Docker API (§15 blast-radius cap): the server can
|
||||||
# create/exec/stop/remove sandbox containers and NOTHING else — no
|
# create/exec/stop/remove sandbox containers and NOTHING else — no
|
||||||
# image builds, no networks, no secrets, no volumes. Proven by
|
# image builds, no networks, no secrets, no volumes. Proven by
|
||||||
@@ -70,6 +94,7 @@ services:
|
|||||||
DOCKER_HOST: tcp://socket-proxy:2375
|
DOCKER_HOST: tcp://socket-proxy:2375
|
||||||
volumes:
|
volumes:
|
||||||
- ./teamclaw.toml:/etc/teamclaw/teamclaw.toml:ro
|
- ./teamclaw.toml:/etc/teamclaw/teamclaw.toml:ro
|
||||||
|
- broker_run:/run/teamclaw
|
||||||
networks: [edge, core, engine_net]
|
networks: [edge, core, engine_net]
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
|
|||||||
@@ -16,3 +16,6 @@ model = "qwen2.5-72b-instruct"
|
|||||||
|
|
||||||
[auth]
|
[auth]
|
||||||
mode = "local"
|
mode = "local"
|
||||||
|
|
||||||
|
[broker]
|
||||||
|
socket_path = "/run/teamclaw/broker.sock"
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# The secret broker: its own minimal image, its own process boundary.
|
||||||
|
# Shares the builder pattern with the server image; ships ONLY the broker
|
||||||
|
# binary.
|
||||||
|
FROM rust:1-bookworm AS builder
|
||||||
|
RUN case "$(uname -m)" in \
|
||||||
|
aarch64) echo aarch64-unknown-linux-musl > /rust-target ;; \
|
||||||
|
*) echo x86_64-unknown-linux-musl > /rust-target ;; \
|
||||||
|
esac \
|
||||||
|
&& rustup target add "$(cat /rust-target)" \
|
||||||
|
&& apt-get update && apt-get install -y --no-install-recommends musl-tools \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
WORKDIR /src
|
||||||
|
COPY Cargo.toml rust-toolchain.toml ./
|
||||||
|
COPY crates ./crates
|
||||||
|
COPY tools ./tools
|
||||||
|
COPY images/seccomp ./images/seccomp
|
||||||
|
COPY migrations ./migrations
|
||||||
|
COPY .sqlx ./.sqlx
|
||||||
|
ENV SQLX_OFFLINE=true
|
||||||
|
RUN cargo build --release --target "$(cat /rust-target)" -p teamclaw-broker \
|
||||||
|
&& cp "target/$(cat /rust-target)/release/teamclaw-broker" /teamclaw-broker
|
||||||
|
|
||||||
|
FROM scratch
|
||||||
|
COPY --from=builder /teamclaw-broker /teamclaw-broker
|
||||||
|
USER 10001:10001
|
||||||
|
ENTRYPOINT ["/teamclaw-broker"]
|
||||||
@@ -13,6 +13,8 @@ RUN case "$TARGETARCH" in \
|
|||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY Cargo.toml rust-toolchain.toml ./
|
COPY Cargo.toml rust-toolchain.toml ./
|
||||||
COPY crates ./crates
|
COPY crates ./crates
|
||||||
|
COPY tools ./tools
|
||||||
|
COPY images/seccomp ./images/seccomp
|
||||||
COPY migrations ./migrations
|
COPY migrations ./migrations
|
||||||
COPY .sqlx ./.sqlx
|
COPY .sqlx ./.sqlx
|
||||||
ENV SQLX_OFFLINE=true
|
ENV SQLX_OFFLINE=true
|
||||||
|
|||||||
Executable
+72
@@ -0,0 +1,72 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Clean-room install rehearsal (plan: per-release clean-VM install.sh
|
||||||
|
# rehearsal): assemble a REAL signed bundle from the built images, run
|
||||||
|
# the customer's install path end to end — offline verify, docker load,
|
||||||
|
# compose up — and assert the platform answers before tearing down.
|
||||||
|
#
|
||||||
|
# Requires: teamclaw/server:latest + teamclaw/frontend:latest built
|
||||||
|
# (POSTGRES_PASSWORD=x docker compose -f deploy/compose/docker-compose.yml build).
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
WORK="$(mktemp -d)"
|
||||||
|
export TEAMCLAW_HOME="$WORK/opt"
|
||||||
|
cleanup() {
|
||||||
|
docker compose --project-directory "$TEAMCLAW_HOME" down -v >/dev/null 2>&1 || true
|
||||||
|
rm -rf "$WORK"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
echo "==> Building bundler + signing key"
|
||||||
|
cargo build -q -p teamclaw-bundler
|
||||||
|
BUNDLER="$ROOT/target/debug/teamclaw-bundler"
|
||||||
|
"$BUNDLER" keygen "$WORK/release.key" "$WORK/release.pub"
|
||||||
|
|
||||||
|
echo "==> Saving runtime images"
|
||||||
|
mkdir -p "$WORK/images"
|
||||||
|
docker pull -q postgres:16-alpine >/dev/null
|
||||||
|
docker pull -q tecnativa/docker-socket-proxy:0.3 >/dev/null
|
||||||
|
docker save teamclaw/server:latest -o "$WORK/images/server.tar"
|
||||||
|
docker save teamclaw/frontend:latest -o "$WORK/images/frontend.tar"
|
||||||
|
docker save teamclaw/broker:latest -o "$WORK/images/broker.tar"
|
||||||
|
docker save postgres:16-alpine -o "$WORK/images/postgres.tar"
|
||||||
|
docker save tecnativa/docker-socket-proxy:0.3 -o "$WORK/images/socket-proxy.tar"
|
||||||
|
|
||||||
|
echo "==> Assembling the signed bundle"
|
||||||
|
ARTIFACTS=""
|
||||||
|
for tar in "$WORK"/images/*.tar; do
|
||||||
|
ARTIFACTS="$ARTIFACTS $tar=images/$(basename "$tar")"
|
||||||
|
done
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
"$BUNDLER" assemble "$WORK/bundle" "rehearsal" "$WORK/release.key" \
|
||||||
|
"$ROOT/deploy/compose/docker-compose.yml=compose/docker-compose.yml" \
|
||||||
|
"$ROOT/deploy/compose/teamclaw.toml=compose/teamclaw.toml" \
|
||||||
|
"$ROOT/deploy/compose/.env.example=compose/.env.example" \
|
||||||
|
"$ROOT/deploy/airgapped/install.sh=install.sh" \
|
||||||
|
"$BUNDLER=bin/teamclaw-bundler" \
|
||||||
|
$ARTIFACTS
|
||||||
|
chmod +x "$WORK/bundle/bin/teamclaw-bundler" "$WORK/bundle/install.sh"
|
||||||
|
|
||||||
|
echo "==> Customer install: verify -> load -> stage"
|
||||||
|
"$WORK/bundle/install.sh" "$WORK/bundle" "$WORK/release.pub"
|
||||||
|
|
||||||
|
echo "==> First boot"
|
||||||
|
echo "POSTGRES_PASSWORD=rehearse-$$" > "$TEAMCLAW_HOME/.env"
|
||||||
|
docker compose --project-directory "$TEAMCLAW_HOME" up -d --no-build
|
||||||
|
|
||||||
|
echo "==> Waiting for the platform"
|
||||||
|
for _ in $(seq 1 60); do
|
||||||
|
if curl -fsS http://127.0.0.1:8080/healthz >/dev/null 2>&1; then
|
||||||
|
echo "==> Server is healthy"
|
||||||
|
if curl -fsS http://127.0.0.1:3000/login | grep -q teamclaw; then
|
||||||
|
echo "==> Frontend serves the login page"
|
||||||
|
echo "REHEARSAL OK"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "REHEARSAL FAILED: platform never became healthy"
|
||||||
|
docker compose --project-directory "$TEAMCLAW_HOME" logs --tail 30
|
||||||
|
exit 1
|
||||||
Reference in New Issue
Block a user