Rebrand: TeamClaw -> Clawmates (clawmates.work)

Full-depth rename per the approved plan; the 'claw' product vocabulary
(claws, /claws routes, clawId, Claw Chat) stays — it is now the brand.

- Display brand: Clawmates (manifest, titles, hero, login/rail logo
  'clawmates'); default host app.clawmates.work; registry
  ghcr.io/clawmates
- Crates tc-* -> cm-* (16 crates + all imports); binaries
  clawmates-server/broker/bundler; images clawmates/*; env prefix
  CLAWMATES_* (+ CM_TEST_DATABASE_URL / CM_LIVE_LLM); config
  clawmates.toml; helm chart deploy/helm/clawmates with clawmates-*
  resources; db names clawmates*; sockets /run/clawmates; cookie
  cm_session; kind cluster clawmates-test; seccomp node profile
  clawmates-agent-profile.json
- All 9 Playwright brand assertions updated in lockstep; historical
  spec document left untouched as the only remaining 'TeamClaw'
- Local env migrated: dev pg clawmates-dev-pg/clawmates_dev, shared
  test server clawmates-test-pg, kind cluster recreated with image +
  profile, compose images rebuilt under clawmates/*

Verified end to end: 161 Rust + 68 frontend tests, 29 Playwright
journeys, 4 live kind tests, helm/install/LOC/placeholder gates, and
the clean-room install rehearsal serving the clawmates login page from
a signed bundle of the rebuilt images.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
Omar Sobh
2026-06-10 12:31:25 -05:00
co-authored by Claude Fable 5
parent 8046853feb
commit add4f79fed
209 changed files with 1429 additions and 1422 deletions
+3 -3
View File
@@ -1,12 +1,12 @@
# Point the whole test suite at the persistent shared Postgres managed by # Point the whole test suite at the persistent shared Postgres managed by
# `scripts/test-server.sh up`. With TC_TEST_DATABASE_URL set, tc-testkit never # `scripts/test-server.sh up`. With CM_TEST_DATABASE_URL set, cm-testkit never
# starts a per-test `postgres` testcontainer (it takes its `_container: None` # starts a per-test `postgres` testcontainer (it takes its `_container: None`
# branch), so a killed/panicking/orphaned `cargo test` has nothing to leak. # branch), so a killed/panicking/orphaned `cargo test` has nothing to leak.
# This is the durable fix for the runaway `postgres:11-alpine` containers. # This is the durable fix for the runaway `postgres:11-alpine` containers.
# #
# Not forced: an explicit TC_TEST_DATABASE_URL in the environment still wins # Not forced: an explicit CM_TEST_DATABASE_URL in the environment still wins
# (e.g. CI pointing at its own server). If the shared server is down, tests # (e.g. CI pointing at its own server). If the shared server is down, tests
# fail fast with a connection error instead of silently leaking containers — # fail fast with a connection error instead of silently leaking containers —
# run `scripts/test-server.sh up` first. # run `scripts/test-server.sh up` first.
[env] [env]
TC_TEST_DATABASE_URL = "postgres://postgres:[email protected]:54331/postgres" CM_TEST_DATABASE_URL = "postgres://postgres:[email protected]:54331/postgres"
+2 -2
View File
@@ -48,9 +48,9 @@ jobs:
- name: Create kind cluster - name: Create kind cluster
uses: helm/kind-action@v1 uses: helm/kind-action@v1
with: with:
cluster_name: teamclaw-test cluster_name: clawmates-test
- name: K8s sandbox kernel assertions - name: K8s sandbox kernel assertions
run: cargo test -p tc-sandbox --features k8s-tests --test k8s_security run: cargo test -p cm-sandbox --features k8s-tests --test k8s_security
frontend: frontend:
runs-on: ubuntu-latest runs-on: ubuntu-latest
+24 -24
View File
@@ -1,7 +1,7 @@
# Release: build the images both deploy targets share, assemble the # Release: build the images both deploy targets share, assemble the
# SIGNED air-gapped bundle, verify it offline, and attach everything to # SIGNED air-gapped bundle, verify it offline, and attach everything to
# the tag. The signing key lives in repo secrets (BUNDLE_SIGNING_KEY, # the tag. The signing key lives in repo secrets (BUNDLE_SIGNING_KEY,
# hex ed25519 from `teamclaw-bundler keygen`); the matching public key is # hex ed25519 from `clawmates-bundler keygen`); the matching public key is
# published out of band so customers can verify before docker load. # published out of band so customers can verify before docker load.
name: release name: release
@@ -22,11 +22,11 @@ jobs:
- name: Build images - name: Build images
run: | run: |
docker build -t "teamclaw/server:$VERSION" -f images/server.Dockerfile . docker build -t "clawmates/server:$VERSION" -f images/server.Dockerfile .
docker build -t "teamclaw/frontend:$VERSION" -f images/frontend.Dockerfile . docker build -t "clawmates/frontend:$VERSION" -f images/frontend.Dockerfile .
docker build -t "teamclaw/broker:$VERSION" -f images/broker.Dockerfile . docker build -t "clawmates/broker:$VERSION" -f images/broker.Dockerfile .
docker build -t "teamclaw/agent-base:$VERSION" images/agent-base docker build -t "clawmates/agent-base:$VERSION" images/agent-base
docker build -t "teamclaw/agent-browser:$VERSION" images/agent-browser docker build -t "clawmates/agent-browser:$VERSION" images/agent-browser
docker pull postgres:16-alpine docker pull postgres:16-alpine
- name: SBOMs for every shipped image - name: SBOMs for every shipped image
@@ -35,31 +35,31 @@ jobs:
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 broker agent-base agent-browser; do for image in server frontend broker agent-base agent-browser; do
syft "teamclaw/$image:$VERSION" -o spdx-json \ syft "clawmates/$image:$VERSION" -o spdx-json \
> "dist/sboms/$image.spdx.json" > "dist/sboms/$image.spdx.json"
done done
- name: Save image tarballs - name: Save image tarballs
run: | run: |
mkdir -p dist/images mkdir -p dist/images
docker save "teamclaw/server:$VERSION" -o dist/images/server.tar docker save "clawmates/server:$VERSION" -o dist/images/server.tar
docker save "teamclaw/frontend:$VERSION" -o dist/images/frontend.tar docker save "clawmates/frontend:$VERSION" -o dist/images/frontend.tar
docker save "teamclaw/broker:$VERSION" -o dist/images/broker.tar docker save "clawmates/broker:$VERSION" -o dist/images/broker.tar
docker pull tecnativa/docker-socket-proxy:0.3 docker pull tecnativa/docker-socket-proxy:0.3
docker save tecnativa/docker-socket-proxy:0.3 -o dist/images/socket-proxy.tar 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 "clawmates/agent-base:$VERSION" -o dist/images/agent-base.tar
docker save "teamclaw/agent-browser:$VERSION" -o dist/images/agent-browser.tar docker save "clawmates/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
- name: Build bundler - name: Build bundler
run: cargo build --release -p teamclaw-bundler run: cargo build --release -p clawmates-bundler
- name: Assemble and sign the bundle - name: Assemble and sign the bundle
env: env:
BUNDLE_SIGNING_KEY: ${{ secrets.BUNDLE_SIGNING_KEY }} BUNDLE_SIGNING_KEY: ${{ secrets.BUNDLE_SIGNING_KEY }}
run: | run: |
printf '%s' "$BUNDLE_SIGNING_KEY" > /tmp/release.key printf '%s' "$BUNDLE_SIGNING_KEY" > /tmp/release.key
BUNDLER=target/release/teamclaw-bundler BUNDLER=target/release/clawmates-bundler
ARTIFACTS="" ARTIFACTS=""
for tar in dist/images/*.tar; do for tar in dist/images/*.tar; do
ARTIFACTS="$ARTIFACTS $tar=images/$(basename "$tar")" ARTIFACTS="$ARTIFACTS $tar=images/$(basename "$tar")"
@@ -70,18 +70,18 @@ jobs:
# shellcheck disable=SC2086 # shellcheck disable=SC2086
"$BUNDLER" assemble dist/bundle "$VERSION" /tmp/release.key \ "$BUNDLER" assemble dist/bundle "$VERSION" /tmp/release.key \
deploy/compose/docker-compose.yml=compose/docker-compose.yml \ deploy/compose/docker-compose.yml=compose/docker-compose.yml \
deploy/compose/teamclaw.toml=compose/teamclaw.toml \ deploy/compose/clawmates.toml=compose/clawmates.toml \
deploy/compose/.env.example=compose/.env.example \ deploy/compose/.env.example=compose/.env.example \
deploy/e2e/scenarios.toml=compose/scenarios.toml \ deploy/e2e/scenarios.toml=compose/scenarios.toml \
images/seccomp/agent-profile.json=seccomp/agent-profile.json \ images/seccomp/agent-profile.json=seccomp/agent-profile.json \
deploy/airgapped/install.sh=install.sh \ deploy/airgapped/install.sh=install.sh \
"$BUNDLER"=bin/teamclaw-bundler \ "$BUNDLER"=bin/clawmates-bundler \
dist/sboms/server.spdx.json=sboms/server.spdx.json \ dist/sboms/server.spdx.json=sboms/server.spdx.json \
dist/sboms/frontend.spdx.json=sboms/frontend.spdx.json \ dist/sboms/frontend.spdx.json=sboms/frontend.spdx.json \
dist/sboms/agent-base.spdx.json=sboms/agent-base.spdx.json \ dist/sboms/agent-base.spdx.json=sboms/agent-base.spdx.json \
dist/sboms/agent-browser.spdx.json=sboms/agent-browser.spdx.json \ dist/sboms/agent-browser.spdx.json=sboms/agent-browser.spdx.json \
$ARTIFACTS $ARTIFACTS
chmod +x dist/bundle/bin/teamclaw-bundler dist/bundle/install.sh chmod +x dist/bundle/bin/clawmates-bundler dist/bundle/install.sh
rm /tmp/release.key rm /tmp/release.key
- name: Verify the bundle offline (public key only) - name: Verify the bundle offline (public key only)
@@ -89,7 +89,7 @@ jobs:
BUNDLE_SIGNING_KEY: ${{ secrets.BUNDLE_SIGNING_KEY }} BUNDLE_SIGNING_KEY: ${{ secrets.BUNDLE_SIGNING_KEY }}
run: | run: |
printf '%s' "$BUNDLE_SIGNING_KEY" > /tmp/release.key printf '%s' "$BUNDLE_SIGNING_KEY" > /tmp/release.key
target/release/teamclaw-bundler pubkey /tmp/release.key dist/release.pub target/release/clawmates-bundler pubkey /tmp/release.key dist/release.pub
rm /tmp/release.key rm /tmp/release.key
# The customer's exact procedure: only the public half — and # The customer's exact procedure: only the public half — and
# inside a NETWORK-DISABLED container, proving verification # inside a NETWORK-DISABLED container, proving verification
@@ -97,21 +97,21 @@ jobs:
docker run --rm --network none \ docker run --rm --network none \
-v "$PWD/dist:/dist:ro" \ -v "$PWD/dist:/dist:ro" \
ubuntu:24.04 \ ubuntu:24.04 \
/dist/bundle/bin/teamclaw-bundler verify /dist/bundle /dist/release.pub /dist/bundle/bin/clawmates-bundler verify /dist/bundle /dist/release.pub
- name: Tarball - name: Tarball
run: tar -C dist -czf "teamclaw-bundle-$VERSION.tgz" bundle run: tar -C dist -czf "clawmates-bundle-$VERSION.tgz" bundle
- name: Clean-room install rehearsal - name: Clean-room install rehearsal
run: | run: |
docker tag "teamclaw/server:$VERSION" teamclaw/server:latest docker tag "clawmates/server:$VERSION" clawmates/server:latest
docker tag "teamclaw/frontend:$VERSION" teamclaw/frontend:latest docker tag "clawmates/frontend:$VERSION" clawmates/frontend:latest
docker tag "teamclaw/broker:$VERSION" teamclaw/broker:latest docker tag "clawmates/broker:$VERSION" clawmates/broker:latest
./scripts/rehearse-install.sh ./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:
files: | files: |
teamclaw-bundle-*.tgz clawmates-bundle-*.tgz
dist/release.pub dist/release.pub
Generated
+324 -324
View File
@@ -433,6 +433,330 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "clawmates-broker"
version = "0.1.0"
dependencies = [
"cm-db",
"cm-secrets",
"tokio",
]
[[package]]
name = "clawmates-bundler"
version = "0.1.0"
dependencies = [
"ed25519-dalek",
"hex",
"rand_core 0.6.4",
"serde",
"serde_json",
"sha2",
"thiserror",
"uuid",
]
[[package]]
name = "clawmates-server"
version = "0.1.0"
dependencies = [
"axum",
"cm-api",
"cm-auth",
"cm-config",
"cm-db",
"cm-domain",
"cm-files",
"cm-llm",
"cm-runtime",
"cm-sandbox",
"cm-scheduler",
"cm-telemetry",
"serde_json",
"sqlx",
"time",
"tokio",
]
[[package]]
name = "cm-api"
version = "0.1.0"
dependencies = [
"async-stream",
"axum",
"base64",
"cm-auth",
"cm-billing",
"cm-config",
"cm-db",
"cm-domain",
"cm-llm",
"cm-runtime",
"cm-safety",
"cm-scheduler",
"cm-secrets",
"cm-testkit",
"eventsource-stream",
"futures",
"hex",
"hmac",
"jsonwebtoken",
"rand_core 0.6.4",
"reqwest 0.12.28",
"rsa",
"serde",
"serde_json",
"sha2",
"sqlx",
"thiserror",
"time",
"tokio",
"tower-http",
"urlencoding",
"uuid",
]
[[package]]
name = "cm-auth"
version = "0.1.0"
dependencies = [
"argon2",
"axum",
"base64",
"cm-db",
"cm-domain",
"cm-testkit",
"jsonwebtoken",
"rand_core 0.6.4",
"reqwest 0.12.28",
"rsa",
"serde",
"serde_json",
"sha2",
"sqlx",
"thiserror",
"time",
"tokio",
]
[[package]]
name = "cm-billing"
version = "0.1.0"
dependencies = [
"cm-db",
"cm-domain",
"cm-testkit",
"sqlx",
"thiserror",
"time",
"tokio",
"uuid",
]
[[package]]
name = "cm-config"
version = "0.1.0"
dependencies = [
"figment",
"serde",
"thiserror",
]
[[package]]
name = "cm-db"
version = "0.1.0"
dependencies = [
"cm-domain",
"cm-testkit",
"serde",
"serde_json",
"sqlx",
"thiserror",
"time",
"tokio",
"uuid",
]
[[package]]
name = "cm-domain"
version = "0.1.0"
dependencies = [
"proptest",
"serde",
"serde_json",
"thiserror",
"time",
"uuid",
]
[[package]]
name = "cm-files"
version = "0.1.0"
dependencies = [
"async-trait",
"object_store",
"testcontainers-modules",
"thiserror",
"tokio",
"uuid",
]
[[package]]
name = "cm-llm"
version = "0.1.0"
dependencies = [
"async-stream",
"async-trait",
"eventsource-stream",
"futures",
"reqwest 0.12.28",
"serde",
"serde_json",
"thiserror",
"tokio",
"toml",
]
[[package]]
name = "cm-runtime"
version = "0.1.0"
dependencies = [
"async-trait",
"axum",
"chrono",
"cm-billing",
"cm-db",
"cm-domain",
"cm-files",
"cm-llm",
"cm-safety",
"cm-sandbox",
"cm-secrets",
"cm-testkit",
"cm-tools",
"croner",
"futures",
"serde",
"serde_json",
"sqlx",
"thiserror",
"time",
"tokio",
"uuid",
]
[[package]]
name = "cm-safety"
version = "0.1.0"
dependencies = [
"cm-db",
"cm-domain",
"cm-testkit",
"serde",
"serde_json",
"sqlx",
"thiserror",
"time",
"tokio",
"uuid",
]
[[package]]
name = "cm-sandbox"
version = "0.1.0"
dependencies = [
"async-trait",
"bollard",
"futures",
"k8s-openapi",
"kube",
"reqwest 0.12.28",
"rustls",
"serde",
"serde_json",
"thiserror",
"tokio",
]
[[package]]
name = "cm-scheduler"
version = "0.1.0"
dependencies = [
"cm-db",
"cm-domain",
"cm-llm",
"cm-runtime",
"cm-testkit",
"serde_json",
"sqlx",
"thiserror",
"time",
"tokio",
"uuid",
]
[[package]]
name = "cm-secrets"
version = "0.1.0"
dependencies = [
"axum",
"chacha20poly1305",
"cm-db",
"cm-domain",
"cm-safety",
"cm-testkit",
"hex",
"hmac",
"reqwest 0.12.28",
"serde",
"serde_json",
"sha2",
"sqlx",
"thiserror",
"time",
"tokio",
"uuid",
]
[[package]]
name = "cm-telemetry"
version = "0.1.0"
dependencies = [
"axum",
"opentelemetry",
"opentelemetry-otlp",
"opentelemetry-proto",
"opentelemetry_sdk",
"prost",
"thiserror",
"tokio",
"tracing",
"tracing-opentelemetry",
"tracing-subscriber",
]
[[package]]
name = "cm-testkit"
version = "0.1.0"
dependencies = [
"cm-db",
"sqlx",
"testcontainers",
"testcontainers-modules",
"tokio",
"uuid",
]
[[package]]
name = "cm-tools"
version = "0.1.0"
dependencies = [
"cm-domain",
"proptest",
"serde",
"serde_json",
]
[[package]] [[package]]
name = "concurrent-queue" name = "concurrent-queue"
version = "2.5.0" version = "2.5.0"
@@ -3462,330 +3786,6 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "tc-api"
version = "0.1.0"
dependencies = [
"async-stream",
"axum",
"base64",
"eventsource-stream",
"futures",
"hex",
"hmac",
"jsonwebtoken",
"rand_core 0.6.4",
"reqwest 0.12.28",
"rsa",
"serde",
"serde_json",
"sha2",
"sqlx",
"tc-auth",
"tc-billing",
"tc-config",
"tc-db",
"tc-domain",
"tc-llm",
"tc-runtime",
"tc-safety",
"tc-scheduler",
"tc-secrets",
"tc-testkit",
"thiserror",
"time",
"tokio",
"tower-http",
"urlencoding",
"uuid",
]
[[package]]
name = "tc-auth"
version = "0.1.0"
dependencies = [
"argon2",
"axum",
"base64",
"jsonwebtoken",
"rand_core 0.6.4",
"reqwest 0.12.28",
"rsa",
"serde",
"serde_json",
"sha2",
"sqlx",
"tc-db",
"tc-domain",
"tc-testkit",
"thiserror",
"time",
"tokio",
]
[[package]]
name = "tc-billing"
version = "0.1.0"
dependencies = [
"sqlx",
"tc-db",
"tc-domain",
"tc-testkit",
"thiserror",
"time",
"tokio",
"uuid",
]
[[package]]
name = "tc-config"
version = "0.1.0"
dependencies = [
"figment",
"serde",
"thiserror",
]
[[package]]
name = "tc-db"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"sqlx",
"tc-domain",
"tc-testkit",
"thiserror",
"time",
"tokio",
"uuid",
]
[[package]]
name = "tc-domain"
version = "0.1.0"
dependencies = [
"proptest",
"serde",
"serde_json",
"thiserror",
"time",
"uuid",
]
[[package]]
name = "tc-files"
version = "0.1.0"
dependencies = [
"async-trait",
"object_store",
"testcontainers-modules",
"thiserror",
"tokio",
"uuid",
]
[[package]]
name = "tc-llm"
version = "0.1.0"
dependencies = [
"async-stream",
"async-trait",
"eventsource-stream",
"futures",
"reqwest 0.12.28",
"serde",
"serde_json",
"thiserror",
"tokio",
"toml",
]
[[package]]
name = "tc-runtime"
version = "0.1.0"
dependencies = [
"async-trait",
"axum",
"chrono",
"croner",
"futures",
"serde",
"serde_json",
"sqlx",
"tc-billing",
"tc-db",
"tc-domain",
"tc-files",
"tc-llm",
"tc-safety",
"tc-sandbox",
"tc-secrets",
"tc-testkit",
"tc-tools",
"thiserror",
"time",
"tokio",
"uuid",
]
[[package]]
name = "tc-safety"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
"sqlx",
"tc-db",
"tc-domain",
"tc-testkit",
"thiserror",
"time",
"tokio",
"uuid",
]
[[package]]
name = "tc-sandbox"
version = "0.1.0"
dependencies = [
"async-trait",
"bollard",
"futures",
"k8s-openapi",
"kube",
"reqwest 0.12.28",
"rustls",
"serde",
"serde_json",
"thiserror",
"tokio",
]
[[package]]
name = "tc-scheduler"
version = "0.1.0"
dependencies = [
"serde_json",
"sqlx",
"tc-db",
"tc-domain",
"tc-llm",
"tc-runtime",
"tc-testkit",
"thiserror",
"time",
"tokio",
"uuid",
]
[[package]]
name = "tc-secrets"
version = "0.1.0"
dependencies = [
"axum",
"chacha20poly1305",
"hex",
"hmac",
"reqwest 0.12.28",
"serde",
"serde_json",
"sha2",
"sqlx",
"tc-db",
"tc-domain",
"tc-safety",
"tc-testkit",
"thiserror",
"time",
"tokio",
"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]]
name = "tc-testkit"
version = "0.1.0"
dependencies = [
"sqlx",
"tc-db",
"testcontainers",
"testcontainers-modules",
"tokio",
"uuid",
]
[[package]]
name = "tc-tools"
version = "0.1.0"
dependencies = [
"proptest",
"serde",
"serde_json",
"tc-domain",
]
[[package]]
name = "teamclaw-broker"
version = "0.1.0"
dependencies = [
"tc-db",
"tc-secrets",
"tokio",
]
[[package]]
name = "teamclaw-bundler"
version = "0.1.0"
dependencies = [
"ed25519-dalek",
"hex",
"rand_core 0.6.4",
"serde",
"serde_json",
"sha2",
"thiserror",
"uuid",
]
[[package]]
name = "teamclaw-server"
version = "0.1.0"
dependencies = [
"axum",
"serde_json",
"sqlx",
"tc-api",
"tc-auth",
"tc-config",
"tc-db",
"tc-domain",
"tc-files",
"tc-llm",
"tc-runtime",
"tc-sandbox",
"tc-scheduler",
"tc-telemetry",
"time",
"tokio",
]
[[package]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.27.0" version = "3.27.0"
+18 -18
View File
@@ -1,24 +1,24 @@
[workspace] [workspace]
resolver = "2" resolver = "2"
members = [ members = [
"crates/tc-domain", "crates/cm-domain",
"crates/tc-config", "crates/cm-config",
"crates/tc-db", "crates/cm-db",
"crates/tc-llm", "crates/cm-llm",
"crates/tc-runtime", "crates/cm-runtime",
"crates/tc-tools", "crates/cm-tools",
"crates/tc-safety", "crates/cm-safety",
"crates/tc-sandbox", "crates/cm-sandbox",
"crates/tc-secrets", "crates/cm-secrets",
"crates/tc-files", "crates/cm-files",
"crates/tc-scheduler", "crates/cm-scheduler",
"crates/tc-billing", "crates/cm-billing",
"crates/tc-telemetry", "crates/cm-telemetry",
"crates/tc-testkit", "crates/cm-testkit",
"crates/tc-auth", "crates/cm-auth",
"crates/tc-api", "crates/cm-api",
"crates/bins/teamclaw-server", "crates/bins/clawmates-server",
"crates/bins/teamclaw-broker", "crates/bins/clawmates-broker",
"tools/bundler", "tools/bundler",
] ]
+10 -10
View File
@@ -5,15 +5,15 @@
set -euo pipefail set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)" ROOT="$(cd "$(dirname "$0")/.." && pwd)"
CHART="$ROOT/deploy/helm/teamclaw" CHART="$ROOT/deploy/helm/clawmates"
helm lint "$CHART" \ helm lint "$CHART" \
--set auth.issuerUrl=https://idp.example.com \ --set auth.issuerUrl=https://idp.example.com \
--set oauth.redirectBase=https://teamclaw.example.com --set oauth.redirectBase=https://app.clawmates.work
RENDERED=$(helm template teamclaw "$CHART" \ RENDERED=$(helm template clawmates "$CHART" \
--set auth.issuerUrl=https://idp.example.com \ --set auth.issuerUrl=https://idp.example.com \
--set oauth.redirectBase=https://teamclaw.example.com) --set oauth.redirectBase=https://app.clawmates.work)
require() { require() {
if ! grep -qF -- "$1" <<<"$RENDERED"; then if ! grep -qF -- "$1" <<<"$RENDERED"; then
@@ -24,8 +24,8 @@ require() {
# Broker sidecar shares the unix-socket emptyDir with the server. # Broker sidecar shares the unix-socket emptyDir with the server.
require 'name: broker' require 'name: broker'
require 'value: /run/teamclaw/broker.sock' require 'value: /run/clawmates/broker.sock'
require 'mountPath: /run/teamclaw' require 'mountPath: /run/clawmates'
# SSE must not buffer at the ingress. # SSE must not buffer at the ingress.
require 'nginx.ingress.kubernetes.io/proxy-buffering: "off"' require 'nginx.ingress.kubernetes.io/proxy-buffering: "off"'
# Hardened pods. # Hardened pods.
@@ -33,7 +33,7 @@ require 'runAsNonRoot: true'
require 'drop: ["ALL"]' require 'drop: ["ALL"]'
require 'readOnlyRootFilesystem: true' require 'readOnlyRootFilesystem: true'
# Config wired through the ConfigMap. # Config wired through the ConfigMap.
require 'socket_path = "/run/teamclaw/broker.sock"' require 'socket_path = "/run/clawmates/broker.sock"'
# The chart-shipped seccomp profile must BE the Docker driver's profile. # The chart-shipped seccomp profile must BE the Docker driver's profile.
if ! diff -q "$ROOT/images/seccomp/agent-profile.json" \ if ! diff -q "$ROOT/images/seccomp/agent-profile.json" \
@@ -42,14 +42,14 @@ if ! diff -q "$ROOT/images/seccomp/agent-profile.json" \
exit 1 exit 1
fi fi
HARDENED=$(helm template teamclaw "$CHART" \ HARDENED=$(helm template clawmates "$CHART" \
--set auth.issuerUrl=https://idp.example.com \ --set auth.issuerUrl=https://idp.example.com \
--set oauth.redirectBase=https://teamclaw.example.com \ --set oauth.redirectBase=https://app.clawmates.work \
--set sandbox.seccomp=localhost \ --set sandbox.seccomp=localhost \
--set server.autoscaling.enabled=true) --set server.autoscaling.enabled=true)
for needle in \ for needle in \
'kind: DaemonSet' \ 'kind: DaemonSet' \
'teamclaw-agent-profile.json' \ 'clawmates-agent-profile.json' \
'kind: HorizontalPodAutoscaler' \ 'kind: HorizontalPodAutoscaler' \
'averageUtilization: 70'; do 'averageUtilization: 70'; do
if ! grep -qF -- "$needle" <<<"$HARDENED"; then if ! grep -qF -- "$needle" <<<"$HARDENED"; then
+6 -6
View File
@@ -8,8 +8,8 @@ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
WORK="$(mktemp -d)" WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT trap 'rm -rf "$WORK"' EXIT
cargo build -q -p teamclaw-bundler cargo build -q -p clawmates-bundler
BUNDLER="$ROOT/target/debug/teamclaw-bundler" BUNDLER="$ROOT/target/debug/clawmates-bundler"
"$BUNDLER" keygen "$WORK/release.key" "$WORK/release.pub" "$BUNDLER" keygen "$WORK/release.key" "$WORK/release.pub"
@@ -17,16 +17,16 @@ BUNDLER="$ROOT/target/debug/teamclaw-bundler"
echo "pretend image bytes" > "$WORK/server.tar" echo "pretend image bytes" > "$WORK/server.tar"
mkdir -p "$WORK/compose-src" mkdir -p "$WORK/compose-src"
cp "$ROOT/deploy/compose/docker-compose.yml" "$WORK/compose-src/" cp "$ROOT/deploy/compose/docker-compose.yml" "$WORK/compose-src/"
cp "$ROOT/deploy/compose/teamclaw.toml" "$WORK/compose-src/" cp "$ROOT/deploy/compose/clawmates.toml" "$WORK/compose-src/"
cp "$ROOT/deploy/compose/.env.example" "$WORK/compose-src/" cp "$ROOT/deploy/compose/.env.example" "$WORK/compose-src/"
"$BUNDLER" assemble "$WORK/bundle" "0.0.0-test" "$WORK/release.key" \ "$BUNDLER" assemble "$WORK/bundle" "0.0.0-test" "$WORK/release.key" \
"$WORK/server.tar=images/server.tar" \ "$WORK/server.tar=images/server.tar" \
"$WORK/compose-src/docker-compose.yml=compose/docker-compose.yml" \ "$WORK/compose-src/docker-compose.yml=compose/docker-compose.yml" \
"$WORK/compose-src/teamclaw.toml=compose/teamclaw.toml" \ "$WORK/compose-src/clawmates.toml=compose/clawmates.toml" \
"$WORK/compose-src/.env.example=compose/.env.example" \ "$WORK/compose-src/.env.example=compose/.env.example" \
"$BUNDLER=bin/teamclaw-bundler" "$BUNDLER=bin/clawmates-bundler"
chmod +x "$WORK/bundle/bin/teamclaw-bundler" chmod +x "$WORK/bundle/bin/clawmates-bundler"
# 1. A clean bundle verifies. # 1. A clean bundle verifies.
"$ROOT/deploy/airgapped/install.sh" "$WORK/bundle" "$WORK/release.pub" --verify-only "$ROOT/deploy/airgapped/install.sh" "$WORK/bundle" "$WORK/release.pub" --verify-only
@@ -1,5 +1,5 @@
[package] [package]
name = "teamclaw-broker" name = "clawmates-broker"
version = "0.1.0" version = "0.1.0"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
@@ -7,8 +7,8 @@ license.workspace = true
publish.workspace = true publish.workspace = true
[dependencies] [dependencies]
tc-db = { path = "../../tc-db" } cm-db = { path = "../../cm-db" }
tc-secrets = { path = "../../tc-secrets" } cm-secrets = { path = "../../cm-secrets" }
tokio = { workspace = true } tokio = { workspace = true }
[lints] [lints]
@@ -3,53 +3,53 @@
//! it by construction. //! it by construction.
//! //!
//! Configuration (environment): //! Configuration (environment):
//! - `TEAMCLAW_DATABASE__URL` Postgres connection string (required) //! - `CLAWMATES_DATABASE__URL` Postgres connection string (required)
//! - `TEAMCLAW_BROKER_SOCKET` unix socket path (default /tmp/teamclaw-broker.sock) //! - `CLAWMATES_BROKER_SOCKET` unix socket path (default /tmp/clawmates-broker.sock)
//! - `TEAMCLAW_BROKER_KEY_FILE` master key file; generated on first boot //! - `CLAWMATES_BROKER_KEY_FILE` master key file; generated on first boot
use std::path::PathBuf; use std::path::PathBuf;
use std::process::ExitCode; use std::process::ExitCode;
use tc_secrets::{BrokerServer, FileKey}; use cm_secrets::{BrokerServer, FileKey};
#[tokio::main] #[tokio::main]
async fn main() -> ExitCode { async fn main() -> ExitCode {
match run().await { match run().await {
Ok(()) => ExitCode::SUCCESS, Ok(()) => ExitCode::SUCCESS,
Err(message) => { Err(message) => {
eprintln!("teamclaw-broker: {message}"); eprintln!("clawmates-broker: {message}");
ExitCode::FAILURE ExitCode::FAILURE
} }
} }
} }
async fn run() -> Result<(), String> { async fn run() -> Result<(), String> {
let database_url = std::env::var("TEAMCLAW_DATABASE__URL") let database_url = std::env::var("CLAWMATES_DATABASE__URL")
.map_err(|_| "TEAMCLAW_DATABASE__URL is required")?; .map_err(|_| "CLAWMATES_DATABASE__URL is required")?;
let socket_path = PathBuf::from( let socket_path = PathBuf::from(
std::env::var("TEAMCLAW_BROKER_SOCKET") std::env::var("CLAWMATES_BROKER_SOCKET")
.unwrap_or_else(|_| "/tmp/teamclaw-broker.sock".into()), .unwrap_or_else(|_| "/tmp/clawmates-broker.sock".into()),
); );
let key_path = PathBuf::from( let key_path = PathBuf::from(
std::env::var("TEAMCLAW_BROKER_KEY_FILE") std::env::var("CLAWMATES_BROKER_KEY_FILE")
.unwrap_or_else(|_| "/etc/teamclaw/broker.key".into()), .unwrap_or_else(|_| "/etc/clawmates/broker.key".into()),
); );
if !key_path.exists() { if !key_path.exists() {
FileKey::generate(&key_path).map_err(|e| format!("key generation failed: {e}"))?; FileKey::generate(&key_path).map_err(|e| format!("key generation failed: {e}"))?;
println!( println!(
"teamclaw-broker: generated master key at {} — BACK IT UP; \ "clawmates-broker: generated master key at {} — BACK IT UP; \
secrets are unrecoverable without it", secrets are unrecoverable without it",
key_path.display() key_path.display()
); );
} }
let key = FileKey::load(&key_path).map_err(|e| format!("key load failed: {e}"))?; let key = FileKey::load(&key_path).map_err(|e| format!("key load failed: {e}"))?;
let pool = tc_db::connect(&database_url, 5) let pool = cm_db::connect(&database_url, 5)
.await .await
.map_err(|e| format!("database connection failed: {e}"))?; .map_err(|e| format!("database connection failed: {e}"))?;
println!("teamclaw-broker listening on {}", socket_path.display()); println!("clawmates-broker listening on {}", socket_path.display());
BrokerServer::new(pool, key, socket_path) BrokerServer::new(pool, key, socket_path)
.serve() .serve()
.await .await
+28
View File
@@ -0,0 +1,28 @@
[package]
name = "clawmates-server"
version = "0.1.0"
edition.workspace = true
rust-version.workspace = true
license.workspace = true
publish.workspace = true
[dependencies]
axum = "0.8"
serde_json = { workspace = true }
sqlx = { workspace = true }
cm-api = { path = "../../cm-api" }
cm-auth = { path = "../../cm-auth" }
cm-config = { path = "../../cm-config" }
cm-db = { path = "../../cm-db" }
cm-files = { path = "../../cm-files" }
cm-llm = { path = "../../cm-llm" }
cm-runtime = { path = "../../cm-runtime" }
cm-telemetry = { path = "../../cm-telemetry" }
cm-sandbox = { path = "../../cm-sandbox" }
cm-scheduler = { path = "../../cm-scheduler" }
cm-domain = { path = "../../cm-domain" }
time = { workspace = true }
tokio = { workspace = true }
[lints]
workspace = true
@@ -1,15 +1,15 @@
//! Deterministic seed data for end-to-end tests. //! Deterministic seed data for end-to-end tests.
//! //!
//! Active only when `TEAMCLAW_MODE=e2e`. Idempotent: a fixed workspace, //! Active only when `CLAWMATES_MODE=e2e`. Idempotent: a fixed workspace,
//! owner, agent, and credit balance that Playwright journeys assert against. //! owner, agent, and credit balance that Playwright journeys assert against.
//! This is real production code driving the real registration paths — the //! This is real production code driving the real registration paths — the
//! only test-specific part is the fixture data itself. //! only test-specific part is the fixture data itself.
use sqlx::PgPool; use cm_auth::AuthService;
use tc_auth::AuthService; use cm_domain::{
use tc_domain::{
AccessPolicy, Agent, AgentId, AgentStatus, Role, User, UserId, Workspace, WorkspaceId, AccessPolicy, Agent, AgentId, AgentStatus, Role, User, UserId, Workspace, WorkspaceId,
}; };
use sqlx::PgPool;
/// In e2e mode the server hosts a Slack-shaped sink so journeys can assert /// In e2e mode the server hosts a Slack-shaped sink so journeys can assert
/// exactly what the broker posted, without external infrastructure. /// exactly what the broker posted, without external infrastructure.
@@ -44,11 +44,11 @@ pub const E2E_OWNER_EMAIL: &str = "[email protected]";
pub const E2E_OWNER_PASSWORD: &str = "e2e-password"; pub const E2E_OWNER_PASSWORD: &str = "e2e-password";
pub fn enabled() -> bool { pub fn enabled() -> bool {
std::env::var("TEAMCLAW_MODE").as_deref() == Ok("e2e") std::env::var("CLAWMATES_MODE").as_deref() == Ok("e2e")
} }
pub async fn seed(pool: &PgPool) -> Result<(), String> { pub async fn seed(pool: &PgPool) -> Result<(), String> {
if tc_db::repo::users::find_by_email(pool, E2E_OWNER_EMAIL) if cm_db::repo::users::find_by_email(pool, E2E_OWNER_EMAIL)
.await .await
.is_ok() .is_ok()
{ {
@@ -60,7 +60,7 @@ pub async fn seed(pool: &PgPool) -> Result<(), String> {
name: "Acme".into(), name: "Acme".into(),
plan: "team".into(), plan: "team".into(),
}; };
tc_db::repo::workspaces::insert(pool, &workspace) cm_db::repo::workspaces::insert(pool, &workspace)
.await .await
.map_err(|e| format!("seed workspace: {e}"))?; .map_err(|e| format!("seed workspace: {e}"))?;
@@ -72,7 +72,7 @@ pub async fn seed(pool: &PgPool) -> Result<(), String> {
display_name: "Avery Owner".into(), display_name: "Avery Owner".into(),
created_at: time::OffsetDateTime::UNIX_EPOCH, created_at: time::OffsetDateTime::UNIX_EPOCH,
}; };
tc_db::repo::users::insert(pool, &owner) cm_db::repo::users::insert(pool, &owner)
.await .await
.map_err(|e| format!("seed owner: {e}"))?; .map_err(|e| format!("seed owner: {e}"))?;
AuthService::new(pool.clone()) AuthService::new(pool.clone())
@@ -92,19 +92,19 @@ pub async fn seed(pool: &PgPool) -> Result<(), String> {
managed_by: owner.id, managed_by: owner.id,
status: AgentStatus::Online, status: AgentStatus::Online,
}; };
tc_db::repo::agents::insert(pool, &agent, &AccessPolicy::default()) cm_db::repo::agents::insert(pool, &agent, &AccessPolicy::default())
.await .await
.map_err(|e| format!("seed agent: {e}"))?; .map_err(|e| format!("seed agent: {e}"))?;
tc_db::repo::credits::add_lot(pool, workspace.id, 1250, "e2e-seed") cm_db::repo::credits::add_lot(pool, workspace.id, 1250, "e2e-seed")
.await .await
.map_err(|e| format!("seed credits: {e}"))?; .map_err(|e| format!("seed credits: {e}"))?;
tc_db::repo::skills::create( cm_db::repo::skills::create(
pool, pool,
None, None,
"Daily briefing", "Daily briefing",
"TeamClaw", "Clawmates",
"Summarize the day's priorities each morning.", "Summarize the day's priorities each morning.",
"Each morning, compile a short briefing of priorities and blockers.", "Each morning, compile a short briefing of priorities and blockers.",
) )
@@ -116,6 +116,6 @@ pub async fn seed(pool: &PgPool) -> Result<(), String> {
.await .await
.map_err(|e| format!("seed promo: {e}"))?; .map_err(|e| format!("seed promo: {e}"))?;
println!("teamclaw-server: e2e seed applied ({E2E_OWNER_EMAIL})"); println!("clawmates-server: e2e seed applied ({E2E_OWNER_EMAIL})");
Ok(()) Ok(())
} }
@@ -1,4 +1,4 @@
//! TeamClaw server: REST API and streaming gateway (later phases add the //! Clawmates server: REST API and streaming gateway (later phases add the
//! scheduler and safety worker) composed into one binary. //! scheduler and safety worker) composed into one binary.
mod e2e; mod e2e;
@@ -7,16 +7,16 @@ use std::path::{Path, PathBuf};
use std::process::ExitCode; use std::process::ExitCode;
use std::sync::Arc; use std::sync::Arc;
use tc_config::{AppConfig, LlmProviderKind}; use cm_config::{AppConfig, LlmProviderKind};
use tc_llm::{AnthropicProvider, LlmProvider, OpenAiCompatProvider, ScriptedProvider}; use cm_llm::{AnthropicProvider, LlmProvider, OpenAiCompatProvider, ScriptedProvider};
use tc_runtime::{Runtime, RuntimeConfig}; use cm_runtime::{Runtime, RuntimeConfig};
#[tokio::main] #[tokio::main]
async fn main() -> ExitCode { async fn main() -> ExitCode {
match run().await { match run().await {
Ok(()) => ExitCode::SUCCESS, Ok(()) => ExitCode::SUCCESS,
Err(message) => { Err(message) => {
eprintln!("teamclaw-server: {message}"); eprintln!("clawmates-server: {message}");
ExitCode::FAILURE ExitCode::FAILURE
} }
} }
@@ -32,10 +32,10 @@ fn build_provider(config: &AppConfig) -> Result<Arc<dyn LlmProvider>, String> {
Ok(Arc::new(AnthropicProvider::new(key))) Ok(Arc::new(AnthropicProvider::new(key)))
} }
LlmProviderKind::OpenAiCompat => { LlmProviderKind::OpenAiCompat => {
let base_url = config.llm.base_url.clone().expect("validated by tc-config"); let base_url = config.llm.base_url.clone().expect("validated by cm-config");
Ok(Arc::new(OpenAiCompatProvider::new( Ok(Arc::new(OpenAiCompatProvider::new(
base_url, base_url,
std::env::var("TEAMCLAW_LLM_API_KEY").ok(), std::env::var("CLAWMATES_LLM_API_KEY").ok(),
))) )))
} }
LlmProviderKind::Scripted => { LlmProviderKind::Scripted => {
@@ -43,7 +43,7 @@ fn build_provider(config: &AppConfig) -> Result<Arc<dyn LlmProvider>, String> {
.llm .llm
.scenario_path .scenario_path
.clone() .clone()
.expect("validated by tc-config"); .expect("validated by cm-config");
let provider = ScriptedProvider::from_path(Path::new(&path)) let provider = ScriptedProvider::from_path(Path::new(&path))
.map_err(|e| format!("scenario load failed: {e}"))?; .map_err(|e| format!("scenario load failed: {e}"))?;
Ok(Arc::new(provider)) Ok(Arc::new(provider))
@@ -52,17 +52,20 @@ fn build_provider(config: &AppConfig) -> Result<Arc<dyn LlmProvider>, String> {
} }
async fn run() -> Result<(), String> { async fn run() -> Result<(), String> {
let config_path = let config_path = PathBuf::from(
PathBuf::from(std::env::var("TEAMCLAW_CONFIG").unwrap_or_else(|_| "teamclaw.toml".into())); std::env::var("CLAWMATES_CONFIG").unwrap_or_else(|_| "clawmates.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 = let _telemetry = cm_telemetry::init(
tc_telemetry::init("teamclaw-server", config.telemetry.otlp_endpoint.as_deref()) "clawmates-server",
.map_err(|e| format!("telemetry: {e}"))?; 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 = cm_db::connect(&config.database.url, config.database.max_connections)
.await .await
.map_err(|e| format!("database connection failed: {e}"))?; .map_err(|e| format!("database connection failed: {e}"))?;
tc_db::MIGRATOR cm_db::MIGRATOR
.run(&pool) .run(&pool)
.await .await
.map_err(|e| format!("migrations failed: {e}"))?; .map_err(|e| format!("migrations failed: {e}"))?;
@@ -72,12 +75,12 @@ async fn run() -> Result<(), String> {
} }
let provider = build_provider(&config)?; let provider = build_provider(&config)?;
let blob: std::sync::Arc<dyn tc_files::BlobStore> = match config.storage.backend { let blob: std::sync::Arc<dyn cm_files::BlobStore> = match config.storage.backend {
tc_config::StorageBackend::Local => std::sync::Arc::new(tc_files::LocalBlobStore::new( cm_config::StorageBackend::Local => std::sync::Arc::new(cm_files::LocalBlobStore::new(
PathBuf::from(&config.storage.data_dir), PathBuf::from(&config.storage.data_dir),
)), )),
tc_config::StorageBackend::S3 => std::sync::Arc::new( cm_config::StorageBackend::S3 => std::sync::Arc::new(
tc_files::S3BlobStore::connect( cm_files::S3BlobStore::connect(
config.storage.s3_endpoint.as_deref().expect("validated"), config.storage.s3_endpoint.as_deref().expect("validated"),
config.storage.s3_bucket.as_deref().expect("validated"), config.storage.s3_bucket.as_deref().expect("validated"),
config.storage.s3_access_key.as_deref().unwrap_or_default(), config.storage.s3_access_key.as_deref().unwrap_or_default(),
@@ -89,11 +92,11 @@ async fn run() -> Result<(), String> {
// Environment tools need a container engine; absence is tolerated // Environment tools need a container engine; absence is tolerated
// (shell.exec reports it per-call) so the API still serves. // (shell.exec reports it per-call) so the API still serves.
let (sandboxes, browser) = if config.sandbox.enabled { let (sandboxes, browser) = if config.sandbox.enabled {
match tc_sandbox::DockerDriver::connect() { match cm_sandbox::DockerDriver::connect() {
Ok(driver) => { Ok(driver) => {
let driver: std::sync::Arc<dyn tc_sandbox::SandboxDriver> = let driver: std::sync::Arc<dyn cm_sandbox::SandboxDriver> =
std::sync::Arc::new(driver); std::sync::Arc::new(driver);
let agents = std::sync::Arc::new(tc_runtime::SandboxManager::new( let agents = std::sync::Arc::new(cm_runtime::SandboxManager::new(
driver.clone(), driver.clone(),
&config.sandbox.image, &config.sandbox.image,
)); ));
@@ -105,13 +108,13 @@ async fn run() -> Result<(), String> {
( (
Some(agents), Some(agents),
Some(std::sync::Arc::new( Some(std::sync::Arc::new(
tc_runtime::SandboxManager::new(driver, &config.sandbox.browser_image) cm_runtime::SandboxManager::new(driver, &config.sandbox.browser_image)
.with_egress(), .with_egress(),
)), )),
) )
} }
Err(error) => { Err(error) => {
eprintln!("teamclaw-server: sandbox engine unavailable: {error}"); eprintln!("clawmates-server: sandbox engine unavailable: {error}");
(None, None) (None, None)
} }
} }
@@ -135,28 +138,28 @@ async fn run() -> Result<(), String> {
// even if the deciding request's process died mid-flight. // even if the deciding request's process died mid-flight.
runtime.spawn_resume_sweeper(std::time::Duration::from_secs(2)); runtime.spawn_resume_sweeper(std::time::Duration::from_secs(2));
// Routine firings (§7.6). // Routine firings (§7.6).
tc_scheduler::Scheduler::new(pool.clone(), runtime.clone()) cm_scheduler::Scheduler::new(pool.clone(), runtime.clone())
.spawn(std::time::Duration::from_secs(5)); .spawn(std::time::Duration::from_secs(5));
// Hosted identity (Clerk / OIDC): pin the issuer and load its JWKS. // Hosted identity (Clerk / OIDC): pin the issuer and load its JWKS.
let auth_verifier = match config.auth.mode { let auth_verifier = match config.auth.mode {
tc_config::AuthMode::Clerk | tc_config::AuthMode::Oidc => { cm_config::AuthMode::Clerk | cm_config::AuthMode::Oidc => {
let issuer = config let issuer = config
.auth .auth
.issuer_url .issuer_url
.as_deref() .as_deref()
.expect("validated by config"); .expect("validated by config");
Some(std::sync::Arc::new( Some(std::sync::Arc::new(
tc_auth::JwtVerifier::discover(issuer) cm_auth::JwtVerifier::discover(issuer)
.await .await
.map_err(|e| format!("identity issuer: {e}"))?, .map_err(|e| format!("identity issuer: {e}"))?,
)) ))
} }
tc_config::AuthMode::Local => None, cm_config::AuthMode::Local => None,
}; };
let mut app = tc_api::router( let mut app = cm_api::router(
tc_api::AppState::new(pool, runtime) cm_api::AppState::new(pool, runtime)
.with_broker(PathBuf::from(&config.broker.socket_path)) .with_broker(PathBuf::from(&config.broker.socket_path))
.with_oauth(config.oauth.clone()) .with_oauth(config.oauth.clone())
.pipe_auth_verifier(auth_verifier), .pipe_auth_verifier(auth_verifier),
@@ -167,7 +170,7 @@ async fn run() -> Result<(), String> {
let listener = tokio::net::TcpListener::bind(config.listen_addr) let listener = tokio::net::TcpListener::bind(config.listen_addr)
.await .await
.map_err(|e| format!("bind {} failed: {e}", config.listen_addr))?; .map_err(|e| format!("bind {} failed: {e}", config.listen_addr))?;
println!("teamclaw-server listening on {}", config.listen_addr); println!("clawmates-server listening on {}", config.listen_addr);
axum::serve(listener, app) axum::serve(listener, app)
.await .await
.map_err(|e| format!("server error: {e}")) .map_err(|e| format!("server error: {e}"))
-28
View File
@@ -1,28 +0,0 @@
[package]
name = "teamclaw-server"
version = "0.1.0"
edition.workspace = true
rust-version.workspace = true
license.workspace = true
publish.workspace = true
[dependencies]
axum = "0.8"
serde_json = { workspace = true }
sqlx = { workspace = true }
tc-api = { path = "../../tc-api" }
tc-auth = { path = "../../tc-auth" }
tc-config = { path = "../../tc-config" }
tc-db = { path = "../../tc-db" }
tc-files = { path = "../../tc-files" }
tc-llm = { path = "../../tc-llm" }
tc-runtime = { path = "../../tc-runtime" }
tc-telemetry = { path = "../../tc-telemetry" }
tc-sandbox = { path = "../../tc-sandbox" }
tc-scheduler = { path = "../../tc-scheduler" }
tc-domain = { path = "../../tc-domain" }
time = { workspace = true }
tokio = { workspace = true }
[lints]
workspace = true
@@ -1,5 +1,5 @@
[package] [package]
name = "tc-api" name = "cm-api"
version = "0.1.0" version = "0.1.0"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
@@ -14,15 +14,15 @@ serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
sqlx = { workspace = true } sqlx = { workspace = true }
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
tc-auth = { path = "../tc-auth" } cm-auth = { path = "../cm-auth" }
tc-billing = { path = "../tc-billing" } cm-billing = { path = "../cm-billing" }
tc-config = { path = "../tc-config" } cm-config = { path = "../cm-config" }
tc-db = { path = "../tc-db" } cm-db = { path = "../cm-db" }
tc-domain = { path = "../tc-domain" } cm-domain = { path = "../cm-domain" }
tc-runtime = { path = "../tc-runtime" } cm-runtime = { path = "../cm-runtime" }
tc-safety = { path = "../tc-safety" } cm-safety = { path = "../cm-safety" }
tc-scheduler = { path = "../tc-scheduler" } cm-scheduler = { path = "../cm-scheduler" }
tc-secrets = { path = "../tc-secrets" } cm-secrets = { path = "../cm-secrets" }
thiserror = { workspace = true } thiserror = { workspace = true }
tower-http = { version = "0.6", features = ["trace"] } tower-http = { version = "0.6", features = ["trace"] }
time = { workspace = true } time = { workspace = true }
@@ -38,8 +38,8 @@ reqwest = { version = "0.12", default-features = false, features = [
"rustls-tls", "rustls-tls",
"stream", "stream",
] } ] }
tc-llm = { path = "../tc-llm" } cm-llm = { path = "../cm-llm" }
tc-testkit = { path = "../tc-testkit" } cm-testkit = { path = "../cm-testkit" }
hex = "0.4" hex = "0.4"
hmac = "0.12" hmac = "0.12"
base64 = "0.22" base64 = "0.22"
@@ -19,19 +19,19 @@ pub enum ApiError {
Internal, Internal,
} }
impl From<tc_db::DbError> for ApiError { impl From<cm_db::DbError> for ApiError {
fn from(err: tc_db::DbError) -> Self { fn from(err: cm_db::DbError) -> Self {
match err { match err {
tc_db::DbError::NotFound => ApiError::NotFound, cm_db::DbError::NotFound => ApiError::NotFound,
_ => ApiError::Internal, _ => ApiError::Internal,
} }
} }
} }
impl From<tc_auth::AuthError> for ApiError { impl From<cm_auth::AuthError> for ApiError {
fn from(err: tc_auth::AuthError) -> Self { fn from(err: cm_auth::AuthError) -> Self {
match err { match err {
tc_auth::AuthError::InvalidCredentials | tc_auth::AuthError::Unauthenticated => { cm_auth::AuthError::InvalidCredentials | cm_auth::AuthError::Unauthenticated => {
ApiError::Unauthorized ApiError::Unauthorized
} }
_ => ApiError::Internal, _ => ApiError::Internal,
@@ -1,7 +1,7 @@
use axum::extract::FromRequestParts; use axum::extract::FromRequestParts;
use axum::http::header::AUTHORIZATION; use axum::http::header::AUTHORIZATION;
use axum::http::request::Parts; use axum::http::request::Parts;
use tc_auth::AuthedUser; use cm_auth::AuthedUser;
use crate::{ApiError, AppState}; use crate::{ApiError, AppState};
@@ -1,4 +1,4 @@
//! REST API for TeamClaw (spec §13). One route resource per module. //! REST API for Clawmates (spec §13). One route resource per module.
mod error; mod error;
mod extract; mod extract;
@@ -6,9 +6,9 @@ mod routes;
use axum::routing::{delete, get, patch, post}; use axum::routing::{delete, get, patch, post};
use axum::Router; use axum::Router;
use cm_auth::AuthService;
use cm_runtime::Runtime;
use sqlx::PgPool; use sqlx::PgPool;
use tc_auth::AuthService;
use tc_runtime::Runtime;
pub use error::ApiError; pub use error::ApiError;
pub use extract::Authed; pub use extract::Authed;
@@ -20,7 +20,7 @@ pub struct AppState {
pub runtime: Runtime, pub runtime: Runtime,
/// Secret broker socket; connect flows refuse without it. /// Secret broker socket; connect flows refuse without it.
pub broker_socket: Option<std::path::PathBuf>, pub broker_socket: Option<std::path::PathBuf>,
pub oauth: tc_config::OAuthConfig, pub oauth: cm_config::OAuthConfig,
} }
impl AppState { impl AppState {
@@ -31,11 +31,11 @@ impl AppState {
auth, auth,
runtime, runtime,
broker_socket: None, broker_socket: None,
oauth: tc_config::OAuthConfig::default(), oauth: cm_config::OAuthConfig::default(),
} }
} }
pub fn with_oauth(mut self, oauth: tc_config::OAuthConfig) -> AppState { pub fn with_oauth(mut self, oauth: cm_config::OAuthConfig) -> AppState {
self.oauth = oauth; self.oauth = oauth;
self self
} }
@@ -43,7 +43,7 @@ impl AppState {
/// Optional form of [`AppState::with_auth_verifier`] for call chains. /// Optional form of [`AppState::with_auth_verifier`] for call chains.
pub fn pipe_auth_verifier( pub fn pipe_auth_verifier(
self, self,
verifier: Option<std::sync::Arc<tc_auth::JwtVerifier>>, verifier: Option<std::sync::Arc<cm_auth::JwtVerifier>>,
) -> AppState { ) -> AppState {
match verifier { match verifier {
Some(verifier) => self.with_auth_verifier(verifier), Some(verifier) => self.with_auth_verifier(verifier),
@@ -55,7 +55,7 @@ impl AppState {
/// extractor, alongside local sessions. /// extractor, alongside local sessions.
pub fn with_auth_verifier( pub fn with_auth_verifier(
mut self, mut self,
verifier: std::sync::Arc<tc_auth::JwtVerifier>, verifier: std::sync::Arc<cm_auth::JwtVerifier>,
) -> AppState { ) -> AppState {
self.auth = self.auth.with_verifier(verifier); self.auth = self.auth.with_verifier(verifier);
self self
@@ -1,7 +1,7 @@
use axum::extract::{Path, State}; use axum::extract::{Path, State};
use axum::Json; use axum::Json;
use cm_safety::{approvals, Approval, Decision, ResumeReady, SafetyError};
use serde_json::{json, Value}; use serde_json::{json, Value};
use tc_safety::{approvals, Approval, Decision, ResumeReady, SafetyError};
use uuid::Uuid; use uuid::Uuid;
use crate::{ApiError, AppState, Authed}; use crate::{ApiError, AppState, Authed};
@@ -19,7 +19,7 @@ impl From<SafetyError> for ApiError {
/// Loads an approval, hiding other workspaces' approvals entirely. /// Loads an approval, hiding other workspaces' approvals entirely.
async fn workspace_approval( async fn workspace_approval(
state: &AppState, state: &AppState,
user: &tc_auth::AuthedUser, user: &cm_auth::AuthedUser,
id: Uuid, id: Uuid,
) -> Result<Approval, ApiError> { ) -> Result<Approval, ApiError> {
let approval = approvals::get(&state.pool, id).await?; let approval = approvals::get(&state.pool, id).await?;
@@ -49,7 +49,7 @@ pub async fn get(
async fn decide( async fn decide(
state: AppState, state: AppState,
user: tc_auth::AuthedUser, user: cm_auth::AuthedUser,
id: Uuid, id: Uuid,
decision: Decision, decision: Decision,
) -> Result<Json<Value>, ApiError> { ) -> Result<Json<Value>, ApiError> {
@@ -1,10 +1,10 @@
use axum::extract::{Query, State}; use axum::extract::{Query, State};
use axum::http::StatusCode; use axum::http::StatusCode;
use axum::Json; use axum::Json;
use cm_db::repo::audit::Actor;
use cm_domain::AgentId;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::{json, Value}; use serde_json::{json, Value};
use tc_db::repo::audit::Actor;
use tc_domain::AgentId;
use uuid::Uuid; use uuid::Uuid;
use crate::routes::claws::workspace_agent; use crate::routes::claws::workspace_agent;
@@ -141,7 +141,7 @@ pub async fn directory(
let connections = match query.claw_id { let connections = match query.claw_id {
Some(claw_id) => { Some(claw_id) => {
let agent = workspace_agent(&state, &user, claw_id).await?; let agent = workspace_agent(&state, &user, claw_id).await?;
tc_db::repo::connections::list_for_agent(&state.pool, user.workspace_id, agent.id) cm_db::repo::connections::list_for_agent(&state.pool, user.workspace_id, agent.id)
.await? .await?
} }
None => Vec::new(), None => Vec::new(),
@@ -187,7 +187,7 @@ pub async fn connect(
} }
let agent = workspace_agent(&state, &user, body.claw_id).await?; let agent = workspace_agent(&state, &user, body.claw_id).await?;
let socket = state.broker_socket.as_ref().ok_or(ApiError::Internal)?; let socket = state.broker_socket.as_ref().ok_or(ApiError::Internal)?;
let mut broker = tc_secrets::BrokerClient::connect(socket) let mut broker = cm_secrets::BrokerClient::connect(socket)
.await .await
.map_err(|_| ApiError::Internal)?; .map_err(|_| ApiError::Internal)?;
let secret_ref = broker let secret_ref = broker
@@ -198,7 +198,7 @@ pub async fn connect(
) )
.await .await
.map_err(|_| ApiError::Internal)?; .map_err(|_| ApiError::Internal)?;
let connection = tc_db::repo::connections::insert( let connection = cm_db::repo::connections::insert(
&state.pool, &state.pool,
user.workspace_id, user.workspace_id,
Some(agent.id), Some(agent.id),
@@ -207,7 +207,7 @@ pub async fn connect(
secret_ref, secret_ref,
) )
.await?; .await?;
tc_db::repo::audit::append( cm_db::repo::audit::append(
&state.pool, &state.pool,
user.workspace_id, user.workspace_id,
Actor::User(user.user_id), Actor::User(user.user_id),
@@ -235,8 +235,8 @@ pub async fn disconnect(
Authed(user): Authed, Authed(user): Authed,
Json(body): Json<DisconnectRequest>, Json(body): Json<DisconnectRequest>,
) -> Result<StatusCode, ApiError> { ) -> Result<StatusCode, ApiError> {
tc_db::repo::connections::disconnect(&state.pool, body.connection_id).await?; cm_db::repo::connections::disconnect(&state.pool, body.connection_id).await?;
tc_db::repo::audit::append( cm_db::repo::audit::append(
&state.pool, &state.pool,
user.workspace_id, user.workspace_id,
Actor::User(user.user_id), Actor::User(user.user_id),
@@ -1,9 +1,9 @@
use axum::extract::State; use axum::extract::State;
use axum::Json; use axum::Json;
use cm_billing::BillingError;
use cm_db::repo::audit::Actor;
use serde::Deserialize; use serde::Deserialize;
use serde_json::{json, Value}; use serde_json::{json, Value};
use tc_billing::BillingError;
use tc_db::repo::audit::Actor;
use crate::{ApiError, AppState, Authed}; use crate::{ApiError, AppState, Authed};
@@ -18,13 +18,13 @@ pub async fn redeem(
Authed(user): Authed, Authed(user): Authed,
Json(body): Json<RedeemRequest>, Json(body): Json<RedeemRequest>,
) -> Result<Json<Value>, ApiError> { ) -> Result<Json<Value>, ApiError> {
let granted = tc_billing::redeem_promo(&state.pool, user.workspace_id, &body.code) let granted = cm_billing::redeem_promo(&state.pool, user.workspace_id, &body.code)
.await .await
.map_err(|e| match e { .map_err(|e| match e {
BillingError::PromoUnavailable => ApiError::Conflict, BillingError::PromoUnavailable => ApiError::Conflict,
BillingError::Db(_) => ApiError::Internal, BillingError::Db(_) => ApiError::Internal,
})?; })?;
tc_db::repo::audit::append( cm_db::repo::audit::append(
&state.pool, &state.pool,
user.workspace_id, user.workspace_id,
Actor::User(user.user_id), Actor::User(user.user_id),
@@ -43,7 +43,7 @@ pub async fn usage(
Authed(user): Authed, Authed(user): Authed,
) -> Result<Json<Value>, ApiError> { ) -> Result<Json<Value>, ApiError> {
let (tokens_in, tokens_out, credits) = let (tokens_in, tokens_out, credits) =
tc_billing::usage_last_7_days(&state.pool, user.workspace_id) cm_billing::usage_last_7_days(&state.pool, user.workspace_id)
.await .await
.map_err(|_| ApiError::Internal)?; .map_err(|_| ApiError::Internal)?;
Ok(Json(json!({ Ok(Json(json!({
@@ -4,7 +4,7 @@
use axum::extract::{Path, State}; use axum::extract::{Path, State};
use axum::http::{header, StatusCode}; use axum::http::{header, StatusCode};
use axum::response::IntoResponse; use axum::response::IntoResponse;
use tc_domain::AgentId; use cm_domain::AgentId;
use crate::routes::claws::workspace_agent; use crate::routes::claws::workspace_agent;
use crate::{ApiError, AppState, Authed}; use crate::{ApiError, AppState, Authed};
@@ -1,8 +1,8 @@
use axum::extract::{Query, State}; use axum::extract::{Query, State};
use axum::Json; use axum::Json;
use cm_db::repo::threads::{Thread, ThreadMessage};
use cm_domain::AgentId;
use serde::Deserialize; use serde::Deserialize;
use tc_db::repo::threads::{Thread, ThreadMessage};
use tc_domain::AgentId;
use uuid::Uuid; use uuid::Uuid;
use crate::routes::claws::workspace_agent; use crate::routes::claws::workspace_agent;
@@ -22,7 +22,7 @@ pub async fn threads(
) -> Result<Json<Vec<Thread>>, ApiError> { ) -> Result<Json<Vec<Thread>>, ApiError> {
let agent = workspace_agent(&state, &user, query.claw_id).await?; let agent = workspace_agent(&state, &user, query.claw_id).await?;
Ok(Json( Ok(Json(
tc_db::repo::threads::list_for_agent(&state.pool, agent.id).await?, cm_db::repo::threads::list_for_agent(&state.pool, agent.id).await?,
)) ))
} }
@@ -41,10 +41,10 @@ pub async fn messages(
Query(query): Query<MessagesQuery>, Query(query): Query<MessagesQuery>,
) -> Result<Json<Vec<ThreadMessage>>, ApiError> { ) -> Result<Json<Vec<ThreadMessage>>, ApiError> {
let agent = workspace_agent(&state, &user, query.claw_id).await?; let agent = workspace_agent(&state, &user, query.claw_id).await?;
if !tc_db::repo::threads::is_participant(&state.pool, query.thread_id, agent.id).await? { if !cm_db::repo::threads::is_participant(&state.pool, query.thread_id, agent.id).await? {
return Err(ApiError::NotFound); return Err(ApiError::NotFound);
} }
Ok(Json( Ok(Json(
tc_db::repo::threads::messages(&state.pool, query.thread_id).await?, cm_db::repo::threads::messages(&state.pool, query.thread_id).await?,
)) ))
} }
@@ -1,10 +1,10 @@
use axum::extract::{Path, Query, State}; use axum::extract::{Path, Query, State};
use axum::http::StatusCode; use axum::http::StatusCode;
use axum::Json; use axum::Json;
use cm_db::repo::audit::Actor;
use cm_domain::{AccessPolicy, Agent, AgentId, AgentStatus};
use serde::Deserialize; use serde::Deserialize;
use serde_json::{json, Value}; use serde_json::{json, Value};
use tc_db::repo::audit::Actor;
use tc_domain::{AccessPolicy, Agent, AgentId, AgentStatus};
use crate::{ApiError, AppState, Authed}; use crate::{ApiError, AppState, Authed};
@@ -12,10 +12,10 @@ use crate::{ApiError, AppState, Authed};
/// are indistinguishable from non-existent ones. /// are indistinguishable from non-existent ones.
pub(crate) async fn workspace_agent( pub(crate) async fn workspace_agent(
state: &AppState, state: &AppState,
user: &tc_auth::AuthedUser, user: &cm_auth::AuthedUser,
agent_id: AgentId, agent_id: AgentId,
) -> Result<Agent, ApiError> { ) -> Result<Agent, ApiError> {
let agent = tc_db::repo::agents::get(&state.pool, agent_id).await?; let agent = cm_db::repo::agents::get(&state.pool, agent_id).await?;
if agent.workspace_id != user.workspace_id { if agent.workspace_id != user.workspace_id {
return Err(ApiError::NotFound); return Err(ApiError::NotFound);
} }
@@ -54,8 +54,8 @@ pub async fn create(
managed_by: user.user_id, managed_by: user.user_id,
status: AgentStatus::Online, status: AgentStatus::Online,
}; };
tc_db::repo::agents::insert(&state.pool, &agent, &AccessPolicy::default()).await?; cm_db::repo::agents::insert(&state.pool, &agent, &AccessPolicy::default()).await?;
tc_db::repo::audit::append( cm_db::repo::audit::append(
&state.pool, &state.pool,
user.workspace_id, user.workspace_id,
Actor::User(user.user_id), Actor::User(user.user_id),
@@ -86,7 +86,7 @@ pub async fn patch(
Json(body): Json<PatchClawRequest>, Json(body): Json<PatchClawRequest>,
) -> Result<Json<Agent>, ApiError> { ) -> Result<Json<Agent>, ApiError> {
workspace_agent(&state, &user, id).await?; workspace_agent(&state, &user, id).await?;
let updated = tc_db::repo::agents::update_profile( let updated = cm_db::repo::agents::update_profile(
&state.pool, &state.pool,
id, id,
body.name.as_deref(), body.name.as_deref(),
@@ -97,7 +97,7 @@ pub async fn patch(
body.wallpaper.as_deref(), body.wallpaper.as_deref(),
) )
.await?; .await?;
tc_db::repo::audit::append( cm_db::repo::audit::append(
&state.pool, &state.pool,
user.workspace_id, user.workspace_id,
Actor::User(user.user_id), Actor::User(user.user_id),
@@ -121,8 +121,8 @@ pub async fn delete(
if !user.role.is_owner() && agent.managed_by != user.user_id { if !user.role.is_owner() && agent.managed_by != user.user_id {
return Err(ApiError::Forbidden); return Err(ApiError::Forbidden);
} }
tc_db::repo::agents::soft_delete(&state.pool, id).await?; cm_db::repo::agents::soft_delete(&state.pool, id).await?;
tc_db::repo::audit::append( cm_db::repo::audit::append(
&state.pool, &state.pool,
user.workspace_id, user.workspace_id,
Actor::User(user.user_id), Actor::User(user.user_id),
@@ -143,8 +143,8 @@ pub async fn set_access(
Json(policy): Json<AccessPolicy>, Json(policy): Json<AccessPolicy>,
) -> Result<Json<AccessPolicy>, ApiError> { ) -> Result<Json<AccessPolicy>, ApiError> {
workspace_agent(&state, &user, id).await?; workspace_agent(&state, &user, id).await?;
tc_db::repo::agents::set_access_policy(&state.pool, id, &policy).await?; cm_db::repo::agents::set_access_policy(&state.pool, id, &policy).await?;
tc_db::repo::audit::append( cm_db::repo::audit::append(
&state.pool, &state.pool,
user.workspace_id, user.workspace_id,
Actor::User(user.user_id), Actor::User(user.user_id),
@@ -170,8 +170,8 @@ pub async fn settings_full(
Query(query): Query<SettingsQuery>, Query(query): Query<SettingsQuery>,
) -> Result<Json<Value>, ApiError> { ) -> Result<Json<Value>, ApiError> {
let agent = workspace_agent(&state, &user, query.claw_id).await?; let agent = workspace_agent(&state, &user, query.claw_id).await?;
let policy = tc_db::repo::agents::access_policy(&state.pool, agent.id).await?; let policy = cm_db::repo::agents::access_policy(&state.pool, agent.id).await?;
let manager = tc_db::repo::users::get(&state.pool, agent.managed_by).await?; let manager = cm_db::repo::users::get(&state.pool, agent.managed_by).await?;
Ok(Json(json!({ Ok(Json(json!({
"agent": agent, "agent": agent,
"access_policy": policy, "access_policy": policy,
@@ -1,7 +1,7 @@
use axum::extract::{Query, State}; use axum::extract::{Query, State};
use axum::Json; use axum::Json;
use cm_domain::{AgentId, FileDrive, FileNode};
use serde::Deserialize; use serde::Deserialize;
use tc_domain::{AgentId, FileDrive, FileNode};
use crate::routes::claws::workspace_agent; use crate::routes::claws::workspace_agent;
use crate::{ApiError, AppState, Authed}; use crate::{ApiError, AppState, Authed};
@@ -30,7 +30,7 @@ pub async fn openclaw_files(
if !drive.is_agent_scoped() { if !drive.is_agent_scoped() {
return Err(ApiError::NotFound); // shared drive has its own route return Err(ApiError::NotFound); // shared drive has its own route
} }
let nodes = tc_db::repo::files::list(&state.pool, user.workspace_id, drive, agent.id).await?; let nodes = cm_db::repo::files::list(&state.pool, user.workspace_id, drive, agent.id).await?;
Ok(Json(nodes)) Ok(Json(nodes))
} }
@@ -48,7 +48,7 @@ pub async fn shared_files(
) -> Result<Json<Vec<FileNode>>, ApiError> { ) -> Result<Json<Vec<FileNode>>, ApiError> {
let agent = workspace_agent(&state, &user, query.claw_id).await?; let agent = workspace_agent(&state, &user, query.claw_id).await?;
let nodes = let nodes =
tc_db::repo::files::list(&state.pool, user.workspace_id, FileDrive::Shared, agent.id) cm_db::repo::files::list(&state.pool, user.workspace_id, FileDrive::Shared, agent.id)
.await?; .await?;
Ok(Json(nodes)) Ok(Json(nodes))
} }
@@ -4,10 +4,10 @@ use std::time::Duration;
use axum::extract::{Query, State}; use axum::extract::{Query, State};
use axum::response::sse::{Event, KeepAlive, Sse}; use axum::response::sse::{Event, KeepAlive, Sse};
use axum::Json; use axum::Json;
use cm_domain::AgentId;
use cm_runtime::{RunEventBody, RunEventEnvelope};
use futures::stream::BoxStream; use futures::stream::BoxStream;
use serde::Deserialize; use serde::Deserialize;
use tc_domain::AgentId;
use tc_runtime::{RunEventBody, RunEventEnvelope};
use tokio::sync::broadcast; use tokio::sync::broadcast;
use crate::routes::claws::workspace_agent; use crate::routes::claws::workspace_agent;
@@ -92,21 +92,21 @@ pub async fn gateway(
.send_message(session.id, &text) .send_message(session.id, &text)
.await .await
.map_err(|e| match e { .map_err(|e| match e {
tc_runtime::RuntimeError::Db(tc_db::DbError::NotFound) => ApiError::NotFound, cm_runtime::RuntimeError::Db(cm_db::DbError::NotFound) => ApiError::NotFound,
_ => ApiError::Internal, _ => ApiError::Internal,
})?; })?;
live_stream(started.events, 0) live_stream(started.events, 0)
} }
None => { None => {
let resume_from = body.resume_from.unwrap_or(0); let resume_from = body.resume_from.unwrap_or(0);
let run = tc_db::repo::runs::latest_for_session(&state.pool, session.id) let run = cm_db::repo::runs::latest_for_session(&state.pool, session.id)
.await? .await?
.ok_or(ApiError::NotFound)?; .ok_or(ApiError::NotFound)?;
// Subscribe before reading the journal so no event falls in the // Subscribe before reading the journal so no event falls in the
// gap; the live tail then skips anything the replay covered. // gap; the live tail then skips anything the replay covered.
let live = state.runtime.subscribe(run.id).await; let live = state.runtime.subscribe(run.id).await;
let journal = let journal =
tc_db::repo::run_events::list_after(&state.pool, run.id, resume_from).await?; cm_db::repo::run_events::list_after(&state.pool, run.id, resume_from).await?;
let last_replayed = journal.last().map(|e| e.seq).unwrap_or(resume_from); let last_replayed = journal.last().map(|e| e.seq).unwrap_or(resume_from);
let replay_done = journal let replay_done = journal
.last() .last()
@@ -1,6 +1,6 @@
use axum::extract::State; use axum::extract::State;
use axum::Json; use axum::Json;
use tc_domain::User; use cm_domain::User;
use crate::{ApiError, AppState, Authed}; use crate::{ApiError, AppState, Authed};
@@ -8,6 +8,6 @@ pub async fn me(
State(state): State<AppState>, State(state): State<AppState>,
Authed(user): Authed, Authed(user): Authed,
) -> Result<Json<User>, ApiError> { ) -> Result<Json<User>, ApiError> {
let me = tc_db::repo::users::get(&state.pool, user.user_id).await?; let me = cm_db::repo::users::get(&state.pool, user.user_id).await?;
Ok(Json(me)) Ok(Json(me))
} }
@@ -9,10 +9,10 @@
use axum::extract::{Query, State}; use axum::extract::{Query, State};
use axum::response::Redirect; use axum::response::Redirect;
use axum::Json; use axum::Json;
use cm_db::repo::audit::Actor;
use cm_domain::{AgentId, UserId, WorkspaceId};
use serde::Deserialize; use serde::Deserialize;
use serde_json::{json, Value}; use serde_json::{json, Value};
use tc_db::repo::audit::Actor;
use tc_domain::{AgentId, UserId, WorkspaceId};
use uuid::Uuid; use uuid::Uuid;
use crate::routes::claws::workspace_agent; use crate::routes::claws::workspace_agent;
@@ -166,7 +166,7 @@ pub async fn callback(
// The token goes straight to the broker; only the ref persists. // The token goes straight to the broker; only the ref persists.
let socket = state.broker_socket.as_ref().ok_or(ApiError::Internal)?; let socket = state.broker_socket.as_ref().ok_or(ApiError::Internal)?;
let mut broker = tc_secrets::BrokerClient::connect(socket) let mut broker = cm_secrets::BrokerClient::connect(socket)
.await .await
.map_err(|_| ApiError::Internal)?; .map_err(|_| ApiError::Internal)?;
let workspace_id = WorkspaceId::from(pending.workspace_id); let workspace_id = WorkspaceId::from(pending.workspace_id);
@@ -178,7 +178,7 @@ pub async fn callback(
) )
.await .await
.map_err(|_| ApiError::Internal)?; .map_err(|_| ApiError::Internal)?;
let connection = tc_db::repo::connections::insert( let connection = cm_db::repo::connections::insert(
&state.pool, &state.pool,
workspace_id, workspace_id,
Some(AgentId::from(pending.agent_id)), Some(AgentId::from(pending.agent_id)),
@@ -187,7 +187,7 @@ pub async fn callback(
secret_ref, secret_ref,
) )
.await?; .await?;
tc_db::repo::audit::append( cm_db::repo::audit::append(
&state.pool, &state.pool,
workspace_id, workspace_id,
Actor::User(UserId::from(pending.user_id)), Actor::User(UserId::from(pending.user_id)),
@@ -1,10 +1,10 @@
use axum::extract::{Query, State}; use axum::extract::{Query, State};
use axum::http::StatusCode; use axum::http::StatusCode;
use axum::Json; use axum::Json;
use cm_db::repo::routines::Routine;
use cm_domain::AgentId;
use serde::Deserialize; use serde::Deserialize;
use serde_json::json; use serde_json::json;
use tc_db::repo::routines::Routine;
use tc_domain::AgentId;
use crate::routes::claws::workspace_agent; use crate::routes::claws::workspace_agent;
use crate::{ApiError, AppState, Authed}; use crate::{ApiError, AppState, Authed};
@@ -23,7 +23,7 @@ pub async fn list(
) -> Result<Json<Vec<Routine>>, ApiError> { ) -> Result<Json<Vec<Routine>>, ApiError> {
let agent = workspace_agent(&state, &user, query.claw_id).await?; let agent = workspace_agent(&state, &user, query.claw_id).await?;
Ok(Json( Ok(Json(
tc_db::repo::routines::list_by_agent(&state.pool, agent.id).await?, cm_db::repo::routines::list_by_agent(&state.pool, agent.id).await?,
)) ))
} }
@@ -43,9 +43,9 @@ pub async fn create(
Json(body): Json<CreateRoutineRequest>, Json(body): Json<CreateRoutineRequest>,
) -> Result<(StatusCode, Json<Routine>), ApiError> { ) -> Result<(StatusCode, Json<Routine>), ApiError> {
let agent = workspace_agent(&state, &user, body.claw_id).await?; let agent = workspace_agent(&state, &user, body.claw_id).await?;
let next = tc_scheduler::next_occurrence(&body.cron, time::OffsetDateTime::now_utc()) let next = cm_scheduler::next_occurrence(&body.cron, time::OffsetDateTime::now_utc())
.map_err(|_| ApiError::Conflict)?; .map_err(|_| ApiError::Conflict)?;
let routine = tc_db::repo::routines::create( let routine = cm_db::repo::routines::create(
&state.pool, &state.pool,
agent.id, agent.id,
&body.name, &body.name,
@@ -3,9 +3,9 @@ use std::str::FromStr;
use axum::extract::{Query, State}; use axum::extract::{Query, State};
use axum::http::StatusCode; use axum::http::StatusCode;
use axum::Json; use axum::Json;
use cm_domain::{AgentId, MessageId, MessageRole, MessageWithSteps, Session, SessionKey};
use serde::Deserialize; use serde::Deserialize;
use serde_json::{json, Value}; use serde_json::{json, Value};
use tc_domain::{AgentId, MessageId, MessageRole, MessageWithSteps, Session, SessionKey};
use uuid::Uuid; use uuid::Uuid;
use crate::routes::claws::workspace_agent; use crate::routes::claws::workspace_agent;
@@ -33,11 +33,11 @@ fn with_key(session: Session, last_message: Option<MessageId>) -> Value {
/// scoping. Used by history and the gateway. /// scoping. Used by history and the gateway.
pub(crate) async fn scoped_session( pub(crate) async fn scoped_session(
state: &AppState, state: &AppState,
user: &tc_auth::AuthedUser, user: &cm_auth::AuthedUser,
raw_key: &str, raw_key: &str,
) -> Result<Session, ApiError> { ) -> Result<Session, ApiError> {
let key = SessionKey::from_str(raw_key).map_err(|_| ApiError::NotFound)?; let key = SessionKey::from_str(raw_key).map_err(|_| ApiError::NotFound)?;
let session = tc_db::repo::sessions::get(&state.pool, key.session_id).await?; let session = cm_db::repo::sessions::get(&state.pool, key.session_id).await?;
if session.agent_id != key.agent_id { if session.agent_id != key.agent_id {
return Err(ApiError::NotFound); return Err(ApiError::NotFound);
} }
@@ -58,7 +58,7 @@ pub async fn list(
Query(query): Query<ListQuery>, Query(query): Query<ListQuery>,
) -> Result<Json<Vec<Value>>, ApiError> { ) -> Result<Json<Vec<Value>>, ApiError> {
workspace_agent(&state, &user, query.claw_id).await?; workspace_agent(&state, &user, query.claw_id).await?;
let sessions = tc_db::repo::sessions::list_by_agent(&state.pool, query.claw_id).await?; let sessions = cm_db::repo::sessions::list_by_agent(&state.pool, query.claw_id).await?;
Ok(Json( Ok(Json(
sessions.into_iter().map(|s| with_key(s, None)).collect(), sessions.into_iter().map(|s| with_key(s, None)).collect(),
)) ))
@@ -80,7 +80,7 @@ pub async fn create(
) -> Result<(StatusCode, Json<Value>), ApiError> { ) -> Result<(StatusCode, Json<Value>), ApiError> {
let agent = workspace_agent(&state, &user, body.claw_id).await?; let agent = workspace_agent(&state, &user, body.claw_id).await?;
let session = let session =
tc_db::repo::sessions::create(&state.pool, agent.id, agent.workspace_id, &body.title) cm_db::repo::sessions::create(&state.pool, agent.id, agent.workspace_id, &body.title)
.await?; .await?;
Ok((StatusCode::CREATED, Json(with_key(session, None)))) Ok((StatusCode::CREATED, Json(with_key(session, None))))
} }
@@ -101,7 +101,7 @@ pub async fn history(
Query(query): Query<HistoryQuery>, Query(query): Query<HistoryQuery>,
) -> Result<Json<Vec<MessageWithSteps>>, ApiError> { ) -> Result<Json<Vec<MessageWithSteps>>, ApiError> {
let session = scoped_session(&state, &user, &query.session_key).await?; let session = scoped_session(&state, &user, &query.session_key).await?;
let mut history = tc_db::repo::messages::history(&state.pool, session.id).await?; let mut history = cm_db::repo::messages::history(&state.pool, session.id).await?;
if !query.tools { if !query.tools {
for entry in &mut history { for entry in &mut history {
entry.steps.clear(); entry.steps.clear();
@@ -1,9 +1,9 @@
use axum::extract::{Query, State}; use axum::extract::{Query, State};
use axum::http::StatusCode; use axum::http::StatusCode;
use axum::Json; use axum::Json;
use cm_db::repo::skills::Skill;
use cm_domain::AgentId;
use serde::Deserialize; use serde::Deserialize;
use tc_db::repo::skills::Skill;
use tc_domain::AgentId;
use uuid::Uuid; use uuid::Uuid;
use crate::routes::claws::workspace_agent; use crate::routes::claws::workspace_agent;
@@ -26,11 +26,11 @@ pub async fn list(
Some(claw_id) => { Some(claw_id) => {
let agent = workspace_agent(&state, &user, claw_id).await?; let agent = workspace_agent(&state, &user, claw_id).await?;
Ok(Json( Ok(Json(
tc_db::repo::skills::installed(&state.pool, agent.id).await?, cm_db::repo::skills::installed(&state.pool, agent.id).await?,
)) ))
} }
None => Ok(Json( None => Ok(Json(
tc_db::repo::skills::library(&state.pool, user.workspace_id).await?, cm_db::repo::skills::library(&state.pool, user.workspace_id).await?,
)), )),
} }
} }
@@ -50,7 +50,7 @@ pub async fn install(
Json(body): Json<InstallRequest>, Json(body): Json<InstallRequest>,
) -> Result<StatusCode, ApiError> { ) -> Result<StatusCode, ApiError> {
let agent = workspace_agent(&state, &user, body.claw_id).await?; let agent = workspace_agent(&state, &user, body.claw_id).await?;
tc_db::repo::skills::install(&state.pool, agent.id, body.skill_id, user.user_id).await?; cm_db::repo::skills::install(&state.pool, agent.id, body.skill_id, user.user_id).await?;
Ok(StatusCode::NO_CONTENT) Ok(StatusCode::NO_CONTENT)
} }
@@ -61,6 +61,6 @@ pub async fn uninstall(
Json(body): Json<InstallRequest>, Json(body): Json<InstallRequest>,
) -> Result<StatusCode, ApiError> { ) -> Result<StatusCode, ApiError> {
let agent = workspace_agent(&state, &user, body.claw_id).await?; let agent = workspace_agent(&state, &user, body.claw_id).await?;
tc_db::repo::skills::uninstall(&state.pool, agent.id, body.skill_id).await?; cm_db::repo::skills::uninstall(&state.pool, agent.id, body.skill_id).await?;
Ok(StatusCode::NO_CONTENT) Ok(StatusCode::NO_CONTENT)
} }
@@ -9,8 +9,8 @@ use axum::body::Bytes;
use axum::extract::State; use axum::extract::State;
use axum::http::{HeaderMap, StatusCode}; use axum::http::{HeaderMap, StatusCode};
use axum::Json; use axum::Json;
use cm_domain::AgentId;
use serde_json::{json, Value}; use serde_json::{json, Value};
use tc_domain::AgentId;
use crate::AppState; use crate::AppState;
@@ -22,7 +22,7 @@ async fn verified_connection(
timestamp: &str, timestamp: &str,
body: &str, body: &str,
signature: &str, signature: &str,
) -> Option<tc_db::repo::connections::AppConnection> { ) -> Option<cm_db::repo::connections::AppConnection> {
let socket = state.broker_socket.as_ref()?; let socket = state.broker_socket.as_ref()?;
let connections = sqlx::query!( let connections = sqlx::query!(
r#"SELECT id, workspace_id, agent_id, provider, auth_type, status, secret_ref r#"SELECT id, workspace_id, agent_id, provider, auth_type, status, secret_ref
@@ -35,7 +35,7 @@ async fn verified_connection(
let Some(secret_ref) = row.secret_ref else { let Some(secret_ref) = row.secret_ref else {
continue; continue;
}; };
let Ok(mut broker) = tc_secrets::BrokerClient::connect(socket).await else { let Ok(mut broker) = cm_secrets::BrokerClient::connect(socket).await else {
return None; return None;
}; };
if broker if broker
@@ -43,7 +43,7 @@ async fn verified_connection(
.await .await
.unwrap_or(false) .unwrap_or(false)
{ {
return Some(tc_db::repo::connections::AppConnection { return Some(cm_db::repo::connections::AppConnection {
id: row.id, id: row.id,
workspace_id: row.workspace_id, workspace_id: row.workspace_id,
agent_id: row.agent_id, agent_id: row.agent_id,
@@ -91,12 +91,12 @@ pub async fn events(
return Ok(Json(json!({ "ok": true }))); return Ok(Json(json!({ "ok": true })));
}; };
let agent_id = AgentId::from(agent_uuid); let agent_id = AgentId::from(agent_uuid);
let Ok(agent) = tc_db::repo::agents::get(&state.pool, agent_id).await else { let Ok(agent) = cm_db::repo::agents::get(&state.pool, agent_id).await else {
return Ok(Json(json!({ "ok": true }))); return Ok(Json(json!({ "ok": true })));
}; };
// One recognizable session per agent for Slack traffic. // One recognizable session per agent for Slack traffic.
let session = match tc_db::repo::sessions::list_by_agent(&state.pool, agent_id) let session = match cm_db::repo::sessions::list_by_agent(&state.pool, agent_id)
.await .await
.ok() .ok()
.and_then(|sessions| { .and_then(|sessions| {
@@ -105,7 +105,7 @@ pub async fn events(
.find(|s| s.title == SLACK_SESSION_TITLE) .find(|s| s.title == SLACK_SESSION_TITLE)
}) { }) {
Some(existing) => existing, Some(existing) => existing,
None => tc_db::repo::sessions::create( None => cm_db::repo::sessions::create(
&state.pool, &state.pool,
agent_id, agent_id,
agent.workspace_id, agent.workspace_id,
@@ -1,7 +1,7 @@
use axum::extract::State; use axum::extract::State;
use axum::Json; use axum::Json;
use cm_domain::{Agent, User};
use serde_json::{json, Value}; use serde_json::{json, Value};
use tc_domain::{Agent, User};
use crate::{ApiError, AppState, Authed}; use crate::{ApiError, AppState, Authed};
@@ -10,7 +10,7 @@ pub async fn members(
State(state): State<AppState>, State(state): State<AppState>,
Authed(user): Authed, Authed(user): Authed,
) -> Result<Json<Vec<User>>, ApiError> { ) -> Result<Json<Vec<User>>, ApiError> {
let members = tc_db::repo::users::list_by_workspace(&state.pool, user.workspace_id).await?; let members = cm_db::repo::users::list_by_workspace(&state.pool, user.workspace_id).await?;
Ok(Json(members)) Ok(Json(members))
} }
@@ -19,7 +19,7 @@ pub async fn claws(
State(state): State<AppState>, State(state): State<AppState>,
Authed(user): Authed, Authed(user): Authed,
) -> Result<Json<Vec<Agent>>, ApiError> { ) -> Result<Json<Vec<Agent>>, ApiError> {
let roster = tc_db::repo::agents::roster(&state.pool, user.workspace_id).await?; let roster = cm_db::repo::agents::roster(&state.pool, user.workspace_id).await?;
Ok(Json(roster)) Ok(Json(roster))
} }
@@ -28,7 +28,7 @@ pub async fn credits(
State(state): State<AppState>, State(state): State<AppState>,
Authed(user): Authed, Authed(user): Authed,
) -> Result<Json<Value>, ApiError> { ) -> Result<Json<Value>, ApiError> {
let available = tc_db::repo::credits::balance(&state.pool, user.workspace_id).await?; let available = cm_db::repo::credits::balance(&state.pool, user.workspace_id).await?;
Ok(Json(json!({ "available": available }))) Ok(Json(json!({ "available": available })))
} }
@@ -3,14 +3,14 @@
use std::sync::Arc; use std::sync::Arc;
use cm_api::AppState;
use cm_auth::AuthService;
use cm_domain::{Role, User, UserId, Workspace, WorkspaceId};
use cm_llm::ScriptedProvider;
use cm_runtime::{Runtime, RuntimeConfig};
use eventsource_stream::Eventsource; use eventsource_stream::Eventsource;
use futures::StreamExt; use futures::StreamExt;
use serde_json::{json, Value}; use serde_json::{json, Value};
use tc_api::AppState;
use tc_auth::AuthService;
use tc_domain::{Role, User, UserId, Workspace, WorkspaceId};
use tc_llm::ScriptedProvider;
use tc_runtime::{Runtime, RuntimeConfig};
const SCENARIOS: &str = r#" const SCENARIOS: &str = r#"
[[scenario]] [[scenario]]
@@ -39,7 +39,7 @@ async fn serve(pool: sqlx::PgPool) -> TestServer {
Arc::new(ScriptedProvider::from_toml(SCENARIOS).unwrap()), Arc::new(ScriptedProvider::from_toml(SCENARIOS).unwrap()),
RuntimeConfig::basic("scripted", 1024), RuntimeConfig::basic("scripted", 1024),
); );
let app = tc_api::router(AppState::new(pool, runtime)); let app = cm_api::router(AppState::new(pool, runtime));
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap(); let addr = listener.local_addr().unwrap();
tokio::spawn(async move { tokio::spawn(async move {
@@ -57,7 +57,7 @@ async fn seed_and_login(pool: &sqlx::PgPool, server: &TestServer) -> (String, St
name: "Acme".into(), name: "Acme".into(),
plan: "team".into(), plan: "team".into(),
}; };
tc_db::repo::workspaces::insert(pool, &ws).await.unwrap(); cm_db::repo::workspaces::insert(pool, &ws).await.unwrap();
let owner = User { let owner = User {
id: UserId::new(), id: UserId::new(),
workspace_id: ws.id, workspace_id: ws.id,
@@ -66,7 +66,7 @@ async fn seed_and_login(pool: &sqlx::PgPool, server: &TestServer) -> (String, St
display_name: "Owner".into(), display_name: "Owner".into(),
created_at: time::OffsetDateTime::UNIX_EPOCH, created_at: time::OffsetDateTime::UNIX_EPOCH,
}; };
tc_db::repo::users::insert(pool, &owner).await.unwrap(); cm_db::repo::users::insert(pool, &owner).await.unwrap();
AuthService::new(pool.clone()) AuthService::new(pool.clone())
.set_password(owner.id, "pw") .set_password(owner.id, "pw")
.await .await
@@ -155,7 +155,7 @@ async fn suspend_gated_run(
#[tokio::test] #[tokio::test]
async fn full_chain_over_http_executes_only_after_approval() { async fn full_chain_over_http_executes_only_after_approval() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let server = serve(pool.clone()).await; let server = serve(pool.clone()).await;
let (token, claw_id) = seed_and_login(&pool, &server).await; let (token, claw_id) = seed_and_login(&pool, &server).await;
let (session_key, approval_id, last_seq) = suspend_gated_run(&server, &token, &claw_id).await; let (session_key, approval_id, last_seq) = suspend_gated_run(&server, &token, &claw_id).await;
@@ -251,7 +251,7 @@ async fn full_chain_over_http_executes_only_after_approval() {
#[tokio::test] #[tokio::test]
async fn reject_over_http_executes_nothing() { async fn reject_over_http_executes_nothing() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let server = serve(pool.clone()).await; let server = serve(pool.clone()).await;
let (token, claw_id) = seed_and_login(&pool, &server).await; let (token, claw_id) = seed_and_login(&pool, &server).await;
let (session_key, approval_id, last_seq) = suspend_gated_run(&server, &token, &claw_id).await; let (session_key, approval_id, last_seq) = suspend_gated_run(&server, &token, &claw_id).await;
@@ -290,7 +290,7 @@ async fn reject_over_http_executes_nothing() {
#[tokio::test] #[tokio::test]
async fn approvals_are_tenant_isolated() { async fn approvals_are_tenant_isolated() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let server = serve(pool.clone()).await; let server = serve(pool.clone()).await;
let (token, claw_id) = seed_and_login(&pool, &server).await; let (token, claw_id) = seed_and_login(&pool, &server).await;
let (other_token, _) = seed_and_login(&pool, &server).await; let (other_token, _) = seed_and_login(&pool, &server).await;
@@ -1,23 +1,23 @@
use cm_api::AppState;
use cm_auth::AuthService;
use cm_domain::{Role, User, UserId, Workspace, WorkspaceId};
use serde_json::{json, Value}; use serde_json::{json, Value};
use tc_api::AppState;
use tc_auth::AuthService;
use tc_domain::{Role, User, UserId, Workspace, WorkspaceId};
struct TestServer { struct TestServer {
base: String, base: String,
client: reqwest::Client, client: reqwest::Client,
} }
fn test_runtime(pool: sqlx::PgPool) -> tc_runtime::Runtime { fn test_runtime(pool: sqlx::PgPool) -> cm_runtime::Runtime {
tc_runtime::Runtime::new( cm_runtime::Runtime::new(
pool, pool,
std::sync::Arc::new(tc_llm::ScriptedProvider::from_toml("").unwrap()), std::sync::Arc::new(cm_llm::ScriptedProvider::from_toml("").unwrap()),
tc_runtime::RuntimeConfig::basic("scripted", 1024), cm_runtime::RuntimeConfig::basic("scripted", 1024),
) )
} }
async fn serve(pool: sqlx::PgPool) -> TestServer { async fn serve(pool: sqlx::PgPool) -> TestServer {
let app = tc_api::router(AppState::new(pool.clone(), test_runtime(pool.clone()))); let app = cm_api::router(AppState::new(pool.clone(), test_runtime(pool.clone())));
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap(); let addr = listener.local_addr().unwrap();
tokio::spawn(async move { tokio::spawn(async move {
@@ -38,7 +38,7 @@ async fn seed_user(pool: &sqlx::PgPool, ws: &Workspace, email: &str, role: Role)
display_name: email.split('@').next().unwrap().into(), display_name: email.split('@').next().unwrap().into(),
created_at: time::OffsetDateTime::UNIX_EPOCH, created_at: time::OffsetDateTime::UNIX_EPOCH,
}; };
tc_db::repo::users::insert(pool, &user).await.unwrap(); cm_db::repo::users::insert(pool, &user).await.unwrap();
AuthService::new(pool.clone()) AuthService::new(pool.clone())
.set_password(user.id, "pw") .set_password(user.id, "pw")
.await .await
@@ -52,7 +52,7 @@ async fn seed_workspace(pool: &sqlx::PgPool) -> Workspace {
name: "Acme".into(), name: "Acme".into(),
plan: "team".into(), plan: "team".into(),
}; };
tc_db::repo::workspaces::insert(pool, &ws).await.unwrap(); cm_db::repo::workspaces::insert(pool, &ws).await.unwrap();
ws ws
} }
@@ -86,7 +86,7 @@ async fn create_claw(server: &TestServer, token: &str, name: &str) -> Value {
#[tokio::test] #[tokio::test]
async fn create_returns_live_agent_with_default_policy_and_audit() { async fn create_returns_live_agent_with_default_policy_and_audit() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let ws = seed_workspace(&pool).await; let ws = seed_workspace(&pool).await;
let owner = seed_user(&pool, &ws, "[email protected]", Role::Owner).await; let owner = seed_user(&pool, &ws, "[email protected]", Role::Owner).await;
let server = serve(pool.clone()).await; let server = serve(pool.clone()).await;
@@ -126,7 +126,7 @@ async fn create_returns_live_agent_with_default_policy_and_audit() {
#[tokio::test] #[tokio::test]
async fn patch_updates_profile_and_system_prompt() { async fn patch_updates_profile_and_system_prompt() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let ws = seed_workspace(&pool).await; let ws = seed_workspace(&pool).await;
seed_user(&pool, &ws, "[email protected]", Role::Owner).await; seed_user(&pool, &ws, "[email protected]", Role::Owner).await;
let server = serve(pool).await; let server = serve(pool).await;
@@ -155,7 +155,7 @@ async fn patch_updates_profile_and_system_prompt() {
#[tokio::test] #[tokio::test]
async fn delete_requires_owner_or_manager() { async fn delete_requires_owner_or_manager() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let ws = seed_workspace(&pool).await; let ws = seed_workspace(&pool).await;
seed_user(&pool, &ws, "[email protected]", Role::Owner).await; seed_user(&pool, &ws, "[email protected]", Role::Owner).await;
seed_user(&pool, &ws, "[email protected]", Role::Member).await; seed_user(&pool, &ws, "[email protected]", Role::Member).await;
@@ -201,7 +201,7 @@ async fn delete_requires_owner_or_manager() {
#[tokio::test] #[tokio::test]
async fn members_can_create_and_manage_their_own_claws() { async fn members_can_create_and_manage_their_own_claws() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let ws = seed_workspace(&pool).await; let ws = seed_workspace(&pool).await;
seed_user(&pool, &ws, "[email protected]", Role::Member).await; seed_user(&pool, &ws, "[email protected]", Role::Member).await;
let server = serve(pool).await; let server = serve(pool).await;
@@ -224,7 +224,7 @@ async fn members_can_create_and_manage_their_own_claws() {
#[tokio::test] #[tokio::test]
async fn cross_workspace_access_is_not_found() { async fn cross_workspace_access_is_not_found() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let ws_a = seed_workspace(&pool).await; let ws_a = seed_workspace(&pool).await;
let ws_b = seed_workspace(&pool).await; let ws_b = seed_workspace(&pool).await;
seed_user(&pool, &ws_a, "[email protected]", Role::Owner).await; seed_user(&pool, &ws_a, "[email protected]", Role::Owner).await;
@@ -5,16 +5,16 @@
use std::sync::Arc; use std::sync::Arc;
use cm_api::AppState;
use cm_auth::JwtVerifier;
use cm_domain::{Workspace, WorkspaceId};
use cm_llm::ScriptedProvider;
use cm_runtime::{Runtime, RuntimeConfig};
use jsonwebtoken::{encode, Algorithm, EncodingKey, Header}; use jsonwebtoken::{encode, Algorithm, EncodingKey, Header};
use rsa::pkcs1::EncodeRsaPrivateKey; use rsa::pkcs1::EncodeRsaPrivateKey;
use rsa::traits::PublicKeyParts; use rsa::traits::PublicKeyParts;
use rsa::RsaPrivateKey; use rsa::RsaPrivateKey;
use serde_json::{json, Value}; use serde_json::{json, Value};
use tc_api::AppState;
use tc_auth::JwtVerifier;
use tc_domain::{Workspace, WorkspaceId};
use tc_llm::ScriptedProvider;
use tc_runtime::{Runtime, RuntimeConfig};
fn b64url(bytes: &[u8]) -> String { fn b64url(bytes: &[u8]) -> String {
use base64::engine::general_purpose::URL_SAFE_NO_PAD; use base64::engine::general_purpose::URL_SAFE_NO_PAD;
@@ -24,7 +24,7 @@ fn b64url(bytes: &[u8]) -> String {
#[tokio::test] #[tokio::test]
async fn a_clerk_session_jwt_reaches_protected_endpoints() { async fn a_clerk_session_jwt_reaches_protected_endpoints() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
// Real issuer with a real key. // Real issuer with a real key.
let mut rng = rand_core::OsRng; let mut rng = rand_core::OsRng;
@@ -71,7 +71,7 @@ async fn a_clerk_session_jwt_reaches_protected_endpoints() {
name: "Acme".into(), name: "Acme".into(),
plan: "team".into(), plan: "team".into(),
}; };
tc_db::repo::workspaces::insert(&pool, &ws).await.unwrap(); cm_db::repo::workspaces::insert(&pool, &ws).await.unwrap();
// The app, Clerk-configured. // The app, Clerk-configured.
let runtime = Runtime::new( let runtime = Runtime::new(
@@ -81,7 +81,7 @@ async fn a_clerk_session_jwt_reaches_protected_endpoints() {
); );
let verifier = JwtVerifier::discover(&issuer).await.unwrap(); let verifier = JwtVerifier::discover(&issuer).await.unwrap();
let app = let app =
tc_api::router(AppState::new(pool.clone(), runtime).with_auth_verifier(Arc::new(verifier))); cm_api::router(AppState::new(pool.clone(), runtime).with_auth_verifier(Arc::new(verifier)));
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let base = format!("http://{}", listener.local_addr().unwrap()); let base = format!("http://{}", listener.local_addr().unwrap());
tokio::spawn(async move { tokio::spawn(async move {
@@ -1,14 +1,14 @@
use std::str::FromStr; use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
use cm_api::AppState;
use cm_auth::AuthService;
use cm_domain::{Role, SessionKey, User, UserId, Workspace, WorkspaceId};
use cm_llm::ScriptedProvider;
use cm_runtime::{Runtime, RuntimeConfig};
use eventsource_stream::Eventsource; use eventsource_stream::Eventsource;
use futures::StreamExt; use futures::StreamExt;
use serde_json::{json, Value}; use serde_json::{json, Value};
use tc_api::AppState;
use tc_auth::AuthService;
use tc_domain::{Role, SessionKey, User, UserId, Workspace, WorkspaceId};
use tc_llm::ScriptedProvider;
use tc_runtime::{Runtime, RuntimeConfig};
const SCENARIOS: &str = r#" const SCENARIOS: &str = r#"
[[scenario]] [[scenario]]
@@ -37,7 +37,7 @@ async fn serve(pool: sqlx::PgPool) -> TestServer {
Arc::new(ScriptedProvider::from_toml(SCENARIOS).unwrap()), Arc::new(ScriptedProvider::from_toml(SCENARIOS).unwrap()),
RuntimeConfig::basic("scripted", 1024), RuntimeConfig::basic("scripted", 1024),
); );
let app = tc_api::router(AppState::new(pool, runtime)); let app = cm_api::router(AppState::new(pool, runtime));
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap(); let addr = listener.local_addr().unwrap();
tokio::spawn(async move { tokio::spawn(async move {
@@ -55,7 +55,7 @@ async fn seed_and_login(pool: &sqlx::PgPool, server: &TestServer) -> (String, St
name: "Acme".into(), name: "Acme".into(),
plan: "team".into(), plan: "team".into(),
}; };
tc_db::repo::workspaces::insert(pool, &ws).await.unwrap(); cm_db::repo::workspaces::insert(pool, &ws).await.unwrap();
let owner = User { let owner = User {
id: UserId::new(), id: UserId::new(),
workspace_id: ws.id, workspace_id: ws.id,
@@ -64,7 +64,7 @@ async fn seed_and_login(pool: &sqlx::PgPool, server: &TestServer) -> (String, St
display_name: "Owner".into(), display_name: "Owner".into(),
created_at: time::OffsetDateTime::UNIX_EPOCH, created_at: time::OffsetDateTime::UNIX_EPOCH,
}; };
tc_db::repo::users::insert(pool, &owner).await.unwrap(); cm_db::repo::users::insert(pool, &owner).await.unwrap();
AuthService::new(pool.clone()) AuthService::new(pool.clone())
.set_password(owner.id, "pw") .set_password(owner.id, "pw")
.await .await
@@ -129,7 +129,7 @@ async fn collect_sse(response: reqwest::Response) -> Vec<(String, String, Value)
#[tokio::test] #[tokio::test]
async fn session_create_returns_a_parseable_session_key() { async fn session_create_returns_a_parseable_session_key() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let server = serve(pool.clone()).await; let server = serve(pool.clone()).await;
let (token, claw_id) = seed_and_login(&pool, &server).await; let (token, claw_id) = seed_and_login(&pool, &server).await;
@@ -141,7 +141,7 @@ async fn session_create_returns_a_parseable_session_key() {
#[tokio::test] #[tokio::test]
async fn sessions_list_is_scoped_to_the_claw() { async fn sessions_list_is_scoped_to_the_claw() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let server = serve(pool.clone()).await; let server = serve(pool.clone()).await;
let (token, claw_id) = seed_and_login(&pool, &server).await; let (token, claw_id) = seed_and_login(&pool, &server).await;
create_session(&server, &token, &claw_id).await; create_session(&server, &token, &claw_id).await;
@@ -162,7 +162,7 @@ async fn sessions_list_is_scoped_to_the_claw() {
#[tokio::test] #[tokio::test]
async fn gateway_streams_a_full_run_with_monotonic_ids() { async fn gateway_streams_a_full_run_with_monotonic_ids() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let server = serve(pool.clone()).await; let server = serve(pool.clone()).await;
let (token, claw_id) = seed_and_login(&pool, &server).await; let (token, claw_id) = seed_and_login(&pool, &server).await;
let session = create_session(&server, &token, &claw_id).await; let session = create_session(&server, &token, &claw_id).await;
@@ -207,7 +207,7 @@ async fn gateway_streams_a_full_run_with_monotonic_ids() {
#[tokio::test] #[tokio::test]
async fn gateway_runs_tools_and_history_replays_with_steps() { async fn gateway_runs_tools_and_history_replays_with_steps() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let server = serve(pool.clone()).await; let server = serve(pool.clone()).await;
let (token, claw_id) = seed_and_login(&pool, &server).await; let (token, claw_id) = seed_and_login(&pool, &server).await;
let session = create_session(&server, &token, &claw_id).await; let session = create_session(&server, &token, &claw_id).await;
@@ -252,7 +252,7 @@ async fn gateway_runs_tools_and_history_replays_with_steps() {
#[tokio::test] #[tokio::test]
async fn resume_replays_the_exact_journal() { async fn resume_replays_the_exact_journal() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let server = serve(pool.clone()).await; let server = serve(pool.clone()).await;
let (token, claw_id) = seed_and_login(&pool, &server).await; let (token, claw_id) = seed_and_login(&pool, &server).await;
let session = create_session(&server, &token, &claw_id).await; let session = create_session(&server, &token, &claw_id).await;
@@ -302,7 +302,7 @@ async fn resume_replays_the_exact_journal() {
#[tokio::test] #[tokio::test]
async fn gateway_rejects_claws_outside_the_workspace() { async fn gateway_rejects_claws_outside_the_workspace() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let server = serve(pool.clone()).await; let server = serve(pool.clone()).await;
let (token, claw_id) = seed_and_login(&pool, &server).await; let (token, claw_id) = seed_and_login(&pool, &server).await;
let (other_token, _) = seed_and_login(&pool, &server).await; let (other_token, _) = seed_and_login(&pool, &server).await;
@@ -5,14 +5,14 @@
use std::sync::Arc; use std::sync::Arc;
use cm_api::AppState;
use cm_auth::AuthService;
use cm_domain::{Role, User, UserId, Workspace, WorkspaceId};
use cm_llm::ScriptedProvider;
use cm_runtime::{Runtime, RuntimeConfig};
use eventsource_stream::Eventsource; use eventsource_stream::Eventsource;
use futures::StreamExt; use futures::StreamExt;
use serde_json::{json, Value}; use serde_json::{json, Value};
use tc_api::AppState;
use tc_auth::AuthService;
use tc_domain::{Role, User, UserId, Workspace, WorkspaceId};
use tc_llm::ScriptedProvider;
use tc_runtime::{Runtime, RuntimeConfig};
const STREAMS: usize = 40; const STREAMS: usize = 40;
@@ -50,13 +50,13 @@ async fn collect_sse(response: reqwest::Response) -> Vec<(String, String, Value)
#[tokio::test] #[tokio::test]
async fn forty_concurrent_streams_complete_and_replay_identically() { async fn forty_concurrent_streams_complete_and_replay_identically() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let runtime = Runtime::new( let runtime = Runtime::new(
pool.clone(), pool.clone(),
Arc::new(ScriptedProvider::from_toml(SCENARIOS).unwrap()), Arc::new(ScriptedProvider::from_toml(SCENARIOS).unwrap()),
RuntimeConfig::basic("scripted", 1024), RuntimeConfig::basic("scripted", 1024),
); );
let app = tc_api::router(AppState::new(pool.clone(), runtime)); let app = cm_api::router(AppState::new(pool.clone(), runtime));
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let base = format!("http://{}", listener.local_addr().unwrap()); let base = format!("http://{}", listener.local_addr().unwrap());
tokio::spawn(async move { tokio::spawn(async move {
@@ -68,7 +68,7 @@ async fn forty_concurrent_streams_complete_and_replay_identically() {
name: "Acme".into(), name: "Acme".into(),
plan: "team".into(), plan: "team".into(),
}; };
tc_db::repo::workspaces::insert(&pool, &ws).await.unwrap(); cm_db::repo::workspaces::insert(&pool, &ws).await.unwrap();
let owner = User { let owner = User {
id: UserId::new(), id: UserId::new(),
workspace_id: ws.id, workspace_id: ws.id,
@@ -77,7 +77,7 @@ async fn forty_concurrent_streams_complete_and_replay_identically() {
display_name: "Owner".into(), display_name: "Owner".into(),
created_at: time::OffsetDateTime::UNIX_EPOCH, created_at: time::OffsetDateTime::UNIX_EPOCH,
}; };
tc_db::repo::users::insert(&pool, &owner).await.unwrap(); cm_db::repo::users::insert(&pool, &owner).await.unwrap();
AuthService::new(pool.clone()) AuthService::new(pool.clone())
.set_password(owner.id, "pw") .set_password(owner.id, "pw")
.await .await
@@ -6,14 +6,14 @@
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use cm_api::AppState;
use cm_auth::AuthService;
use cm_config::OAuthConfig;
use cm_domain::{Role, User, UserId, Workspace, WorkspaceId};
use cm_llm::ScriptedProvider;
use cm_runtime::{Runtime, RuntimeConfig};
use cm_secrets::{BrokerServer, FileKey};
use serde_json::{json, Value}; use serde_json::{json, Value};
use tc_api::AppState;
use tc_auth::AuthService;
use tc_config::OAuthConfig;
use tc_domain::{Role, User, UserId, Workspace, WorkspaceId};
use tc_llm::ScriptedProvider;
use tc_runtime::{Runtime, RuntimeConfig};
use tc_secrets::{BrokerServer, FileKey};
/// A minimal real IdP: discovery doc + token endpoint that validates the /// A minimal real IdP: discovery doc + token endpoint that validates the
/// code and client, then issues a bearer token. /// code and client, then issues a bearer token.
@@ -44,7 +44,7 @@ async fn spawn_idp() -> String {
|AxState(()): AxState<()>, axum::Form(form): axum::Form<Value>| async move { |AxState(()): AxState<()>, axum::Form(form): axum::Form<Value>| async move {
if form["grant_type"] == "authorization_code" if form["grant_type"] == "authorization_code"
&& form["code"] == "good-code" && form["code"] == "good-code"
&& form["client_id"] == "teamclaw" && form["client_id"] == "clawmates"
&& form["client_secret"] == "tc-secret" && form["client_secret"] == "tc-secret"
{ {
axum::Json(json!({ axum::Json(json!({
@@ -93,7 +93,7 @@ async fn spawn_broker(pool: sqlx::PgPool) -> std::path::PathBuf {
#[tokio::test] #[tokio::test]
async fn oauth_round_trip_stores_the_token_in_the_broker_only() { async fn oauth_round_trip_stores_the_token_in_the_broker_only() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let issuer = spawn_idp().await; let issuer = spawn_idp().await;
let socket = spawn_broker(pool.clone()).await; let socket = spawn_broker(pool.clone()).await;
@@ -104,11 +104,11 @@ async fn oauth_round_trip_stores_the_token_in_the_broker_only() {
); );
let oauth = OAuthConfig { let oauth = OAuthConfig {
issuer_url: Some(issuer.clone()), issuer_url: Some(issuer.clone()),
client_id: Some("teamclaw".into()), client_id: Some("clawmates".into()),
client_secret: Some("tc-secret".into()), client_secret: Some("tc-secret".into()),
redirect_base: Some("http://127.0.0.1:9".into()), // shape only redirect_base: Some("http://127.0.0.1:9".into()), // shape only
}; };
let app = tc_api::router( let app = cm_api::router(
AppState::new(pool.clone(), runtime) AppState::new(pool.clone(), runtime)
.with_broker(socket) .with_broker(socket)
.with_oauth(oauth), .with_oauth(oauth),
@@ -130,7 +130,7 @@ async fn oauth_round_trip_stores_the_token_in_the_broker_only() {
name: "Acme".into(), name: "Acme".into(),
plan: "team".into(), plan: "team".into(),
}; };
tc_db::repo::workspaces::insert(&pool, &ws).await.unwrap(); cm_db::repo::workspaces::insert(&pool, &ws).await.unwrap();
let owner = User { let owner = User {
id: UserId::new(), id: UserId::new(),
workspace_id: ws.id, workspace_id: ws.id,
@@ -139,7 +139,7 @@ async fn oauth_round_trip_stores_the_token_in_the_broker_only() {
display_name: "Owner".into(), display_name: "Owner".into(),
created_at: time::OffsetDateTime::UNIX_EPOCH, created_at: time::OffsetDateTime::UNIX_EPOCH,
}; };
tc_db::repo::users::insert(&pool, &owner).await.unwrap(); cm_db::repo::users::insert(&pool, &owner).await.unwrap();
AuthService::new(pool.clone()) AuthService::new(pool.clone())
.set_password(owner.id, "pw") .set_password(owner.id, "pw")
.await .await
@@ -190,7 +190,7 @@ async fn oauth_round_trip_stores_the_token_in_the_broker_only() {
assert_eq!(status, 200, "start failed: {start}"); assert_eq!(status, 200, "start failed: {start}");
let authorize_url = start["authorize_url"].as_str().unwrap(); let authorize_url = start["authorize_url"].as_str().unwrap();
assert!(authorize_url.starts_with(&format!("{issuer}/auth?"))); assert!(authorize_url.starts_with(&format!("{issuer}/auth?")));
assert!(authorize_url.contains("client_id=teamclaw")); assert!(authorize_url.contains("client_id=clawmates"));
let oauth_state = start["state"].as_str().unwrap(); let oauth_state = start["state"].as_str().unwrap();
// The IdP redirects back with a code: exchange succeeds, connection // The IdP redirects back with a code: exchange succeeds, connection
@@ -274,7 +274,7 @@ async fn oauth_round_trip_stores_the_token_in_the_broker_only() {
#[tokio::test] #[tokio::test]
async fn mcp_oauth_uses_the_custom_issuer() { async fn mcp_oauth_uses_the_custom_issuer() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let issuer = spawn_idp().await; let issuer = spawn_idp().await;
let socket = spawn_broker(pool.clone()).await; let socket = spawn_broker(pool.clone()).await;
let runtime = Runtime::new( let runtime = Runtime::new(
@@ -286,11 +286,11 @@ async fn mcp_oauth_uses_the_custom_issuer() {
// can start. // can start.
let oauth = OAuthConfig { let oauth = OAuthConfig {
issuer_url: None, issuer_url: None,
client_id: Some("teamclaw".into()), client_id: Some("clawmates".into()),
client_secret: Some("tc-secret".into()), client_secret: Some("tc-secret".into()),
redirect_base: Some("http://127.0.0.1:9".into()), redirect_base: Some("http://127.0.0.1:9".into()),
}; };
let app = tc_api::router( let app = cm_api::router(
AppState::new(pool.clone(), runtime) AppState::new(pool.clone(), runtime)
.with_broker(socket) .with_broker(socket)
.with_oauth(oauth), .with_oauth(oauth),
@@ -308,7 +308,7 @@ async fn mcp_oauth_uses_the_custom_issuer() {
name: "Acme".into(), name: "Acme".into(),
plan: "team".into(), plan: "team".into(),
}; };
tc_db::repo::workspaces::insert(&pool, &ws).await.unwrap(); cm_db::repo::workspaces::insert(&pool, &ws).await.unwrap();
let owner = User { let owner = User {
id: UserId::new(), id: UserId::new(),
workspace_id: ws.id, workspace_id: ws.id,
@@ -317,7 +317,7 @@ async fn mcp_oauth_uses_the_custom_issuer() {
display_name: "Owner".into(), display_name: "Owner".into(),
created_at: time::OffsetDateTime::UNIX_EPOCH, created_at: time::OffsetDateTime::UNIX_EPOCH,
}; };
tc_db::repo::users::insert(&pool, &owner).await.unwrap(); cm_db::repo::users::insert(&pool, &owner).await.unwrap();
AuthService::new(pool.clone()) AuthService::new(pool.clone())
.set_password(owner.id, "pw") .set_password(owner.id, "pw")
.await .await
@@ -1,9 +1,9 @@
use serde_json::{json, Value}; use cm_api::AppState;
use tc_api::AppState; use cm_auth::AuthService;
use tc_auth::AuthService; use cm_domain::{
use tc_domain::{
AccessPolicy, Agent, AgentId, AgentStatus, Role, User, UserId, Workspace, WorkspaceId, AccessPolicy, Agent, AgentId, AgentStatus, Role, User, UserId, Workspace, WorkspaceId,
}; };
use serde_json::{json, Value};
struct TestServer { struct TestServer {
base: String, base: String,
@@ -11,17 +11,17 @@ struct TestServer {
} }
/// Boots the real axum server on an ephemeral port over real TCP. /// Boots the real axum server on an ephemeral port over real TCP.
fn test_runtime(pool: sqlx::PgPool) -> tc_runtime::Runtime { fn test_runtime(pool: sqlx::PgPool) -> cm_runtime::Runtime {
tc_runtime::Runtime::new( cm_runtime::Runtime::new(
pool, pool,
std::sync::Arc::new(tc_llm::ScriptedProvider::from_toml("").unwrap()), std::sync::Arc::new(cm_llm::ScriptedProvider::from_toml("").unwrap()),
tc_runtime::RuntimeConfig::basic("scripted", 1024), cm_runtime::RuntimeConfig::basic("scripted", 1024),
) )
} }
async fn serve(pool: sqlx::PgPool) -> TestServer { async fn serve(pool: sqlx::PgPool) -> TestServer {
let state = AppState::new(pool.clone(), test_runtime(pool)); let state = AppState::new(pool.clone(), test_runtime(pool));
let app = tc_api::router(state); let app = cm_api::router(state);
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap(); let addr = listener.local_addr().unwrap();
tokio::spawn(async move { tokio::spawn(async move {
@@ -39,7 +39,7 @@ async fn seed(pool: &sqlx::PgPool) -> (Workspace, User, Agent) {
name: "Acme".into(), name: "Acme".into(),
plan: "team".into(), plan: "team".into(),
}; };
tc_db::repo::workspaces::insert(pool, &ws).await.unwrap(); cm_db::repo::workspaces::insert(pool, &ws).await.unwrap();
let owner = User { let owner = User {
id: UserId::new(), id: UserId::new(),
workspace_id: ws.id, workspace_id: ws.id,
@@ -48,7 +48,7 @@ async fn seed(pool: &sqlx::PgPool) -> (Workspace, User, Agent) {
display_name: "Owner".into(), display_name: "Owner".into(),
created_at: time::OffsetDateTime::UNIX_EPOCH, created_at: time::OffsetDateTime::UNIX_EPOCH,
}; };
tc_db::repo::users::insert(pool, &owner).await.unwrap(); cm_db::repo::users::insert(pool, &owner).await.unwrap();
let agent = Agent { let agent = Agent {
id: AgentId::new(), id: AgentId::new(),
workspace_id: ws.id, workspace_id: ws.id,
@@ -61,7 +61,7 @@ async fn seed(pool: &sqlx::PgPool) -> (Workspace, User, Agent) {
managed_by: owner.id, managed_by: owner.id,
status: AgentStatus::Online, status: AgentStatus::Online,
}; };
tc_db::repo::agents::insert(pool, &agent, &AccessPolicy::default()) cm_db::repo::agents::insert(pool, &agent, &AccessPolicy::default())
.await .await
.unwrap(); .unwrap();
AuthService::new(pool.clone()) AuthService::new(pool.clone())
@@ -88,7 +88,7 @@ async fn login(server: &TestServer) -> String {
#[tokio::test] #[tokio::test]
async fn healthz_needs_no_auth() { async fn healthz_needs_no_auth() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let server = serve(pool).await; let server = serve(pool).await;
let res = server let res = server
.client .client
@@ -101,7 +101,7 @@ async fn healthz_needs_no_auth() {
#[tokio::test] #[tokio::test]
async fn protected_routes_reject_missing_or_bad_tokens() { async fn protected_routes_reject_missing_or_bad_tokens() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let server = serve(pool).await; let server = serve(pool).await;
for path in ["/api/user/me", "/api/team/claws", "/api/team/credits"] { for path in ["/api/user/me", "/api/team/claws", "/api/team/credits"] {
let bare = server let bare = server
@@ -124,7 +124,7 @@ async fn protected_routes_reject_missing_or_bad_tokens() {
#[tokio::test] #[tokio::test]
async fn login_rejects_wrong_password() { async fn login_rejects_wrong_password() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
seed(&pool).await; seed(&pool).await;
let server = serve(pool).await; let server = serve(pool).await;
let res = server let res = server
@@ -139,7 +139,7 @@ async fn login_rejects_wrong_password() {
#[tokio::test] #[tokio::test]
async fn user_me_returns_the_authenticated_user() { async fn user_me_returns_the_authenticated_user() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let (ws, owner, _) = seed(&pool).await; let (ws, owner, _) = seed(&pool).await;
let server = serve(pool).await; let server = serve(pool).await;
let token = login(&server).await; let token = login(&server).await;
@@ -162,7 +162,7 @@ async fn user_me_returns_the_authenticated_user() {
#[tokio::test] #[tokio::test]
async fn team_claws_lists_the_roster() { async fn team_claws_lists_the_roster() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let (_, _, agent) = seed(&pool).await; let (_, _, agent) = seed(&pool).await;
let server = serve(pool).await; let server = serve(pool).await;
let token = login(&server).await; let token = login(&server).await;
@@ -186,9 +186,9 @@ async fn team_claws_lists_the_roster() {
#[tokio::test] #[tokio::test]
async fn team_credits_returns_available_balance() { async fn team_credits_returns_available_balance() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let (ws, _, _) = seed(&pool).await; let (ws, _, _) = seed(&pool).await;
tc_db::repo::credits::add_lot(&pool, ws.id, 500, "purchase") cm_db::repo::credits::add_lot(&pool, ws.id, 500, "purchase")
.await .await
.unwrap(); .unwrap();
let server = serve(pool).await; let server = serve(pool).await;
@@ -209,7 +209,7 @@ async fn team_credits_returns_available_balance() {
#[tokio::test] #[tokio::test]
async fn team_permissions_reflect_role() { async fn team_permissions_reflect_role() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
seed(&pool).await; seed(&pool).await;
let server = serve(pool).await; let server = serve(pool).await;
let token = login(&server).await; let token = login(&server).await;
@@ -231,7 +231,7 @@ async fn team_permissions_reflect_role() {
#[tokio::test] #[tokio::test]
async fn logout_revokes_the_session() { async fn logout_revokes_the_session() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
seed(&pool).await; seed(&pool).await;
let server = serve(pool).await; let server = serve(pool).await;
let token = login(&server).await; let token = login(&server).await;
@@ -257,7 +257,7 @@ async fn logout_revokes_the_session() {
#[tokio::test] #[tokio::test]
async fn team_members_lists_workspace_users() { async fn team_members_lists_workspace_users() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let (_, owner, _) = seed(&pool).await; let (_, owner, _) = seed(&pool).await;
let server = serve(pool).await; let server = serve(pool).await;
let token = login(&server).await; let token = login(&server).await;
@@ -2,12 +2,12 @@
use std::sync::Arc; use std::sync::Arc;
use cm_api::AppState;
use cm_auth::AuthService;
use cm_domain::{FileDrive, FileNode, Role, User, UserId, Workspace, WorkspaceId};
use cm_llm::ScriptedProvider;
use cm_runtime::{Runtime, RuntimeConfig};
use serde_json::{json, Value}; use serde_json::{json, Value};
use tc_api::AppState;
use tc_auth::AuthService;
use tc_domain::{FileDrive, FileNode, Role, User, UserId, Workspace, WorkspaceId};
use tc_llm::ScriptedProvider;
use tc_runtime::{Runtime, RuntimeConfig};
use uuid::Uuid; use uuid::Uuid;
struct TestServer { struct TestServer {
@@ -21,7 +21,7 @@ async fn serve(pool: sqlx::PgPool) -> TestServer {
Arc::new(ScriptedProvider::from_toml("").unwrap()), Arc::new(ScriptedProvider::from_toml("").unwrap()),
RuntimeConfig::basic("scripted", 1024), RuntimeConfig::basic("scripted", 1024),
); );
let app = tc_api::router(AppState::new(pool, runtime)); let app = cm_api::router(AppState::new(pool, runtime));
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap(); let addr = listener.local_addr().unwrap();
tokio::spawn(async move { tokio::spawn(async move {
@@ -39,7 +39,7 @@ async fn seed_and_login(pool: &sqlx::PgPool, server: &TestServer) -> (String, St
name: "Acme".into(), name: "Acme".into(),
plan: "team".into(), plan: "team".into(),
}; };
tc_db::repo::workspaces::insert(pool, &ws).await.unwrap(); cm_db::repo::workspaces::insert(pool, &ws).await.unwrap();
let owner = User { let owner = User {
id: UserId::new(), id: UserId::new(),
workspace_id: ws.id, workspace_id: ws.id,
@@ -48,7 +48,7 @@ async fn seed_and_login(pool: &sqlx::PgPool, server: &TestServer) -> (String, St
display_name: "Owner".into(), display_name: "Owner".into(),
created_at: time::OffsetDateTime::UNIX_EPOCH, created_at: time::OffsetDateTime::UNIX_EPOCH,
}; };
tc_db::repo::users::insert(pool, &owner).await.unwrap(); cm_db::repo::users::insert(pool, &owner).await.unwrap();
AuthService::new(pool.clone()) AuthService::new(pool.clone())
.set_password(owner.id, "pw") .set_password(owner.id, "pw")
.await .await
@@ -82,15 +82,15 @@ async fn seed_and_login(pool: &sqlx::PgPool, server: &TestServer) -> (String, St
#[tokio::test] #[tokio::test]
async fn skill_library_install_and_uninstall_round_trip() { async fn skill_library_install_and_uninstall_round_trip() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let server = serve(pool.clone()).await; let server = serve(pool.clone()).await;
let (token, claw_id) = seed_and_login(&pool, &server).await; let (token, claw_id) = seed_and_login(&pool, &server).await;
let skill = tc_db::repo::skills::create( let skill = cm_db::repo::skills::create(
&pool, &pool,
None, None,
"Daily briefing", "Daily briefing",
"TeamClaw", "Clawmates",
"Summarize the day each morning.", "Summarize the day each morning.",
"Each morning, compile...", "Each morning, compile...",
) )
@@ -173,11 +173,11 @@ async fn skill_library_install_and_uninstall_round_trip() {
#[tokio::test] #[tokio::test]
async fn file_listings_are_drive_and_agent_scoped() { async fn file_listings_are_drive_and_agent_scoped() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let server = serve(pool.clone()).await; let server = serve(pool.clone()).await;
let (token, claw_id) = seed_and_login(&pool, &server).await; let (token, claw_id) = seed_and_login(&pool, &server).await;
let agent_uuid: Uuid = claw_id.parse().unwrap(); let agent_uuid: Uuid = claw_id.parse().unwrap();
let workspace_id = tc_db::repo::agents::get(&pool, agent_uuid.into()) let workspace_id = cm_db::repo::agents::get(&pool, agent_uuid.into())
.await .await
.unwrap() .unwrap()
.workspace_id; .workspace_id;
@@ -187,7 +187,7 @@ async fn file_listings_are_drive_and_agent_scoped() {
(FileDrive::Received, Some(agent_uuid), "incoming.csv"), (FileDrive::Received, Some(agent_uuid), "incoming.csv"),
(FileDrive::Shared, None, "team-handbook.md"), (FileDrive::Shared, None, "team-handbook.md"),
] { ] {
tc_db::repo::files::upsert( cm_db::repo::files::upsert(
&pool, &pool,
&FileNode { &FileNode {
id: Uuid::now_v7(), id: Uuid::now_v7(),
@@ -4,14 +4,14 @@
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use cm_api::AppState;
use cm_auth::AuthService;
use cm_domain::{Role, User, UserId, Workspace, WorkspaceId};
use cm_llm::ScriptedProvider;
use cm_runtime::{Runtime, RuntimeConfig};
use cm_secrets::{BrokerServer, FileKey};
use hmac::{Hmac, Mac}; use hmac::{Hmac, Mac};
use serde_json::{json, Value}; use serde_json::{json, Value};
use tc_api::AppState;
use tc_auth::AuthService;
use tc_domain::{Role, User, UserId, Workspace, WorkspaceId};
use tc_llm::ScriptedProvider;
use tc_runtime::{Runtime, RuntimeConfig};
use tc_secrets::{BrokerServer, FileKey};
const SCENARIOS: &str = r##" const SCENARIOS: &str = r##"
[[scenario]] [[scenario]]
@@ -57,7 +57,7 @@ async fn spawn_broker(pool: sqlx::PgPool) -> std::path::PathBuf {
#[tokio::test] #[tokio::test]
async fn mention_round_trip_verifies_runs_and_gates_the_reply() { async fn mention_round_trip_verifies_runs_and_gates_the_reply() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let socket = spawn_broker(pool.clone()).await; let socket = spawn_broker(pool.clone()).await;
let runtime = Runtime::new( let runtime = Runtime::new(
pool.clone(), pool.clone(),
@@ -71,7 +71,7 @@ async fn mention_round_trip_verifies_runs_and_gates_the_reply() {
slack_base_url: "http://127.0.0.1:1".into(), // never reached here slack_base_url: "http://127.0.0.1:1".into(), // never reached here
}, },
); );
let app = tc_api::router(AppState::new(pool.clone(), runtime).with_broker(socket.clone())); let app = cm_api::router(AppState::new(pool.clone(), runtime).with_broker(socket.clone()));
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap(); let addr = listener.local_addr().unwrap();
tokio::spawn(async move { tokio::spawn(async move {
@@ -86,7 +86,7 @@ async fn mention_round_trip_verifies_runs_and_gates_the_reply() {
name: "Acme".into(), name: "Acme".into(),
plan: "team".into(), plan: "team".into(),
}; };
tc_db::repo::workspaces::insert(&pool, &ws).await.unwrap(); cm_db::repo::workspaces::insert(&pool, &ws).await.unwrap();
let owner = User { let owner = User {
id: UserId::new(), id: UserId::new(),
workspace_id: ws.id, workspace_id: ws.id,
@@ -95,7 +95,7 @@ async fn mention_round_trip_verifies_runs_and_gates_the_reply() {
display_name: "Owner".into(), display_name: "Owner".into(),
created_at: time::OffsetDateTime::UNIX_EPOCH, created_at: time::OffsetDateTime::UNIX_EPOCH,
}; };
tc_db::repo::users::insert(&pool, &owner).await.unwrap(); cm_db::repo::users::insert(&pool, &owner).await.unwrap();
AuthService::new(pool.clone()) AuthService::new(pool.clone())
.set_password(owner.id, "pw") .set_password(owner.id, "pw")
.await .await
@@ -186,14 +186,14 @@ async fn mention_round_trip_verifies_runs_and_gates_the_reply() {
.unwrap(); .unwrap();
assert_eq!(mention.status(), 200); assert_eq!(mention.status(), 200);
let agent_id = tc_domain::AgentId::from(claw_id.parse::<uuid::Uuid>().unwrap()); let agent_id = cm_domain::AgentId::from(claw_id.parse::<uuid::Uuid>().unwrap());
let mut gated = false; let mut gated = false;
for _ in 0..100 { for _ in 0..100 {
let sessions = tc_db::repo::sessions::list_by_agent(&pool, agent_id) let sessions = cm_db::repo::sessions::list_by_agent(&pool, agent_id)
.await .await
.unwrap(); .unwrap();
if sessions.iter().any(|s| s.title == "💬 Slack") { if sessions.iter().any(|s| s.title == "💬 Slack") {
let pending = tc_safety::approvals::list_pending(&pool, ws.id) let pending = cm_safety::approvals::list_pending(&pool, ws.id)
.await .await
.unwrap(); .unwrap();
if pending.len() == 1 && pending[0].action_type == "slack.post" { if pending.len() == 1 && pending[0].action_type == "slack.post" {
@@ -1,5 +1,5 @@
[package] [package]
name = "tc-auth" name = "cm-auth"
version = "0.1.0" version = "0.1.0"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
@@ -16,8 +16,8 @@ serde = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
sha2 = "0.10" sha2 = "0.10"
sqlx = { workspace = true } sqlx = { workspace = true }
tc-db = { path = "../tc-db" } cm-db = { path = "../cm-db" }
tc-domain = { path = "../tc-domain" } cm-domain = { path = "../cm-domain" }
thiserror = { workspace = true } thiserror = { workspace = true }
time = { workspace = true } time = { workspace = true }
@@ -26,7 +26,7 @@ axum = "0.8"
rsa = { version = "0.9", features = ["pem"] } rsa = { version = "0.9", features = ["pem"] }
serde_json = { workspace = true } serde_json = { workspace = true }
sqlx = { workspace = true } sqlx = { workspace = true }
tc-testkit = { path = "../tc-testkit" } cm-testkit = { path = "../cm-testkit" }
tokio = { workspace = true } tokio = { workspace = true }
[lints] [lints]
@@ -1,4 +1,4 @@
//! Authentication for TeamClaw (spec §16: SSO + RBAC). //! Authentication for Clawmates (spec §16: SSO + RBAC).
//! //!
//! `local` mode: argon2id password hashes and opaque bearer tokens whose //! `local` mode: argon2id password hashes and opaque bearer tokens whose
//! SHA-256 hashes are stored in `auth_sessions`. This is the zero-dependency //! SHA-256 hashes are stored in `auth_sessions`. This is the zero-dependency
@@ -1,8 +1,8 @@
use argon2::password_hash::rand_core::OsRng; use argon2::password_hash::rand_core::OsRng;
use argon2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString}; use argon2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString};
use argon2::Argon2; use argon2::Argon2;
use cm_domain::{Role, UserId, WorkspaceId};
use sqlx::PgPool; use sqlx::PgPool;
use tc_domain::{Role, UserId, WorkspaceId};
use time::{Duration, OffsetDateTime}; use time::{Duration, OffsetDateTime};
use crate::token::{hash_token, SessionToken}; use crate::token::{hash_token, SessionToken};
@@ -5,13 +5,13 @@
use std::sync::Arc; use std::sync::Arc;
use cm_auth::{AuthError, AuthService, JwtVerifier};
use cm_domain::Role;
use jsonwebtoken::{encode, Algorithm, EncodingKey, Header}; use jsonwebtoken::{encode, Algorithm, EncodingKey, Header};
use rsa::pkcs1::EncodeRsaPrivateKey; use rsa::pkcs1::EncodeRsaPrivateKey;
use rsa::traits::PublicKeyParts; use rsa::traits::PublicKeyParts;
use rsa::RsaPrivateKey; use rsa::RsaPrivateKey;
use serde_json::json; use serde_json::json;
use tc_auth::{AuthError, AuthService, JwtVerifier};
use tc_domain::Role;
struct Issuer { struct Issuer {
url: String, url: String,
@@ -115,14 +115,14 @@ fn token(
#[tokio::test] #[tokio::test]
async fn clerk_tokens_authenticate_with_jit_provisioning_and_role_mapping() { async fn clerk_tokens_authenticate_with_jit_provisioning_and_role_mapping() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let issuer = spawn_issuer().await; let issuer = spawn_issuer().await;
let ws = tc_domain::Workspace { let ws = cm_domain::Workspace {
id: tc_domain::WorkspaceId::new(), id: cm_domain::WorkspaceId::new(),
name: "Acme".into(), name: "Acme".into(),
plan: "team".into(), plan: "team".into(),
}; };
tc_db::repo::workspaces::insert(&pool, &ws).await.unwrap(); cm_db::repo::workspaces::insert(&pool, &ws).await.unwrap();
let verifier = JwtVerifier::discover(&issuer.url).await.expect("discovery"); let verifier = JwtVerifier::discover(&issuer.url).await.expect("discovery");
let auth = AuthService::new(pool.clone()).with_verifier(Arc::new(verifier)); let auth = AuthService::new(pool.clone()).with_verifier(Arc::new(verifier));
@@ -197,15 +197,15 @@ async fn clerk_tokens_authenticate_with_jit_provisioning_and_role_mapping() {
#[tokio::test] #[tokio::test]
async fn the_wrong_issuer_is_refused() { async fn the_wrong_issuer_is_refused() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let real = spawn_issuer().await; let real = spawn_issuer().await;
let evil = spawn_issuer().await; let evil = spawn_issuer().await;
let ws = tc_domain::Workspace { let ws = cm_domain::Workspace {
id: tc_domain::WorkspaceId::new(), id: cm_domain::WorkspaceId::new(),
name: "Acme".into(), name: "Acme".into(),
plan: "team".into(), plan: "team".into(),
}; };
tc_db::repo::workspaces::insert(&pool, &ws).await.unwrap(); cm_db::repo::workspaces::insert(&pool, &ws).await.unwrap();
let verifier = JwtVerifier::discover(&real.url).await.unwrap(); let verifier = JwtVerifier::discover(&real.url).await.unwrap();
let auth = AuthService::new(pool.clone()).with_verifier(Arc::new(verifier)); let auth = AuthService::new(pool.clone()).with_verifier(Arc::new(verifier));
@@ -1,5 +1,5 @@
use tc_auth::{AuthError, AuthService}; use cm_auth::{AuthError, AuthService};
use tc_domain::{Role, User, UserId, Workspace, WorkspaceId}; use cm_domain::{Role, User, UserId, Workspace, WorkspaceId};
async fn seeded(pool: &sqlx::PgPool) -> (Workspace, User) { async fn seeded(pool: &sqlx::PgPool) -> (Workspace, User) {
let ws = Workspace { let ws = Workspace {
@@ -7,7 +7,7 @@ async fn seeded(pool: &sqlx::PgPool) -> (Workspace, User) {
name: "Acme".into(), name: "Acme".into(),
plan: "team".into(), plan: "team".into(),
}; };
tc_db::repo::workspaces::insert(pool, &ws).await.unwrap(); cm_db::repo::workspaces::insert(pool, &ws).await.unwrap();
let user = User { let user = User {
id: UserId::new(), id: UserId::new(),
workspace_id: ws.id, workspace_id: ws.id,
@@ -16,13 +16,13 @@ async fn seeded(pool: &sqlx::PgPool) -> (Workspace, User) {
display_name: "Owner".into(), display_name: "Owner".into(),
created_at: time::OffsetDateTime::UNIX_EPOCH, created_at: time::OffsetDateTime::UNIX_EPOCH,
}; };
tc_db::repo::users::insert(pool, &user).await.unwrap(); cm_db::repo::users::insert(pool, &user).await.unwrap();
(ws, user) (ws, user)
} }
#[tokio::test] #[tokio::test]
async fn register_login_authenticate_round_trip() { async fn register_login_authenticate_round_trip() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let (ws, user) = seeded(&pool).await; let (ws, user) = seeded(&pool).await;
let auth = AuthService::new(pool); let auth = AuthService::new(pool);
@@ -43,7 +43,7 @@ async fn register_login_authenticate_round_trip() {
#[tokio::test] #[tokio::test]
async fn wrong_password_is_rejected_without_detail() { async fn wrong_password_is_rejected_without_detail() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let (_, user) = seeded(&pool).await; let (_, user) = seeded(&pool).await;
let auth = AuthService::new(pool); let auth = AuthService::new(pool);
auth.set_password(user.id, "right").await.unwrap(); auth.set_password(user.id, "right").await.unwrap();
@@ -57,7 +57,7 @@ async fn wrong_password_is_rejected_without_detail() {
#[tokio::test] #[tokio::test]
async fn unknown_email_is_the_same_error_as_wrong_password() { async fn unknown_email_is_the_same_error_as_wrong_password() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let auth = AuthService::new(pool); let auth = AuthService::new(pool);
let err = auth.login_local("[email protected]", "pw").await.unwrap_err(); let err = auth.login_local("[email protected]", "pw").await.unwrap_err();
// Indistinguishable from a wrong password: no account enumeration. // Indistinguishable from a wrong password: no account enumeration.
@@ -66,7 +66,7 @@ async fn unknown_email_is_the_same_error_as_wrong_password() {
#[tokio::test] #[tokio::test]
async fn user_without_password_cannot_login_locally() { async fn user_without_password_cannot_login_locally() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let (_, _user) = seeded(&pool).await; let (_, _user) = seeded(&pool).await;
let auth = AuthService::new(pool); let auth = AuthService::new(pool);
let err = auth.login_local("[email protected]", "pw").await.unwrap_err(); let err = auth.login_local("[email protected]", "pw").await.unwrap_err();
@@ -75,7 +75,7 @@ async fn user_without_password_cannot_login_locally() {
#[tokio::test] #[tokio::test]
async fn unknown_token_is_unauthenticated() { async fn unknown_token_is_unauthenticated() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let auth = AuthService::new(pool); let auth = AuthService::new(pool);
let err = auth.authenticate("not-a-real-token").await.unwrap_err(); let err = auth.authenticate("not-a-real-token").await.unwrap_err();
assert!(matches!(err, AuthError::Unauthenticated)); assert!(matches!(err, AuthError::Unauthenticated));
@@ -83,7 +83,7 @@ async fn unknown_token_is_unauthenticated() {
#[tokio::test] #[tokio::test]
async fn expired_session_is_unauthenticated() { async fn expired_session_is_unauthenticated() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let (_, user) = seeded(&pool).await; let (_, user) = seeded(&pool).await;
let auth = AuthService::new(pool.clone()); let auth = AuthService::new(pool.clone());
auth.set_password(user.id, "pw").await.unwrap(); auth.set_password(user.id, "pw").await.unwrap();
@@ -100,7 +100,7 @@ async fn expired_session_is_unauthenticated() {
#[tokio::test] #[tokio::test]
async fn logout_invalidates_the_token() { async fn logout_invalidates_the_token() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let (_, user) = seeded(&pool).await; let (_, user) = seeded(&pool).await;
let auth = AuthService::new(pool); let auth = AuthService::new(pool);
auth.set_password(user.id, "pw").await.unwrap(); auth.set_password(user.id, "pw").await.unwrap();
@@ -114,7 +114,7 @@ async fn logout_invalidates_the_token() {
#[tokio::test] #[tokio::test]
async fn tokens_are_unique_per_login() { async fn tokens_are_unique_per_login() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let (_, user) = seeded(&pool).await; let (_, user) = seeded(&pool).await;
let auth = AuthService::new(pool); let auth = AuthService::new(pool);
auth.set_password(user.id, "pw").await.unwrap(); auth.set_password(user.id, "pw").await.unwrap();
@@ -1,5 +1,5 @@
[package] [package]
name = "tc-billing" name = "cm-billing"
version = "0.1.0" version = "0.1.0"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
@@ -8,13 +8,13 @@ publish.workspace = true
[dependencies] [dependencies]
sqlx = { workspace = true } sqlx = { workspace = true }
tc-domain = { path = "../tc-domain" } cm-domain = { path = "../cm-domain" }
thiserror = { workspace = true } thiserror = { workspace = true }
uuid = { workspace = true } uuid = { workspace = true }
[dev-dependencies] [dev-dependencies]
tc-db = { path = "../tc-db" } cm-db = { path = "../cm-db" }
tc-testkit = { path = "../tc-testkit" } cm-testkit = { path = "../cm-testkit" }
time = { workspace = true } time = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
@@ -3,8 +3,8 @@
//! first and never expire; a workspace can run dry but never goes //! first and never expire; a workspace can run dry but never goes
//! negative — usage is always recorded in full either way. //! negative — usage is always recorded in full either way.
use cm_domain::{AgentId, WorkspaceId};
use sqlx::PgPool; use sqlx::PgPool;
use tc_domain::{AgentId, WorkspaceId};
use uuid::Uuid; use uuid::Uuid;
pub const TOKENS_PER_CREDIT: u64 = 1000; pub const TOKENS_PER_CREDIT: u64 = 1000;
@@ -1,5 +1,5 @@
use tc_billing::{charge, credits_for_tokens, redeem_promo, usage_last_7_days, BillingError}; use cm_billing::{charge, credits_for_tokens, redeem_promo, usage_last_7_days, BillingError};
use tc_domain::{ use cm_domain::{
AccessPolicy, Agent, AgentId, AgentStatus, Role, User, UserId, Workspace, WorkspaceId, AccessPolicy, Agent, AgentId, AgentStatus, Role, User, UserId, Workspace, WorkspaceId,
}; };
@@ -18,7 +18,7 @@ async fn seeded(pool: &sqlx::PgPool) -> (Workspace, Agent, uuid::Uuid) {
name: "Acme".into(), name: "Acme".into(),
plan: "team".into(), plan: "team".into(),
}; };
tc_db::repo::workspaces::insert(pool, &ws).await.unwrap(); cm_db::repo::workspaces::insert(pool, &ws).await.unwrap();
let owner = User { let owner = User {
id: UserId::new(), id: UserId::new(),
workspace_id: ws.id, workspace_id: ws.id,
@@ -27,7 +27,7 @@ async fn seeded(pool: &sqlx::PgPool) -> (Workspace, Agent, uuid::Uuid) {
display_name: "Owner".into(), display_name: "Owner".into(),
created_at: time::OffsetDateTime::UNIX_EPOCH, created_at: time::OffsetDateTime::UNIX_EPOCH,
}; };
tc_db::repo::users::insert(pool, &owner).await.unwrap(); cm_db::repo::users::insert(pool, &owner).await.unwrap();
let agent = Agent { let agent = Agent {
id: AgentId::new(), id: AgentId::new(),
workspace_id: ws.id, workspace_id: ws.id,
@@ -40,24 +40,24 @@ async fn seeded(pool: &sqlx::PgPool) -> (Workspace, Agent, uuid::Uuid) {
managed_by: owner.id, managed_by: owner.id,
status: AgentStatus::Online, status: AgentStatus::Online,
}; };
tc_db::repo::agents::insert(pool, &agent, &AccessPolicy::default()) cm_db::repo::agents::insert(pool, &agent, &AccessPolicy::default())
.await .await
.unwrap(); .unwrap();
let session = tc_db::repo::sessions::create(pool, agent.id, ws.id, "Chat") let session = cm_db::repo::sessions::create(pool, agent.id, ws.id, "Chat")
.await .await
.unwrap(); .unwrap();
let run_id = tc_db::repo::runs::create(pool, session.id).await.unwrap(); let run_id = cm_db::repo::runs::create(pool, session.id).await.unwrap();
(ws, agent, run_id) (ws, agent, run_id)
} }
#[tokio::test] #[tokio::test]
async fn charges_span_lots_oldest_first_and_record_usage() { async fn charges_span_lots_oldest_first_and_record_usage() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let (ws, agent, run_id) = seeded(&pool).await; let (ws, agent, run_id) = seeded(&pool).await;
tc_db::repo::credits::add_lot(&pool, ws.id, 2, "first") cm_db::repo::credits::add_lot(&pool, ws.id, 2, "first")
.await .await
.unwrap(); .unwrap();
tc_db::repo::credits::add_lot(&pool, ws.id, 100, "second") cm_db::repo::credits::add_lot(&pool, ws.id, 100, "second")
.await .await
.unwrap(); .unwrap();
@@ -67,7 +67,7 @@ async fn charges_span_lots_oldest_first_and_record_usage() {
.unwrap(); .unwrap();
assert_eq!(deducted, 3); assert_eq!(deducted, 3);
assert_eq!( assert_eq!(
tc_db::repo::credits::balance(&pool, ws.id).await.unwrap(), cm_db::repo::credits::balance(&pool, ws.id).await.unwrap(),
99 99
); );
@@ -89,9 +89,9 @@ async fn charges_span_lots_oldest_first_and_record_usage() {
#[tokio::test] #[tokio::test]
async fn an_empty_workspace_records_usage_but_clamps_at_zero() { async fn an_empty_workspace_records_usage_but_clamps_at_zero() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let (ws, agent, run_id) = seeded(&pool).await; let (ws, agent, run_id) = seeded(&pool).await;
tc_db::repo::credits::add_lot(&pool, ws.id, 1, "tiny") cm_db::repo::credits::add_lot(&pool, ws.id, 1, "tiny")
.await .await
.unwrap(); .unwrap();
@@ -101,7 +101,7 @@ async fn an_empty_workspace_records_usage_but_clamps_at_zero() {
.unwrap(); .unwrap();
assert_eq!(deducted, 1); assert_eq!(deducted, 1);
assert_eq!( assert_eq!(
tc_db::repo::credits::balance(&pool, ws.id).await.unwrap(), cm_db::repo::credits::balance(&pool, ws.id).await.unwrap(),
0 0
); );
@@ -112,7 +112,7 @@ async fn an_empty_workspace_records_usage_but_clamps_at_zero() {
#[tokio::test] #[tokio::test]
async fn promo_codes_redeem_exactly_once() { async fn promo_codes_redeem_exactly_once() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let (ws, _, _) = seeded(&pool).await; let (ws, _, _) = seeded(&pool).await;
sqlx::query("INSERT INTO promo_codes (code, credits) VALUES ('WELCOME500', 500)") sqlx::query("INSERT INTO promo_codes (code, credits) VALUES ('WELCOME500', 500)")
.execute(&pool) .execute(&pool)
@@ -122,7 +122,7 @@ async fn promo_codes_redeem_exactly_once() {
let granted = redeem_promo(&pool, ws.id, "WELCOME500").await.unwrap(); let granted = redeem_promo(&pool, ws.id, "WELCOME500").await.unwrap();
assert_eq!(granted, 500); assert_eq!(granted, 500);
assert_eq!( assert_eq!(
tc_db::repo::credits::balance(&pool, ws.id).await.unwrap(), cm_db::repo::credits::balance(&pool, ws.id).await.unwrap(),
500 500
); );
@@ -1,5 +1,5 @@
[package] [package]
name = "tc-config" name = "cm-config"
version = "0.1.0" version = "0.1.0"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
@@ -1,7 +1,7 @@
//! Configuration loading for all TeamClaw binaries. //! Configuration loading for all Clawmates binaries.
//! //!
//! One TOML file plus `TEAMCLAW_*` environment overrides (nested keys split //! One TOML file plus `CLAWMATES_*` environment overrides (nested keys split
//! on `__`, e.g. `TEAMCLAW_DATABASE__URL`). The same configuration tree //! on `__`, e.g. `CLAWMATES_DATABASE__URL`). The same configuration tree
//! drives both deployment targets; semantic validation rejects combinations //! drives both deployment targets; semantic validation rejects combinations
//! that would only fail at runtime (e.g. an OpenAI-compatible provider with //! that would only fail at runtime (e.g. an OpenAI-compatible provider with
//! no endpoint to call). //! no endpoint to call).
@@ -84,7 +84,7 @@ pub struct StorageConfig {
pub data_dir: String, pub data_dir: String,
#[serde(default = "default_backend")] #[serde(default = "default_backend")]
pub backend: StorageBackend, pub backend: StorageBackend,
/// S3 backend settings; keys arrive via TEAMCLAW_STORAGE__* env vars. /// S3 backend settings; keys arrive via CLAWMATES_STORAGE__* env vars.
pub s3_endpoint: Option<String>, pub s3_endpoint: Option<String>,
pub s3_bucket: Option<String>, pub s3_bucket: Option<String>,
pub s3_access_key: Option<String>, pub s3_access_key: Option<String>,
@@ -117,7 +117,7 @@ pub struct BrokerConfig {
impl Default for BrokerConfig { impl Default for BrokerConfig {
fn default() -> Self { fn default() -> Self {
BrokerConfig { BrokerConfig {
socket_path: "/tmp/teamclaw-broker.sock".into(), socket_path: "/tmp/clawmates-broker.sock".into(),
} }
} }
} }
@@ -151,8 +151,8 @@ pub struct SandboxConfig {
impl Default for SandboxConfig { impl Default for SandboxConfig {
fn default() -> Self { fn default() -> Self {
SandboxConfig { SandboxConfig {
image: "teamclaw/agent-base:dev".into(), image: "clawmates/agent-base:dev".into(),
browser_image: "teamclaw/agent-browser:dev".into(), browser_image: "clawmates/agent-browser:dev".into(),
enabled: true, enabled: true,
warm_pool: 0, warm_pool: 0,
} }
@@ -206,7 +206,7 @@ pub enum ConfigError {
} }
impl AppConfig { impl AppConfig {
/// Loads configuration from `path`, overlaying `TEAMCLAW_*` environment /// Loads configuration from `path`, overlaying `CLAWMATES_*` environment
/// variables, and validates it. /// variables, and validates it.
pub fn load_from(path: &Path) -> Result<AppConfig, ConfigError> { pub fn load_from(path: &Path) -> Result<AppConfig, ConfigError> {
if !path.is_file() { if !path.is_file() {
@@ -217,7 +217,7 @@ impl AppConfig {
} }
let config: AppConfig = Figment::new() let config: AppConfig = Figment::new()
.merge(Toml::file(path)) .merge(Toml::file(path))
.merge(Env::prefixed("TEAMCLAW_").split("__")) .merge(Env::prefixed("CLAWMATES_").split("__"))
.extract() .extract()
.map_err(|e| ConfigError::Load(e.to_string()))?; .map_err(|e| ConfigError::Load(e.to_string()))?;
config.validate()?; config.validate()?;
@@ -2,14 +2,14 @@
// (large) error type; the lint has nothing actionable here. // (large) error type; the lint has nothing actionable here.
#![allow(clippy::result_large_err)] #![allow(clippy::result_large_err)]
use tc_config::{AppConfig, AuthMode, ConfigError, DeployTarget, LlmProviderKind}; use cm_config::{AppConfig, AuthMode, ConfigError, DeployTarget, LlmProviderKind};
const AIR_GAPPED_TOML: &str = r#" const AIR_GAPPED_TOML: &str = r#"
deploy_target = "air_gapped" deploy_target = "air_gapped"
listen_addr = "0.0.0.0:8080" listen_addr = "0.0.0.0:8080"
[database] [database]
url = "postgres://teamclaw:pw@db:5432/teamclaw" url = "postgres://clawmates:pw@db:5432/clawmates"
[llm] [llm]
provider = "openai_compat" provider = "openai_compat"
@@ -25,7 +25,7 @@ deploy_target = "cloud"
listen_addr = "0.0.0.0:8080" listen_addr = "0.0.0.0:8080"
[database] [database]
url = "postgres://teamclaw:pw@db:5432/teamclaw" url = "postgres://clawmates:pw@db:5432/clawmates"
max_connections = 32 max_connections = 32
[llm] [llm]
@@ -35,17 +35,20 @@ model = "claude-sonnet-4-5"
[auth] [auth]
mode = "oidc" mode = "oidc"
issuer_url = "https://idp.example.com" issuer_url = "https://idp.example.com"
client_id = "teamclaw" client_id = "clawmates"
"#; "#;
#[test] #[test]
fn loads_air_gapped_config_from_toml() { fn loads_air_gapped_config_from_toml() {
figment::Jail::expect_with(|jail| { figment::Jail::expect_with(|jail| {
jail.create_file("teamclaw.toml", AIR_GAPPED_TOML)?; jail.create_file("clawmates.toml", AIR_GAPPED_TOML)?;
let cfg = AppConfig::load_from(&jail.directory().join("teamclaw.toml")).unwrap(); let cfg = AppConfig::load_from(&jail.directory().join("clawmates.toml")).unwrap();
assert_eq!(cfg.deploy_target, DeployTarget::AirGapped); assert_eq!(cfg.deploy_target, DeployTarget::AirGapped);
assert_eq!(cfg.listen_addr.port(), 8080); assert_eq!(cfg.listen_addr.port(), 8080);
assert_eq!(cfg.database.url, "postgres://teamclaw:pw@db:5432/teamclaw"); assert_eq!(
cfg.database.url,
"postgres://clawmates:pw@db:5432/clawmates"
);
assert_eq!(cfg.llm.provider, LlmProviderKind::OpenAiCompat); assert_eq!(cfg.llm.provider, LlmProviderKind::OpenAiCompat);
assert_eq!( assert_eq!(
cfg.llm.base_url.as_deref(), cfg.llm.base_url.as_deref(),
@@ -59,8 +62,8 @@ fn loads_air_gapped_config_from_toml() {
#[test] #[test]
fn loads_cloud_config_with_oidc() { fn loads_cloud_config_with_oidc() {
figment::Jail::expect_with(|jail| { figment::Jail::expect_with(|jail| {
jail.create_file("teamclaw.toml", CLOUD_TOML)?; jail.create_file("clawmates.toml", CLOUD_TOML)?;
let cfg = AppConfig::load_from(&jail.directory().join("teamclaw.toml")).unwrap(); let cfg = AppConfig::load_from(&jail.directory().join("clawmates.toml")).unwrap();
assert_eq!(cfg.deploy_target, DeployTarget::Cloud); assert_eq!(cfg.deploy_target, DeployTarget::Cloud);
assert_eq!(cfg.database.max_connections, 32); assert_eq!(cfg.database.max_connections, 32);
assert_eq!(cfg.llm.provider, LlmProviderKind::Anthropic); assert_eq!(cfg.llm.provider, LlmProviderKind::Anthropic);
@@ -76,10 +79,10 @@ fn loads_cloud_config_with_oidc() {
#[test] #[test]
fn env_overrides_toml_values() { fn env_overrides_toml_values() {
figment::Jail::expect_with(|jail| { figment::Jail::expect_with(|jail| {
jail.create_file("teamclaw.toml", AIR_GAPPED_TOML)?; jail.create_file("clawmates.toml", AIR_GAPPED_TOML)?;
jail.set_env("TEAMCLAW_DATABASE__URL", "postgres://other:pw@x:5432/y"); jail.set_env("CLAWMATES_DATABASE__URL", "postgres://other:pw@x:5432/y");
jail.set_env("TEAMCLAW_LLM__MODEL", "llama-3.1-8b-instruct"); jail.set_env("CLAWMATES_LLM__MODEL", "llama-3.1-8b-instruct");
let cfg = AppConfig::load_from(&jail.directory().join("teamclaw.toml")).unwrap(); let cfg = AppConfig::load_from(&jail.directory().join("clawmates.toml")).unwrap();
assert_eq!(cfg.database.url, "postgres://other:pw@x:5432/y"); assert_eq!(cfg.database.url, "postgres://other:pw@x:5432/y");
assert_eq!(cfg.llm.model, "llama-3.1-8b-instruct"); assert_eq!(cfg.llm.model, "llama-3.1-8b-instruct");
Ok(()) Ok(())
@@ -89,8 +92,8 @@ fn env_overrides_toml_values() {
#[test] #[test]
fn database_max_connections_defaults_to_ten() { fn database_max_connections_defaults_to_ten() {
figment::Jail::expect_with(|jail| { figment::Jail::expect_with(|jail| {
jail.create_file("teamclaw.toml", AIR_GAPPED_TOML)?; jail.create_file("clawmates.toml", AIR_GAPPED_TOML)?;
let cfg = AppConfig::load_from(&jail.directory().join("teamclaw.toml")).unwrap(); let cfg = AppConfig::load_from(&jail.directory().join("clawmates.toml")).unwrap();
assert_eq!(cfg.database.max_connections, 10); assert_eq!(cfg.database.max_connections, 10);
Ok(()) Ok(())
}); });
@@ -100,8 +103,8 @@ fn database_max_connections_defaults_to_ten() {
fn openai_compat_without_base_url_is_rejected() { fn openai_compat_without_base_url_is_rejected() {
figment::Jail::expect_with(|jail| { figment::Jail::expect_with(|jail| {
let toml = AIR_GAPPED_TOML.replace("base_url = \"http://local-llm:8000/v1\"\n", ""); let toml = AIR_GAPPED_TOML.replace("base_url = \"http://local-llm:8000/v1\"\n", "");
jail.create_file("teamclaw.toml", &toml)?; jail.create_file("clawmates.toml", &toml)?;
let err = AppConfig::load_from(&jail.directory().join("teamclaw.toml")).unwrap_err(); let err = AppConfig::load_from(&jail.directory().join("clawmates.toml")).unwrap_err();
assert!(matches!(err, ConfigError::Invalid(msg) if msg.contains("base_url"))); assert!(matches!(err, ConfigError::Invalid(msg) if msg.contains("base_url")));
Ok(()) Ok(())
}); });
@@ -113,8 +116,8 @@ fn scripted_provider_requires_scenario_path() {
let toml = AIR_GAPPED_TOML let toml = AIR_GAPPED_TOML
.replace("provider = \"openai_compat\"", "provider = \"scripted\"") .replace("provider = \"openai_compat\"", "provider = \"scripted\"")
.replace("base_url = \"http://local-llm:8000/v1\"\n", ""); .replace("base_url = \"http://local-llm:8000/v1\"\n", "");
jail.create_file("teamclaw.toml", &toml)?; jail.create_file("clawmates.toml", &toml)?;
let err = AppConfig::load_from(&jail.directory().join("teamclaw.toml")).unwrap_err(); let err = AppConfig::load_from(&jail.directory().join("clawmates.toml")).unwrap_err();
assert!(matches!(err, ConfigError::Invalid(msg) if msg.contains("scenario_path"))); assert!(matches!(err, ConfigError::Invalid(msg) if msg.contains("scenario_path")));
Ok(()) Ok(())
}); });
@@ -124,8 +127,8 @@ fn scripted_provider_requires_scenario_path() {
fn oidc_mode_requires_issuer_and_client_id() { fn oidc_mode_requires_issuer_and_client_id() {
figment::Jail::expect_with(|jail| { figment::Jail::expect_with(|jail| {
let toml = CLOUD_TOML.replace("issuer_url = \"https://idp.example.com\"\n", ""); let toml = CLOUD_TOML.replace("issuer_url = \"https://idp.example.com\"\n", "");
jail.create_file("teamclaw.toml", &toml)?; jail.create_file("clawmates.toml", &toml)?;
let err = AppConfig::load_from(&jail.directory().join("teamclaw.toml")).unwrap_err(); let err = AppConfig::load_from(&jail.directory().join("clawmates.toml")).unwrap_err();
assert!(matches!(err, ConfigError::Invalid(msg) if msg.contains("issuer_url"))); assert!(matches!(err, ConfigError::Invalid(msg) if msg.contains("issuer_url")));
Ok(()) Ok(())
}); });
@@ -145,8 +148,8 @@ fn s3_backend_requires_endpoint_and_bucket() {
figment::Jail::expect_with(|jail| { figment::Jail::expect_with(|jail| {
let toml = let toml =
format!("{AIR_GAPPED_TOML}\n[storage]\ndata_dir = \"./data\"\nbackend = \"s3\"\n"); format!("{AIR_GAPPED_TOML}\n[storage]\ndata_dir = \"./data\"\nbackend = \"s3\"\n");
jail.create_file("teamclaw.toml", &toml)?; jail.create_file("clawmates.toml", &toml)?;
let err = AppConfig::load_from(&jail.directory().join("teamclaw.toml")).unwrap_err(); let err = AppConfig::load_from(&jail.directory().join("clawmates.toml")).unwrap_err();
assert!(matches!(err, ConfigError::Invalid(msg) if msg.contains("s3_endpoint"))); assert!(matches!(err, ConfigError::Invalid(msg) if msg.contains("s3_endpoint")));
Ok(()) Ok(())
}); });
@@ -156,16 +159,16 @@ fn s3_backend_requires_endpoint_and_bucket() {
fn clerk_mode_requires_the_instance_issuer() { fn clerk_mode_requires_the_instance_issuer() {
figment::Jail::expect_with(|jail| { figment::Jail::expect_with(|jail| {
let toml = AIR_GAPPED_TOML.replace("mode = \"local\"", "mode = \"clerk\""); let toml = AIR_GAPPED_TOML.replace("mode = \"local\"", "mode = \"clerk\"");
jail.create_file("teamclaw.toml", &toml)?; jail.create_file("clawmates.toml", &toml)?;
let err = AppConfig::load_from(&jail.directory().join("teamclaw.toml")).unwrap_err(); let err = AppConfig::load_from(&jail.directory().join("clawmates.toml")).unwrap_err();
assert!(matches!(err, ConfigError::Invalid(msg) if msg.contains("clerk"))); assert!(matches!(err, ConfigError::Invalid(msg) if msg.contains("clerk")));
let toml = toml.replace( let toml = toml.replace(
"mode = \"clerk\"", "mode = \"clerk\"",
"mode = \"clerk\"\nissuer_url = \"https://acme.clerk.accounts.dev\"", "mode = \"clerk\"\nissuer_url = \"https://acme.clerk.accounts.dev\"",
); );
jail.create_file("teamclaw.toml", &toml)?; jail.create_file("clawmates.toml", &toml)?;
let config = AppConfig::load_from(&jail.directory().join("teamclaw.toml")).unwrap(); let config = AppConfig::load_from(&jail.directory().join("clawmates.toml")).unwrap();
assert_eq!(config.auth.mode, AuthMode::Clerk); assert_eq!(config.auth.mode, AuthMode::Clerk);
Ok(()) Ok(())
}); });
@@ -1,5 +1,5 @@
[package] [package]
name = "tc-db" name = "cm-db"
version = "0.1.0" version = "0.1.0"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
@@ -10,13 +10,13 @@ publish.workspace = true
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
sqlx = { workspace = true } sqlx = { workspace = true }
tc-domain = { path = "../tc-domain" } cm-domain = { path = "../cm-domain" }
thiserror = { workspace = true } thiserror = { workspace = true }
time = { workspace = true } time = { workspace = true }
uuid = { workspace = true } uuid = { workspace = true }
[dev-dependencies] [dev-dependencies]
tc-testkit = { path = "../tc-testkit" } cm-testkit = { path = "../cm-testkit" }
tokio = { workspace = true } tokio = { workspace = true }
[lints] [lints]
@@ -1,8 +1,8 @@
//! Postgres persistence for TeamClaw. //! Postgres persistence for Clawmates.
//! //!
//! Queries are compile-time checked (`sqlx::query!`) against the schema in //! Queries are compile-time checked (`sqlx::query!`) against the schema in
//! `/migrations`, and every repository is tested only against a real //! `/migrations`, and every repository is tested only against a real
//! Postgres via `tc-testkit` — no in-memory store exists. //! Postgres via `cm-testkit` — no in-memory store exists.
pub mod repo; pub mod repo;
@@ -1,7 +1,7 @@
use sqlx::PgPool; use cm_domain::{
use tc_domain::{
AccessPolicy, Agent, AgentId, AgentScope, AgentStatus, HumanScope, UserId, WorkspaceId, AccessPolicy, Agent, AgentId, AgentScope, AgentStatus, HumanScope, UserId, WorkspaceId,
}; };
use sqlx::PgPool;
use uuid::Uuid; use uuid::Uuid;
use crate::DbError; use crate::DbError;
@@ -1,5 +1,5 @@
use cm_domain::{AgentId, UserId, WorkspaceId};
use sqlx::PgPool; use sqlx::PgPool;
use tc_domain::{AgentId, UserId, WorkspaceId};
use uuid::Uuid; use uuid::Uuid;
use crate::DbError; use crate::DbError;
@@ -1,5 +1,5 @@
use cm_domain::{AgentId, WorkspaceId};
use sqlx::PgPool; use sqlx::PgPool;
use tc_domain::{AgentId, WorkspaceId};
use uuid::Uuid; use uuid::Uuid;
use crate::DbError; use crate::DbError;
@@ -1,5 +1,5 @@
use cm_domain::WorkspaceId;
use sqlx::PgPool; use sqlx::PgPool;
use tc_domain::WorkspaceId;
use uuid::Uuid; use uuid::Uuid;
use crate::DbError; use crate::DbError;
@@ -1,5 +1,5 @@
use cm_domain::{AgentId, FileDrive, FileNode, WorkspaceId};
use sqlx::PgPool; use sqlx::PgPool;
use tc_domain::{AgentId, FileDrive, FileNode, WorkspaceId};
use uuid::Uuid; use uuid::Uuid;
use crate::DbError; use crate::DbError;
@@ -1,5 +1,5 @@
use cm_domain::{Message, MessageId, MessageRole, MessageWithSteps, SessionId, Step, StepStatus};
use sqlx::PgPool; use sqlx::PgPool;
use tc_domain::{Message, MessageId, MessageRole, MessageWithSteps, SessionId, Step, StepStatus};
use crate::DbError; use crate::DbError;
@@ -1,5 +1,5 @@
use cm_domain::AgentId;
use sqlx::PgPool; use sqlx::PgPool;
use tc_domain::AgentId;
use time::OffsetDateTime; use time::OffsetDateTime;
use uuid::Uuid; use uuid::Uuid;
@@ -1,5 +1,5 @@
use cm_domain::{AgentRun, RunState, SessionId};
use sqlx::PgPool; use sqlx::PgPool;
use tc_domain::{AgentRun, RunState, SessionId};
use uuid::Uuid; use uuid::Uuid;
use crate::DbError; use crate::DbError;
@@ -1,5 +1,5 @@
use cm_domain::{shard_of, AgentId, Session, SessionId, WorkspaceId};
use sqlx::PgPool; use sqlx::PgPool;
use tc_domain::{shard_of, AgentId, Session, SessionId, WorkspaceId};
use crate::DbError; use crate::DbError;
@@ -1,5 +1,5 @@
use cm_domain::{AgentId, UserId, WorkspaceId};
use sqlx::PgPool; use sqlx::PgPool;
use tc_domain::{AgentId, UserId, WorkspaceId};
use uuid::Uuid; use uuid::Uuid;
use crate::DbError; use crate::DbError;
@@ -1,5 +1,5 @@
use cm_domain::Step;
use sqlx::PgPool; use sqlx::PgPool;
use tc_domain::Step;
use crate::DbError; use crate::DbError;
@@ -1,5 +1,5 @@
use cm_domain::{AgentId, WorkspaceId};
use sqlx::PgPool; use sqlx::PgPool;
use tc_domain::{AgentId, WorkspaceId};
use time::OffsetDateTime; use time::OffsetDateTime;
use uuid::Uuid; use uuid::Uuid;
@@ -1,5 +1,5 @@
use cm_domain::{Role, User, UserId, WorkspaceId};
use sqlx::PgPool; use sqlx::PgPool;
use tc_domain::{Role, User, UserId, WorkspaceId};
use crate::DbError; use crate::DbError;
@@ -1,5 +1,5 @@
use cm_domain::{Workspace, WorkspaceId};
use sqlx::PgPool; use sqlx::PgPool;
use tc_domain::{Workspace, WorkspaceId};
use crate::DbError; use crate::DbError;
@@ -1,10 +1,10 @@
use serde_json::json; use cm_db::repo::{agents, messages, run_events, runs, sessions, steps, users, workspaces};
use tc_db::repo::{agents, messages, run_events, runs, sessions, steps, users, workspaces}; use cm_db::DbError;
use tc_db::DbError; use cm_domain::{
use tc_domain::{
AccessPolicy, Agent, AgentId, AgentStatus, MessageRole, Role, RunState, Session, SessionId, AccessPolicy, Agent, AgentId, AgentStatus, MessageRole, Role, RunState, Session, SessionId,
Step, StepStatus, User, UserId, Workspace, WorkspaceId, Step, StepStatus, User, UserId, Workspace, WorkspaceId,
}; };
use serde_json::json;
async fn seeded(pool: &sqlx::PgPool) -> (Workspace, User, Agent) { async fn seeded(pool: &sqlx::PgPool) -> (Workspace, User, Agent) {
let ws = Workspace { let ws = Workspace {
@@ -50,18 +50,18 @@ async fn new_session(pool: &sqlx::PgPool, agent: &Agent, title: &str) -> Session
#[tokio::test] #[tokio::test]
async fn session_create_get_and_shard_are_consistent() { async fn session_create_get_and_shard_are_consistent() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let (_, _, agent) = seeded(&pool).await; let (_, _, agent) = seeded(&pool).await;
let session = new_session(&pool, &agent, "Quarterly research").await; let session = new_session(&pool, &agent, "Quarterly research").await;
let fetched = sessions::get(&pool, session.id).await.unwrap(); let fetched = sessions::get(&pool, session.id).await.unwrap();
assert_eq!(fetched, session); assert_eq!(fetched, session);
assert_eq!(fetched.shard, tc_domain::shard_of(agent.id)); assert_eq!(fetched.shard, cm_domain::shard_of(agent.id));
} }
#[tokio::test] #[tokio::test]
async fn sessions_list_most_recently_active_first() { async fn sessions_list_most_recently_active_first() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let (_, _, agent) = seeded(&pool).await; let (_, _, agent) = seeded(&pool).await;
let older = new_session(&pool, &agent, "First").await; let older = new_session(&pool, &agent, "First").await;
let newer = new_session(&pool, &agent, "Second").await; let newer = new_session(&pool, &agent, "Second").await;
@@ -80,7 +80,7 @@ async fn sessions_list_most_recently_active_first() {
#[tokio::test] #[tokio::test]
async fn messages_get_increasing_seq_starting_at_one() { async fn messages_get_increasing_seq_starting_at_one() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let (_, _, agent) = seeded(&pool).await; let (_, _, agent) = seeded(&pool).await;
let session = new_session(&pool, &agent, "Chat").await; let session = new_session(&pool, &agent, "Chat").await;
@@ -102,7 +102,7 @@ async fn messages_get_increasing_seq_starting_at_one() {
#[tokio::test] #[tokio::test]
async fn history_returns_messages_with_ordered_steps() { async fn history_returns_messages_with_ordered_steps() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let (_, _, agent) = seeded(&pool).await; let (_, _, agent) = seeded(&pool).await;
let session = new_session(&pool, &agent, "Chat").await; let session = new_session(&pool, &agent, "Chat").await;
@@ -154,7 +154,7 @@ async fn history_returns_messages_with_ordered_steps() {
#[tokio::test] #[tokio::test]
async fn runs_track_state_transitions() { async fn runs_track_state_transitions() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let (_, _, agent) = seeded(&pool).await; let (_, _, agent) = seeded(&pool).await;
let session = new_session(&pool, &agent, "Chat").await; let session = new_session(&pool, &agent, "Chat").await;
@@ -177,7 +177,7 @@ async fn runs_track_state_transitions() {
#[tokio::test] #[tokio::test]
async fn run_events_replay_from_an_offset() { async fn run_events_replay_from_an_offset() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let (_, _, agent) = seeded(&pool).await; let (_, _, agent) = seeded(&pool).await;
let session = new_session(&pool, &agent, "Chat").await; let session = new_session(&pool, &agent, "Chat").await;
let run_id = runs::create(&pool, session.id).await.unwrap(); let run_id = runs::create(&pool, session.id).await.unwrap();
@@ -200,7 +200,7 @@ async fn run_events_replay_from_an_offset() {
#[tokio::test] #[tokio::test]
async fn duplicate_event_seq_is_a_conflict() { async fn duplicate_event_seq_is_a_conflict() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let (_, _, agent) = seeded(&pool).await; let (_, _, agent) = seeded(&pool).await;
let session = new_session(&pool, &agent, "Chat").await; let session = new_session(&pool, &agent, "Chat").await;
let run_id = runs::create(&pool, session.id).await.unwrap(); let run_id = runs::create(&pool, session.id).await.unwrap();
@@ -216,7 +216,7 @@ async fn duplicate_event_seq_is_a_conflict() {
#[tokio::test] #[tokio::test]
async fn missing_session_is_not_found() { async fn missing_session_is_not_found() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let err = sessions::get(&pool, SessionId::new()).await.unwrap_err(); let err = sessions::get(&pool, SessionId::new()).await.unwrap_err();
assert!(matches!(err, DbError::NotFound)); assert!(matches!(err, DbError::NotFound));
} }
@@ -1,8 +1,8 @@
use std::str::FromStr; use std::str::FromStr;
use tc_db::repo::{agents, audit, credits, users, workspaces}; use cm_db::repo::{agents, audit, credits, users, workspaces};
use tc_db::DbError; use cm_db::DbError;
use tc_domain::{ use cm_domain::{
AccessPolicy, Agent, AgentId, AgentScope, AgentStatus, HumanScope, Role, User, UserId, AccessPolicy, Agent, AgentId, AgentScope, AgentStatus, HumanScope, Role, User, UserId,
Workspace, WorkspaceId, Workspace, WorkspaceId,
}; };
@@ -43,7 +43,7 @@ fn agent_in(ws: &Workspace, owner: &User) -> Agent {
#[tokio::test] #[tokio::test]
async fn workspace_round_trips() { async fn workspace_round_trips() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let ws = workspace(); let ws = workspace();
workspaces::insert(&pool, &ws).await.unwrap(); workspaces::insert(&pool, &ws).await.unwrap();
let found = workspaces::get(&pool, ws.id).await.unwrap(); let found = workspaces::get(&pool, ws.id).await.unwrap();
@@ -52,7 +52,7 @@ async fn workspace_round_trips() {
#[tokio::test] #[tokio::test]
async fn missing_workspace_is_not_found() { async fn missing_workspace_is_not_found() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let err = workspaces::get(&pool, WorkspaceId::new()) let err = workspaces::get(&pool, WorkspaceId::new())
.await .await
.unwrap_err(); .unwrap_err();
@@ -61,7 +61,7 @@ async fn missing_workspace_is_not_found() {
#[tokio::test] #[tokio::test]
async fn user_round_trips_and_finds_by_email() { async fn user_round_trips_and_finds_by_email() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let ws = workspace(); let ws = workspace();
workspaces::insert(&pool, &ws).await.unwrap(); workspaces::insert(&pool, &ws).await.unwrap();
let user = user_in(&ws, Role::Owner); let user = user_in(&ws, Role::Owner);
@@ -77,7 +77,7 @@ async fn user_round_trips_and_finds_by_email() {
#[tokio::test] #[tokio::test]
async fn duplicate_email_is_a_conflict() { async fn duplicate_email_is_a_conflict() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let ws = workspace(); let ws = workspace();
workspaces::insert(&pool, &ws).await.unwrap(); workspaces::insert(&pool, &ws).await.unwrap();
let mut a = user_in(&ws, Role::Member); let mut a = user_in(&ws, Role::Member);
@@ -92,7 +92,7 @@ async fn duplicate_email_is_a_conflict() {
#[tokio::test] #[tokio::test]
async fn workspace_members_lists_in_join_order() { async fn workspace_members_lists_in_join_order() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let ws = workspace(); let ws = workspace();
workspaces::insert(&pool, &ws).await.unwrap(); workspaces::insert(&pool, &ws).await.unwrap();
let owner = user_in(&ws, Role::Owner); let owner = user_in(&ws, Role::Owner);
@@ -108,7 +108,7 @@ async fn workspace_members_lists_in_join_order() {
#[tokio::test] #[tokio::test]
async fn agent_insert_creates_default_access_policy() { async fn agent_insert_creates_default_access_policy() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let ws = workspace(); let ws = workspace();
workspaces::insert(&pool, &ws).await.unwrap(); workspaces::insert(&pool, &ws).await.unwrap();
let owner = user_in(&ws, Role::Owner); let owner = user_in(&ws, Role::Owner);
@@ -126,7 +126,7 @@ async fn agent_insert_creates_default_access_policy() {
#[tokio::test] #[tokio::test]
async fn agent_roster_excludes_deleted_and_round_trips_fields() { async fn agent_roster_excludes_deleted_and_round_trips_fields() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let ws = workspace(); let ws = workspace();
workspaces::insert(&pool, &ws).await.unwrap(); workspaces::insert(&pool, &ws).await.unwrap();
let owner = user_in(&ws, Role::Owner); let owner = user_in(&ws, Role::Owner);
@@ -149,7 +149,7 @@ async fn agent_roster_excludes_deleted_and_round_trips_fields() {
#[tokio::test] #[tokio::test]
async fn agent_status_updates() { async fn agent_status_updates() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let ws = workspace(); let ws = workspace();
workspaces::insert(&pool, &ws).await.unwrap(); workspaces::insert(&pool, &ws).await.unwrap();
let owner = user_in(&ws, Role::Owner); let owner = user_in(&ws, Role::Owner);
@@ -168,7 +168,7 @@ async fn agent_status_updates() {
#[tokio::test] #[tokio::test]
async fn specific_access_policy_round_trips() { async fn specific_access_policy_round_trips() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let ws = workspace(); let ws = workspace();
workspaces::insert(&pool, &ws).await.unwrap(); workspaces::insert(&pool, &ws).await.unwrap();
let owner = user_in(&ws, Role::Owner); let owner = user_in(&ws, Role::Owner);
@@ -186,7 +186,7 @@ async fn specific_access_policy_round_trips() {
#[tokio::test] #[tokio::test]
async fn credit_balance_sums_remaining_lots() { async fn credit_balance_sums_remaining_lots() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let ws = workspace(); let ws = workspace();
workspaces::insert(&pool, &ws).await.unwrap(); workspaces::insert(&pool, &ws).await.unwrap();
@@ -200,7 +200,7 @@ async fn credit_balance_sums_remaining_lots() {
#[tokio::test] #[tokio::test]
async fn audit_log_appends_and_rejects_mutation() { async fn audit_log_appends_and_rejects_mutation() {
let pool = tc_testkit::test_pool().await; let pool = cm_testkit::test_pool().await;
let ws = workspace(); let ws = workspace();
workspaces::insert(&pool, &ws).await.unwrap(); workspaces::insert(&pool, &ws).await.unwrap();
@@ -1,5 +1,5 @@
[package] [package]
name = "tc-domain" name = "cm-domain"
version = "0.1.0" version = "0.1.0"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
@@ -1,4 +1,4 @@
//! Core domain types for the TeamClaw platform (spec §14). //! Core domain types for the Clawmates platform (spec §14).
//! //!
//! This crate has no I/O: every other crate depends on it for identifiers, //! This crate has no I/O: every other crate depends on it for identifiers,
//! the session-key codec, role/RBAC vocabulary, agent access policies, and //! the session-key codec, role/RBAC vocabulary, agent access policies, and
@@ -1,6 +1,6 @@
use std::str::FromStr; use std::str::FromStr;
use tc_domain::{AccessPolicy, AgentId, AgentScope, GatedCategory, HumanScope, Role, UserId}; use cm_domain::{AccessPolicy, AgentId, AgentScope, GatedCategory, HumanScope, Role, UserId};
#[test] #[test]
fn ids_display_as_uuid_and_parse_back() { fn ids_display_as_uuid_and_parse_back() {
@@ -1,6 +1,6 @@
use std::str::FromStr; use std::str::FromStr;
use tc_domain::{AgentId, MessageId, SessionId, SessionKey, SessionKeyError}; use cm_domain::{AgentId, MessageId, SessionId, SessionKey, SessionKeyError};
fn sample() -> SessionKey { fn sample() -> SessionKey {
SessionKey { SessionKey {
@@ -1,5 +1,5 @@
[package] [package]
name = "tc-files" name = "cm-files"
version = "0.1.0" version = "0.1.0"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
@@ -1,4 +1,4 @@
use tc_files::{BlobError, BlobStore, LocalBlobStore}; use cm_files::{BlobError, BlobStore, LocalBlobStore};
fn store() -> (LocalBlobStore, std::path::PathBuf) { fn store() -> (LocalBlobStore, std::path::PathBuf) {
let root = std::env::temp_dir().join(format!("tc-blobs-{}", uuid::Uuid::now_v7())); let root = std::env::temp_dir().join(format!("tc-blobs-{}", uuid::Uuid::now_v7()));
@@ -5,7 +5,7 @@ use testcontainers_modules::testcontainers::core::{ExecCommand, IntoContainerPor
use testcontainers_modules::testcontainers::runners::AsyncRunner; use testcontainers_modules::testcontainers::runners::AsyncRunner;
use testcontainers_modules::testcontainers::{GenericImage, ImageExt}; use testcontainers_modules::testcontainers::{GenericImage, ImageExt};
use tc_files::{BlobError, BlobStore, S3BlobStore}; use cm_files::{BlobError, BlobStore, S3BlobStore};
async fn minio_store() -> ( async fn minio_store() -> (
S3BlobStore, S3BlobStore,
@@ -25,7 +25,7 @@ async fn minio_store() -> (
.exec(ExecCommand::new([ .exec(ExecCommand::new([
"sh", "sh",
"-c", "-c",
"mc alias set local http://127.0.0.1:9000 tc-access tc-secret-key && mc mb local/teamclaw", "mc alias set local http://127.0.0.1:9000 tc-access tc-secret-key && mc mb local/clawmates",
])) ]))
.await .await
.expect("bucket created"); .expect("bucket created");
@@ -33,7 +33,7 @@ async fn minio_store() -> (
// The bucket is created asynchronously after boot; retry connect+probe. // The bucket is created asynchronously after boot; retry connect+probe.
let store = S3BlobStore::connect( let store = S3BlobStore::connect(
&format!("http://127.0.0.1:{port}"), &format!("http://127.0.0.1:{port}"),
"teamclaw", "clawmates",
"tc-access", "tc-access",
"tc-secret-key", "tc-secret-key",
) )
@@ -1,5 +1,5 @@
[package] [package]
name = "tc-llm" name = "cm-llm"
version = "0.1.0" version = "0.1.0"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
@@ -1,4 +1,4 @@
//! LLM access for TeamClaw. One provider trait, three shipping //! LLM access for Clawmates. One provider trait, three shipping
//! implementations selected by configuration: //! implementations selected by configuration:
//! //!
//! - `anthropic` — Anthropic Messages API (cloud target) //! - `anthropic` — Anthropic Messages API (cloud target)
@@ -1,18 +1,18 @@
//! Opt-in tests against real inference endpoints. Activated with //! Opt-in tests against real inference endpoints. Activated with
//! `TC_LIVE_LLM=1` plus `ANTHROPIC_API_KEY` (Anthropic) or //! `CM_LIVE_LLM=1` plus `ANTHROPIC_API_KEY` (Anthropic) or
//! `TC_OPENAI_COMPAT_URL` (e.g. a local Ollama at //! `TC_OPENAI_COMPAT_URL` (e.g. a local Ollama at
//! `http://127.0.0.1:11434/v1` with `TC_OPENAI_COMPAT_MODEL` set). //! `http://127.0.0.1:11434/v1` with `TC_OPENAI_COMPAT_MODEL` set).
//! CI runs these in a dedicated credentialed job; the default suite uses //! CI runs these in a dedicated credentialed job; the default suite uses
//! the scripted provider, which exercises the identical seam. //! the scripted provider, which exercises the identical seam.
use futures::StreamExt; use cm_llm::{
use tc_llm::{
AnthropicProvider, ChatMessage, ChatRequest, ChatRole, ContentPart, LlmEvent, LlmProvider, AnthropicProvider, ChatMessage, ChatRequest, ChatRole, ContentPart, LlmEvent, LlmProvider,
OpenAiCompatProvider, OpenAiCompatProvider,
}; };
use futures::StreamExt;
fn live_enabled() -> bool { fn live_enabled() -> bool {
std::env::var("TC_LIVE_LLM").as_deref() == Ok("1") std::env::var("CM_LIVE_LLM").as_deref() == Ok("1")
} }
fn simple_request(model: &str) -> ChatRequest { fn simple_request(model: &str) -> ChatRequest {
@@ -42,7 +42,7 @@ async fn collect_text(provider: &dyn LlmProvider, request: ChatRequest) -> Strin
#[tokio::test] #[tokio::test]
async fn anthropic_streams_text() { async fn anthropic_streams_text() {
if !live_enabled() { if !live_enabled() {
eprintln!("skipped: set TC_LIVE_LLM=1 to run"); eprintln!("skipped: set CM_LIVE_LLM=1 to run");
return; return;
} }
let Ok(key) = std::env::var("ANTHROPIC_API_KEY") else { let Ok(key) = std::env::var("ANTHROPIC_API_KEY") else {
@@ -57,7 +57,7 @@ async fn anthropic_streams_text() {
#[tokio::test] #[tokio::test]
async fn openai_compat_streams_text() { async fn openai_compat_streams_text() {
if !live_enabled() { if !live_enabled() {
eprintln!("skipped: set TC_LIVE_LLM=1 to run"); eprintln!("skipped: set CM_LIVE_LLM=1 to run");
return; return;
} }
let Ok(url) = std::env::var("TC_OPENAI_COMPAT_URL") else { let Ok(url) = std::env::var("TC_OPENAI_COMPAT_URL") else {

Some files were not shown because too many files have changed in this diff Show More