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:
co-authored by
Claude Fable 5
parent
8046853feb
commit
add4f79fed
+3
-3
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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]
|
||||||
+14
-14
@@ -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
|
||||||
@@ -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(())
|
||||||
}
|
}
|
||||||
+34
-31
@@ -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}"))
|
||||||
@@ -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
Reference in New Issue
Block a user