Merge pull request 'feat: full copy from apollo_nxt-trcaa with complete sanitization' (#69) from feature/full-copy-from-trcaa into master
Some checks failed
Auto Tag / autotag (push) Successful in 5s
Auto Tag / wiki-sync (push) Successful in 8s
Build CI Docker Images / linux-amd64 (push) Successful in 9s
Build CI Docker Images / linux-arm64 (push) Successful in 13s
Test / frontend-tests (push) Successful in 1m16s
Test / frontend-typecheck (push) Successful in 1m28s
Build CI Docker Images / windows-cross (push) Successful in 1m41s
Auto Tag / changelog (push) Successful in 1m47s
Auto Tag / build-linux-amd64 (push) Successful in 9m21s
Auto Tag / build-macos-arm64 (push) Successful in 12m23s
Auto Tag / build-windows-amd64 (push) Successful in 11m18s
Auto Tag / build-linux-arm64 (push) Successful in 10m39s
Test / rust-fmt-check (push) Successful in 15m7s
Test / rust-clippy (push) Successful in 16m47s
Test / rust-tests (push) Successful in 18m24s
Renovate / renovate (push) Failing after 12s
Reviewed-on: #69
@ -24,5 +24,9 @@ RUN apt-get update -qq \
|
||||
&& apt-get install -y --no-install-recommends nodejs \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# tea (Gitea CLI) can be installed if needed:
|
||||
# RUN curl -sL https://dl.gitea.com/tea/master/tea-master-linux-amd64 -o /usr/local/bin/tea \
|
||||
# && chmod +x /usr/local/bin/tea
|
||||
|
||||
RUN rustup target add x86_64-unknown-linux-gnu \
|
||||
&& rustup component add rustfmt clippy
|
||||
|
||||
@ -39,7 +39,12 @@ RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
|
||||
&& apt-get install -y --no-install-recommends nodejs \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Step 4: Rust 1.88 with arm64 cross-compilation target
|
||||
# Step 4: GitHub CLI
|
||||
# tea (Gitea CLI) can be installed if needed:
|
||||
# RUN curl -sL https://dl.gitea.com/tea/master/tea-master-linux-arm64 -o /usr/local/bin/tea \
|
||||
# && chmod +x /usr/local/bin/tea
|
||||
|
||||
# Step 5: Rust 1.88 with arm64 cross-compilation target
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \
|
||||
--default-toolchain 1.88.0 --profile minimal --no-modify-path \
|
||||
&& /root/.cargo/bin/rustup target add aarch64-unknown-linux-gnu \
|
||||
|
||||
@ -20,4 +20,24 @@ RUN apt-get update -qq \
|
||||
&& apt-get install -y --no-install-recommends nodejs \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# tea (Gitea CLI) can be installed if needed:
|
||||
# RUN curl -sL https://dl.gitea.com/tea/master/tea-master-linux-amd64 -o /usr/local/bin/tea \
|
||||
# && chmod +x /usr/local/bin/tea
|
||||
|
||||
# Pre-build libsodium for x86_64-pc-windows-gnu so libsodium-sys-stable
|
||||
# does not attempt a network download at cargo build time (no DNS in CI containers).
|
||||
RUN set -eu \
|
||||
&& SODIUM_VER="1.0.20" \
|
||||
&& curl -fsSL "https://download.libsodium.org/libsodium/releases/libsodium-${SODIUM_VER}.tar.gz" \
|
||||
| tar -xz -C /tmp \
|
||||
&& cd "/tmp/libsodium-${SODIUM_VER}" \
|
||||
&& ./configure \
|
||||
--host=x86_64-w64-mingw32 \
|
||||
--prefix=/usr/x86_64-w64-mingw32 \
|
||||
--disable-shared \
|
||||
--enable-static \
|
||||
&& make -j"$(nproc)" \
|
||||
&& make install \
|
||||
&& rm -rf "/tmp/libsodium-${SODIUM_VER}"
|
||||
|
||||
RUN rustup target add x86_64-pc-windows-gnu
|
||||
|
||||
@ -307,7 +307,7 @@ jobs:
|
||||
needs: autotag
|
||||
runs-on: linux-amd64
|
||||
container:
|
||||
image: 172.0.0.29:3000/sarman/trcaa-linux-amd64:rust1.88-node22
|
||||
image: 172.0.0.29:3000/sarman/tftsr-linux-amd64:rust1.88-node22
|
||||
steps:
|
||||
- name: Checkout
|
||||
run: |
|
||||
@ -402,7 +402,7 @@ jobs:
|
||||
needs: autotag
|
||||
runs-on: linux-amd64
|
||||
container:
|
||||
image: 172.0.0.29:3000/sarman/trcaa-windows-cross:rust1.88-node22
|
||||
image: 172.0.0.29:3000/sarman/tftsr-windows-cross:rust1.88-node22
|
||||
steps:
|
||||
- name: Checkout
|
||||
run: |
|
||||
@ -586,7 +586,7 @@ jobs:
|
||||
needs: autotag
|
||||
runs-on: linux-amd64
|
||||
container:
|
||||
image: 172.0.0.29:3000/sarman/trcaa-linux-arm64:rust1.88-node22
|
||||
image: 172.0.0.29:3000/sarman/tftsr-linux-arm64:rust1.88-node22
|
||||
steps:
|
||||
- name: Checkout
|
||||
run: |
|
||||
|
||||
@ -13,9 +13,9 @@ name: Build CI Docker Images
|
||||
# sudo systemctl restart docker
|
||||
#
|
||||
# Images produced:
|
||||
# 172.0.0.29:3000/sarman/trcaa-linux-amd64:rust1.88-node22
|
||||
# 172.0.0.29:3000/sarman/trcaa-windows-cross:rust1.88-node22
|
||||
# 172.0.0.29:3000/sarman/trcaa-linux-arm64:rust1.88-node22
|
||||
# 172.0.0.29:3000/sarman/tftsr-linux-amd64:rust1.88-node22
|
||||
# 172.0.0.29:3000/sarman/tftsr-windows-cross:rust1.88-node22
|
||||
# 172.0.0.29:3000/sarman/tftsr-linux-arm64:rust1.88-node22
|
||||
|
||||
on:
|
||||
push:
|
||||
@ -52,10 +52,10 @@ jobs:
|
||||
run: |
|
||||
echo "$RELEASE_TOKEN" | docker login $REGISTRY -u $REGISTRY_USER --password-stdin
|
||||
docker build \
|
||||
-t $REGISTRY/$REGISTRY_USER/trcaa-linux-amd64:rust1.88-node22 \
|
||||
-t $REGISTRY/$REGISTRY_USER/tftsr-linux-amd64:rust1.88-node22 \
|
||||
-f .docker/Dockerfile.linux-amd64 .
|
||||
docker push $REGISTRY/$REGISTRY_USER/trcaa-linux-amd64:rust1.88-node22
|
||||
echo "✓ Pushed $REGISTRY/$REGISTRY_USER/trcaa-linux-amd64:rust1.88-node22"
|
||||
docker push $REGISTRY/$REGISTRY_USER/tftsr-linux-amd64:rust1.88-node22
|
||||
echo "✓ Pushed $REGISTRY/$REGISTRY_USER/tftsr-linux-amd64:rust1.88-node22"
|
||||
|
||||
windows-cross:
|
||||
runs-on: linux-amd64
|
||||
@ -75,10 +75,10 @@ jobs:
|
||||
run: |
|
||||
echo "$RELEASE_TOKEN" | docker login $REGISTRY -u $REGISTRY_USER --password-stdin
|
||||
docker build \
|
||||
-t $REGISTRY/$REGISTRY_USER/trcaa-windows-cross:rust1.88-node22 \
|
||||
-t $REGISTRY/$REGISTRY_USER/tftsr-windows-cross:rust1.88-node22 \
|
||||
-f .docker/Dockerfile.windows-cross .
|
||||
docker push $REGISTRY/$REGISTRY_USER/trcaa-windows-cross:rust1.88-node22
|
||||
echo "✓ Pushed $REGISTRY/$REGISTRY_USER/trcaa-windows-cross:rust1.88-node22"
|
||||
docker push $REGISTRY/$REGISTRY_USER/tftsr-windows-cross:rust1.88-node22
|
||||
echo "✓ Pushed $REGISTRY/$REGISTRY_USER/tftsr-windows-cross:rust1.88-node22"
|
||||
|
||||
linux-arm64:
|
||||
runs-on: linux-amd64
|
||||
@ -98,7 +98,7 @@ jobs:
|
||||
run: |
|
||||
echo "$RELEASE_TOKEN" | docker login $REGISTRY -u $REGISTRY_USER --password-stdin
|
||||
docker build \
|
||||
-t $REGISTRY/$REGISTRY_USER/trcaa-linux-arm64:rust1.88-node22 \
|
||||
-t $REGISTRY/$REGISTRY_USER/tftsr-linux-arm64:rust1.88-node22 \
|
||||
-f .docker/Dockerfile.linux-arm64 .
|
||||
docker push $REGISTRY/$REGISTRY_USER/trcaa-linux-arm64:rust1.88-node22
|
||||
echo "✓ Pushed $REGISTRY/$REGISTRY_USER/trcaa-linux-arm64:rust1.88-node22"
|
||||
docker push $REGISTRY/$REGISTRY_USER/tftsr-linux-arm64:rust1.88-node22
|
||||
echo "✓ Pushed $REGISTRY/$REGISTRY_USER/tftsr-linux-arm64:rust1.88-node22"
|
||||
|
||||
@ -10,7 +10,7 @@ jobs:
|
||||
rust-fmt-check:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: 172.0.0.29:3000/sarman/trcaa-linux-amd64:rust1.88-node22
|
||||
image: rustlang/rust:nightly
|
||||
steps:
|
||||
- name: Checkout
|
||||
run: |
|
||||
@ -30,16 +30,23 @@ jobs:
|
||||
echo "Fetched fallback ref: master"
|
||||
fi
|
||||
git checkout FETCH_HEAD
|
||||
- name: Cache cargo registry
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry/index
|
||||
~/.cargo/registry/cache
|
||||
~/.cargo/git/db
|
||||
key: ${{ runner.os }}-cargo-linux-amd64-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cargo-linux-amd64-
|
||||
- name: Install Node.js
|
||||
run: |
|
||||
apt-get update && apt-get install -y curl
|
||||
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
|
||||
apt-get install -y nodejs
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
apt-get update && apt-get install -y \
|
||||
libwebkit2gtk-4.1-dev \
|
||||
libssl-dev \
|
||||
libgtk-3-dev \
|
||||
libayatana-appindicator3-dev \
|
||||
librsvg2-dev \
|
||||
libdbus-1-dev \
|
||||
pkg-config
|
||||
- name: Install Rust components
|
||||
run: rustup component add rustfmt
|
||||
- name: Install dependencies
|
||||
run: npm install --legacy-peer-deps
|
||||
- name: Update version from Git
|
||||
@ -50,7 +57,7 @@ jobs:
|
||||
rust-clippy:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: 172.0.0.29:3000/sarman/trcaa-linux-amd64:rust1.88-node22
|
||||
image: rustlang/rust:nightly
|
||||
steps:
|
||||
- name: Checkout
|
||||
run: |
|
||||
@ -70,22 +77,24 @@ jobs:
|
||||
echo "Fetched fallback ref: master"
|
||||
fi
|
||||
git checkout FETCH_HEAD
|
||||
- name: Cache cargo registry
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry/index
|
||||
~/.cargo/registry/cache
|
||||
~/.cargo/git/db
|
||||
key: ${{ runner.os }}-cargo-linux-amd64-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cargo-linux-amd64-
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
apt-get update && apt-get install -y \
|
||||
libwebkit2gtk-4.1-dev \
|
||||
libssl-dev \
|
||||
libgtk-3-dev \
|
||||
libayatana-appindicator3-dev \
|
||||
librsvg2-dev \
|
||||
libdbus-1-dev \
|
||||
pkg-config
|
||||
- name: Install clippy
|
||||
run: rustup component add clippy
|
||||
- run: cargo clippy --manifest-path src-tauri/Cargo.toml -- -D warnings
|
||||
|
||||
rust-tests:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: 172.0.0.29:3000/sarman/trcaa-linux-amd64:rust1.88-node22
|
||||
image: rustlang/rust:nightly
|
||||
steps:
|
||||
- name: Checkout
|
||||
run: |
|
||||
@ -105,16 +114,16 @@ jobs:
|
||||
echo "Fetched fallback ref: master"
|
||||
fi
|
||||
git checkout FETCH_HEAD
|
||||
- name: Cache cargo registry
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry/index
|
||||
~/.cargo/registry/cache
|
||||
~/.cargo/git/db
|
||||
key: ${{ runner.os }}-cargo-linux-amd64-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-cargo-linux-amd64-
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
apt-get update && apt-get install -y \
|
||||
libwebkit2gtk-4.1-dev \
|
||||
libssl-dev \
|
||||
libgtk-3-dev \
|
||||
libayatana-appindicator3-dev \
|
||||
librsvg2-dev \
|
||||
libdbus-1-dev \
|
||||
pkg-config
|
||||
- run: cargo test --manifest-path src-tauri/Cargo.toml -- --test-threads=1
|
||||
|
||||
- name: Run shell module tests
|
||||
|
||||
95
.github/workflows/build-images.yml
vendored
@ -1,95 +0,0 @@
|
||||
name: Build CI Docker Images
|
||||
|
||||
# Rebuilds the pre-baked builder images and pushes them to the local Gitea
|
||||
# container registry (172.0.0.29:3000).
|
||||
#
|
||||
# WHEN TO RUN:
|
||||
# - Automatically: whenever a Dockerfile under .docker/ changes on master.
|
||||
# - Manually: via workflow_dispatch (e.g. first-time setup, forced rebuild).
|
||||
#
|
||||
# ONE-TIME SERVER PREREQUISITE (run once on 172.0.0.29 before first use):
|
||||
# echo '{"insecure-registries":["172.0.0.29:3000"]}' \
|
||||
# | sudo tee /etc/docker/daemon.json
|
||||
# sudo systemctl restart docker
|
||||
#
|
||||
# Images produced:
|
||||
# 172.0.0.29:3000/sarman/trcaa-linux-amd64:rust1.88-node22
|
||||
# 172.0.0.29:3000/sarman/trcaa-windows-cross:rust1.88-node22
|
||||
# 172.0.0.29:3000/sarman/trcaa-linux-arm64:rust1.88-node22
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- '.docker/**'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: build-ci-images
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
REGISTRY: 172.0.0.29:3000
|
||||
REGISTRY_USER: sarman
|
||||
|
||||
jobs:
|
||||
linux-amd64:
|
||||
runs-on: linux-amd64
|
||||
container:
|
||||
image: docker:24-cli
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Build and push linux-amd64 builder
|
||||
env:
|
||||
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
run: |
|
||||
echo "$RELEASE_TOKEN" | docker login $REGISTRY -u $REGISTRY_USER --password-stdin
|
||||
docker build \
|
||||
-t $REGISTRY/$REGISTRY_USER/trcaa-linux-amd64:rust1.88-node22 \
|
||||
-f .docker/Dockerfile.linux-amd64 .
|
||||
docker push $REGISTRY/$REGISTRY_USER/trcaa-linux-amd64:rust1.88-node22
|
||||
echo "✓ Pushed $REGISTRY/$REGISTRY_USER/trcaa-linux-amd64:rust1.88-node22"
|
||||
|
||||
windows-cross:
|
||||
runs-on: linux-amd64
|
||||
container:
|
||||
image: docker:24-cli
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Build and push windows-cross builder
|
||||
env:
|
||||
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
run: |
|
||||
echo "$RELEASE_TOKEN" | docker login $REGISTRY -u $REGISTRY_USER --password-stdin
|
||||
docker build \
|
||||
-t $REGISTRY/$REGISTRY_USER/trcaa-windows-cross:rust1.88-node22 \
|
||||
-f .docker/Dockerfile.windows-cross .
|
||||
docker push $REGISTRY/$REGISTRY_USER/trcaa-windows-cross:rust1.88-node22
|
||||
echo "✓ Pushed $REGISTRY/$REGISTRY_USER/trcaa-windows-cross:rust1.88-node22"
|
||||
|
||||
linux-arm64:
|
||||
runs-on: linux-amd64
|
||||
container:
|
||||
image: docker:24-cli
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Build and push linux-arm64 builder
|
||||
env:
|
||||
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
run: |
|
||||
echo "$RELEASE_TOKEN" | docker login $REGISTRY -u $REGISTRY_USER --password-stdin
|
||||
docker build \
|
||||
-t $REGISTRY/$REGISTRY_USER/trcaa-linux-arm64:rust1.88-node22 \
|
||||
-f .docker/Dockerfile.linux-arm64 .
|
||||
docker push $REGISTRY/$REGISTRY_USER/trcaa-linux-arm64:rust1.88-node22
|
||||
echo "✓ Pushed $REGISTRY/$REGISTRY_USER/trcaa-linux-arm64:rust1.88-node22"
|
||||
504
.github/workflows/release.yml
vendored
@ -1,504 +0,0 @@
|
||||
name: Auto Tag
|
||||
|
||||
# Runs on every merge to master — reads the latest semver tag, increments
|
||||
# the patch version, pushes a new tag, then runs release builds in this workflow.
|
||||
# workflow_dispatch allows manual triggering when Gitea drops a push event.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: auto-tag-master
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
autotag:
|
||||
runs-on: linux-amd64
|
||||
container:
|
||||
image: alpine:latest
|
||||
steps:
|
||||
- name: Bump patch version and create tag
|
||||
id: bump
|
||||
env:
|
||||
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
run: |
|
||||
set -eu
|
||||
apk add --no-cache curl jq git
|
||||
|
||||
API="http://172.0.0.29:3000/api/v1/repos/$GITHUB_REPOSITORY"
|
||||
|
||||
# Get the latest clean semver tag (vX.Y.Z only, ignore rc/test suffixes)
|
||||
LATEST=$(curl -s "$API/tags?limit=50" \
|
||||
-H "Authorization: token $RELEASE_TOKEN" | \
|
||||
jq -r '.[].name' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | \
|
||||
sort -V | tail -1)
|
||||
|
||||
if [ -z "$LATEST" ]; then
|
||||
NEXT="v0.1.0"
|
||||
else
|
||||
MAJOR=$(echo "$LATEST" | cut -d. -f1 | tr -d 'v')
|
||||
MINOR=$(echo "$LATEST" | cut -d. -f2)
|
||||
PATCH=$(echo "$LATEST" | cut -d. -f3)
|
||||
NEXT="v${MAJOR}.${MINOR}.$((PATCH + 1))"
|
||||
fi
|
||||
|
||||
echo "Latest tag: ${LATEST:-none} → Next: $NEXT"
|
||||
|
||||
# Create and push the tag via git.
|
||||
git init
|
||||
git remote add origin "http://oauth2:${RELEASE_TOKEN}@172.0.0.29:3000/${GITHUB_REPOSITORY}.git"
|
||||
git fetch --depth=1 origin "$GITHUB_SHA"
|
||||
git checkout FETCH_HEAD
|
||||
git config user.name "gitea-actions[bot]"
|
||||
git config user.email "gitea-actions@local"
|
||||
|
||||
if git ls-remote --exit-code --tags origin "refs/tags/$NEXT" >/dev/null 2>&1; then
|
||||
echo "Tag $NEXT already exists; skipping."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git tag -a "$NEXT" -m "Release $NEXT"
|
||||
git push origin "refs/tags/$NEXT"
|
||||
|
||||
echo "Tag $NEXT pushed successfully"
|
||||
|
||||
wiki-sync:
|
||||
runs-on: linux-amd64
|
||||
container:
|
||||
image: alpine:latest
|
||||
steps:
|
||||
- name: Install dependencies
|
||||
run: apk add --no-cache git
|
||||
|
||||
- name: Checkout main repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Configure git
|
||||
run: |
|
||||
git config --global user.email "actions@gitea.local"
|
||||
git config --global user.name "Gitea Actions"
|
||||
git config --global credential.helper ''
|
||||
|
||||
- name: Clone and sync wiki
|
||||
env:
|
||||
WIKI_TOKEN: ${{ secrets.Wiki }}
|
||||
run: |
|
||||
cd /tmp
|
||||
if [ -n "$WIKI_TOKEN" ]; then
|
||||
WIKI_URL="http://${WIKI_TOKEN}@172.0.0.29:3000/sarman/tftsr-devops_investigation.wiki.git"
|
||||
else
|
||||
WIKI_URL="http://172.0.0.29:3000/sarman/tftsr-devops_investigation.wiki.git"
|
||||
fi
|
||||
|
||||
if ! git clone "$WIKI_URL" wiki 2>/dev/null; then
|
||||
echo "Wiki doesn't exist yet, creating initial structure..."
|
||||
mkdir -p wiki
|
||||
cd wiki
|
||||
git init
|
||||
git checkout -b master
|
||||
echo "# Wiki" > Home.md
|
||||
git add Home.md
|
||||
git commit -m "Initial wiki commit"
|
||||
git remote add origin "$WIKI_URL"
|
||||
fi
|
||||
|
||||
cd /tmp/wiki
|
||||
if [ -d "$GITHUB_WORKSPACE/docs/wiki" ]; then
|
||||
cp -v "$GITHUB_WORKSPACE"/docs/wiki/*.md . 2>/dev/null || echo "No wiki files to copy"
|
||||
fi
|
||||
|
||||
git add -A
|
||||
if ! git diff --staged --quiet; then
|
||||
git commit -m "docs: sync from docs/wiki/ at commit ${GITHUB_SHA:0:8}"
|
||||
echo "Pushing to wiki..."
|
||||
if git push origin master; then
|
||||
echo "✓ Wiki successfully synced"
|
||||
else
|
||||
echo "⚠ Wiki push failed - check token permissions"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "No wiki changes to commit"
|
||||
fi
|
||||
|
||||
build-linux-amd64:
|
||||
needs: autotag
|
||||
runs-on: linux-amd64
|
||||
container:
|
||||
image: rust:1.88-slim
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
apt-get update -qq && apt-get install -y -qq \
|
||||
libwebkit2gtk-4.1-dev libssl-dev libgtk-3-dev \
|
||||
libayatana-appindicator3-dev librsvg2-dev patchelf \
|
||||
pkg-config curl perl jq
|
||||
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
|
||||
apt-get install -y nodejs
|
||||
- name: Build
|
||||
run: |
|
||||
npm ci --legacy-peer-deps
|
||||
rustup target add x86_64-unknown-linux-gnu
|
||||
CI=true npx tauri build --target x86_64-unknown-linux-gnu
|
||||
- name: Upload artifacts
|
||||
env:
|
||||
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
run: |
|
||||
set -eu
|
||||
API="http://172.0.0.29:3000/api/v1/repos/$GITHUB_REPOSITORY"
|
||||
TAG=$(curl -s "$API/tags?limit=50" \
|
||||
-H "Authorization: token $RELEASE_TOKEN" | \
|
||||
jq -r '.[].name' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | \
|
||||
sort -V | tail -1 || true)
|
||||
if [ -z "$TAG" ]; then
|
||||
echo "ERROR: Could not resolve release tag from repository tags."
|
||||
exit 1
|
||||
fi
|
||||
echo "Creating release for $TAG..."
|
||||
curl -sf -X POST "$API/releases" \
|
||||
-H "Authorization: token $RELEASE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"tag_name\":\"$TAG\",\"name\":\"TFTSR $TAG\",\"body\":\"Release $TAG\",\"draft\":false}" || true
|
||||
RELEASE_ID=$(curl -sf "$API/releases/tags/$TAG" \
|
||||
-H "Authorization: token $RELEASE_TOKEN" | jq -r '.id')
|
||||
if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" = "null" ]; then
|
||||
echo "ERROR: Failed to get release ID for $TAG"
|
||||
exit 1
|
||||
fi
|
||||
echo "Release ID: $RELEASE_ID"
|
||||
ARTIFACTS=$(find src-tauri/target/x86_64-unknown-linux-gnu/release/bundle -type f \
|
||||
\( -name "*.deb" -o -name "*.rpm" -o -name "*.AppImage" \))
|
||||
if [ -z "$ARTIFACTS" ]; then
|
||||
echo "ERROR: No Linux amd64 artifacts were found to upload."
|
||||
exit 1
|
||||
fi
|
||||
printf '%s\n' "$ARTIFACTS" | while IFS= read -r f; do
|
||||
NAME=$(basename "$f")
|
||||
UPLOAD_NAME="linux-amd64-$NAME"
|
||||
echo "Uploading $UPLOAD_NAME..."
|
||||
EXISTING_IDS=$(curl -sf "$API/releases/$RELEASE_ID" \
|
||||
-H "Authorization: token $RELEASE_TOKEN" \
|
||||
| jq -r --arg name "$UPLOAD_NAME" '.assets[]? | select(.name == $name) | .id')
|
||||
if [ -n "$EXISTING_IDS" ]; then
|
||||
printf '%s\n' "$EXISTING_IDS" | while IFS= read -r id; do
|
||||
[ -n "$id" ] || continue
|
||||
echo "Deleting existing asset id=$id name=$UPLOAD_NAME before upload..."
|
||||
curl -sf -X DELETE "$API/releases/$RELEASE_ID/assets/$id" \
|
||||
-H "Authorization: token $RELEASE_TOKEN"
|
||||
done
|
||||
fi
|
||||
RESP_FILE=$(mktemp)
|
||||
HTTP_CODE=$(curl -sS -o "$RESP_FILE" -w "%{http_code}" -X POST "$API/releases/$RELEASE_ID/assets" \
|
||||
-H "Authorization: token $RELEASE_TOKEN" \
|
||||
-F "attachment=@$f;filename=$UPLOAD_NAME")
|
||||
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
|
||||
echo "✓ Uploaded $UPLOAD_NAME"
|
||||
else
|
||||
echo "✗ Upload failed for $UPLOAD_NAME (HTTP $HTTP_CODE)"
|
||||
python -c 'import pathlib,sys;print(pathlib.Path(sys.argv[1]).read_text(errors="replace")[:2000])' "$RESP_FILE"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
build-windows-amd64:
|
||||
needs: autotag
|
||||
runs-on: linux-amd64
|
||||
container:
|
||||
image: rust:1.88-slim
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
apt-get update -qq && apt-get install -y -qq mingw-w64 curl nsis perl make jq
|
||||
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
|
||||
apt-get install -y nodejs
|
||||
- name: Build
|
||||
env:
|
||||
CC_x86_64_pc_windows_gnu: x86_64-w64-mingw32-gcc
|
||||
CXX_x86_64_pc_windows_gnu: x86_64-w64-mingw32-g++
|
||||
AR_x86_64_pc_windows_gnu: x86_64-w64-mingw32-ar
|
||||
CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER: x86_64-w64-mingw32-gcc
|
||||
OPENSSL_NO_VENDOR: "0"
|
||||
OPENSSL_STATIC: "1"
|
||||
run: |
|
||||
npm ci --legacy-peer-deps
|
||||
rustup target add x86_64-pc-windows-gnu
|
||||
CI=true npx tauri build --target x86_64-pc-windows-gnu
|
||||
- name: Upload artifacts
|
||||
env:
|
||||
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
run: |
|
||||
set -eu
|
||||
API="http://172.0.0.29:3000/api/v1/repos/$GITHUB_REPOSITORY"
|
||||
TAG=$(curl -s "$API/tags?limit=50" \
|
||||
-H "Authorization: token $RELEASE_TOKEN" | \
|
||||
jq -r '.[].name' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | \
|
||||
sort -V | tail -1 || true)
|
||||
if [ -z "$TAG" ]; then
|
||||
echo "ERROR: Could not resolve release tag from repository tags."
|
||||
exit 1
|
||||
fi
|
||||
echo "Creating release for $TAG..."
|
||||
curl -sf -X POST "$API/releases" \
|
||||
-H "Authorization: token $RELEASE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"tag_name\":\"$TAG\",\"name\":\"TFTSR $TAG\",\"body\":\"Release $TAG\",\"draft\":false}" || true
|
||||
RELEASE_ID=$(curl -sf "$API/releases/tags/$TAG" \
|
||||
-H "Authorization: token $RELEASE_TOKEN" | jq -r '.id')
|
||||
if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" = "null" ]; then
|
||||
echo "ERROR: Failed to get release ID for $TAG"
|
||||
exit 1
|
||||
fi
|
||||
echo "Release ID: $RELEASE_ID"
|
||||
ARTIFACTS=$(find src-tauri/target/x86_64-pc-windows-gnu/release/bundle -type f \
|
||||
\( -name "*.exe" -o -name "*.msi" \) 2>/dev/null)
|
||||
if [ -z "$ARTIFACTS" ]; then
|
||||
echo "ERROR: No Windows amd64 artifacts were found to upload."
|
||||
exit 1
|
||||
fi
|
||||
printf '%s\n' "$ARTIFACTS" | while IFS= read -r f; do
|
||||
NAME=$(basename "$f")
|
||||
echo "Uploading $NAME..."
|
||||
EXISTING_IDS=$(curl -sf "$API/releases/$RELEASE_ID" \
|
||||
-H "Authorization: token $RELEASE_TOKEN" \
|
||||
| jq -r --arg name "$NAME" '.assets[]? | select(.name == $name) | .id')
|
||||
if [ -n "$EXISTING_IDS" ]; then
|
||||
printf '%s\n' "$EXISTING_IDS" | while IFS= read -r id; do
|
||||
[ -n "$id" ] || continue
|
||||
echo "Deleting existing asset id=$id name=$NAME before upload..."
|
||||
curl -sf -X DELETE "$API/releases/$RELEASE_ID/assets/$id" \
|
||||
-H "Authorization: token $RELEASE_TOKEN"
|
||||
done
|
||||
fi
|
||||
RESP_FILE=$(mktemp)
|
||||
HTTP_CODE=$(curl -sS -o "$RESP_FILE" -w "%{http_code}" -X POST "$API/releases/$RELEASE_ID/assets" \
|
||||
-H "Authorization: token $RELEASE_TOKEN" \
|
||||
-F "attachment=@$f;filename=$NAME")
|
||||
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
|
||||
echo "✓ Uploaded $NAME"
|
||||
else
|
||||
echo "✗ Upload failed for $NAME (HTTP $HTTP_CODE)"
|
||||
python -c 'import pathlib,sys;print(pathlib.Path(sys.argv[1]).read_text(errors="replace")[:2000])' "$RESP_FILE"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
build-macos-arm64:
|
||||
needs: autotag
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Build
|
||||
env:
|
||||
MACOSX_DEPLOYMENT_TARGET: "11.0"
|
||||
run: |
|
||||
npm ci --legacy-peer-deps
|
||||
rustup target add aarch64-apple-darwin
|
||||
CI=true npx tauri build --target aarch64-apple-darwin --bundles app
|
||||
APP=$(find src-tauri/target/aarch64-apple-darwin/release/bundle/macos -maxdepth 1 -type d -name "*.app" | head -n 1)
|
||||
if [ -z "$APP" ]; then
|
||||
echo "ERROR: Could not find macOS app bundle"
|
||||
exit 1
|
||||
fi
|
||||
APP_NAME=$(basename "$APP" .app)
|
||||
codesign --deep --force --sign - "$APP"
|
||||
mkdir -p src-tauri/target/aarch64-apple-darwin/release/bundle/dmg
|
||||
DMG=src-tauri/target/aarch64-apple-darwin/release/bundle/dmg/${APP_NAME}.dmg
|
||||
hdiutil create -volname "$APP_NAME" -srcfolder "$APP" -ov -format UDZO "$DMG"
|
||||
- name: Upload artifacts
|
||||
env:
|
||||
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
run: |
|
||||
set -eu
|
||||
API="http://172.0.0.29:3000/api/v1/repos/$GITHUB_REPOSITORY"
|
||||
TAG=$(curl -s "$API/tags?limit=50" \
|
||||
-H "Authorization: token $RELEASE_TOKEN" | \
|
||||
jq -r '.[].name' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | \
|
||||
sort -V | tail -1 || true)
|
||||
if [ -z "$TAG" ]; then
|
||||
echo "ERROR: Could not resolve release tag from repository tags."
|
||||
exit 1
|
||||
fi
|
||||
echo "Creating release for $TAG..."
|
||||
curl -sf -X POST "$API/releases" \
|
||||
-H "Authorization: token $RELEASE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"tag_name\":\"$TAG\",\"name\":\"TFTSR $TAG\",\"body\":\"Release $TAG\",\"draft\":false}" || true
|
||||
RELEASE_ID=$(curl -sf "$API/releases/tags/$TAG" \
|
||||
-H "Authorization: token $RELEASE_TOKEN" | jq -r '.id')
|
||||
if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" = "null" ]; then
|
||||
echo "ERROR: Failed to get release ID for $TAG"
|
||||
exit 1
|
||||
fi
|
||||
echo "Release ID: $RELEASE_ID"
|
||||
ARTIFACTS=$(find src-tauri/target/aarch64-apple-darwin/release/bundle -type f -name "*.dmg")
|
||||
if [ -z "$ARTIFACTS" ]; then
|
||||
echo "ERROR: No macOS arm64 DMG artifacts were found to upload."
|
||||
exit 1
|
||||
fi
|
||||
printf '%s\n' "$ARTIFACTS" | while IFS= read -r f; do
|
||||
NAME=$(basename "$f")
|
||||
echo "Uploading $NAME..."
|
||||
EXISTING_IDS=$(curl -sf "$API/releases/$RELEASE_ID" \
|
||||
-H "Authorization: token $RELEASE_TOKEN" \
|
||||
| jq -r --arg name "$NAME" '.assets[]? | select(.name == $name) | .id')
|
||||
if [ -n "$EXISTING_IDS" ]; then
|
||||
printf '%s\n' "$EXISTING_IDS" | while IFS= read -r id; do
|
||||
[ -n "$id" ] || continue
|
||||
echo "Deleting existing asset id=$id name=$NAME before upload..."
|
||||
curl -sf -X DELETE "$API/releases/$RELEASE_ID/assets/$id" \
|
||||
-H "Authorization: token $RELEASE_TOKEN"
|
||||
done
|
||||
fi
|
||||
RESP_FILE=$(mktemp)
|
||||
HTTP_CODE=$(curl -sS -o "$RESP_FILE" -w "%{http_code}" -X POST "$API/releases/$RELEASE_ID/assets" \
|
||||
-H "Authorization: token $RELEASE_TOKEN" \
|
||||
-F "attachment=@$f;filename=$NAME")
|
||||
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
|
||||
echo "✓ Uploaded $NAME"
|
||||
else
|
||||
echo "✗ Upload failed for $NAME (HTTP $HTTP_CODE)"
|
||||
python -c 'import pathlib,sys;print(pathlib.Path(sys.argv[1]).read_text(errors="replace")[:2000])' "$RESP_FILE"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
build-linux-arm64:
|
||||
needs: autotag
|
||||
runs-on: linux-amd64
|
||||
container:
|
||||
image: ubuntu:22.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Install dependencies
|
||||
env:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
run: |
|
||||
# Step 1: Host tools + cross-compiler (all amd64, no multiarch yet)
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq curl git gcc g++ make patchelf pkg-config perl jq \
|
||||
gcc-aarch64-linux-gnu g++-aarch64-linux-gnu
|
||||
|
||||
# Step 2: Multiarch — Ubuntu uses ports.ubuntu.com for arm64,
|
||||
# keeping it on a separate mirror from amd64 (archive.ubuntu.com).
|
||||
# This avoids the binary-all index duplication and -dev package
|
||||
# conflicts that plagued the Debian single-mirror approach.
|
||||
dpkg --add-architecture arm64
|
||||
sed -i 's|^deb http://archive.ubuntu.com|deb [arch=amd64] http://archive.ubuntu.com|g' /etc/apt/sources.list
|
||||
sed -i 's|^deb http://security.ubuntu.com|deb [arch=amd64] http://security.ubuntu.com|g' /etc/apt/sources.list
|
||||
printf '%s\n' \
|
||||
'deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports jammy main restricted universe multiverse' \
|
||||
'deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports jammy-updates main restricted universe multiverse' \
|
||||
'deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports jammy-security main restricted universe multiverse' \
|
||||
> /etc/apt/sources.list.d/arm64-ports.list
|
||||
apt-get update -qq
|
||||
|
||||
# Step 3: ARM64 dev libs — libayatana omitted (no tray icon in this app)
|
||||
apt-get install -y -qq \
|
||||
libwebkit2gtk-4.1-dev:arm64 \
|
||||
libssl-dev:arm64 \
|
||||
libgtk-3-dev:arm64 \
|
||||
librsvg2-dev:arm64
|
||||
|
||||
# Step 4: Node.js
|
||||
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
|
||||
apt-get install -y nodejs
|
||||
|
||||
# Step 5: Rust (not pre-installed in ubuntu:22.04)
|
||||
# source "$HOME/.cargo/env" in the Build step handles PATH — no GITHUB_PATH needed
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \
|
||||
--default-toolchain 1.88.0 --profile minimal --no-modify-path
|
||||
- name: Build
|
||||
env:
|
||||
CC_aarch64_unknown_linux_gnu: aarch64-linux-gnu-gcc
|
||||
CXX_aarch64_unknown_linux_gnu: aarch64-linux-gnu-g++
|
||||
AR_aarch64_unknown_linux_gnu: aarch64-linux-gnu-ar
|
||||
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
|
||||
PKG_CONFIG_SYSROOT_DIR: /usr/aarch64-linux-gnu
|
||||
PKG_CONFIG_PATH: /usr/lib/aarch64-linux-gnu/pkgconfig
|
||||
PKG_CONFIG_ALLOW_CROSS: "1"
|
||||
OPENSSL_NO_VENDOR: "0"
|
||||
OPENSSL_STATIC: "1"
|
||||
APPIMAGE_EXTRACT_AND_RUN: "1"
|
||||
run: |
|
||||
. "$HOME/.cargo/env"
|
||||
npm ci --legacy-peer-deps
|
||||
rustup target add aarch64-unknown-linux-gnu
|
||||
CI=true npx tauri build --target aarch64-unknown-linux-gnu --bundles deb,rpm
|
||||
- name: Upload artifacts
|
||||
env:
|
||||
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
run: |
|
||||
set -eu
|
||||
API="http://172.0.0.29:3000/api/v1/repos/$GITHUB_REPOSITORY"
|
||||
TAG=$(curl -s "$API/tags?limit=50" \
|
||||
-H "Authorization: token $RELEASE_TOKEN" | \
|
||||
jq -r '.[].name' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | \
|
||||
sort -V | tail -1 || true)
|
||||
if [ -z "$TAG" ]; then
|
||||
echo "ERROR: Could not resolve release tag from repository tags."
|
||||
exit 1
|
||||
fi
|
||||
echo "Creating release for $TAG..."
|
||||
curl -sf -X POST "$API/releases" \
|
||||
-H "Authorization: token $RELEASE_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"tag_name\":\"$TAG\",\"name\":\"TFTSR $TAG\",\"body\":\"Release $TAG\",\"draft\":false}" || true
|
||||
RELEASE_ID=$(curl -sf "$API/releases/tags/$TAG" \
|
||||
-H "Authorization: token $RELEASE_TOKEN" | jq -r '.id')
|
||||
if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" = "null" ]; then
|
||||
echo "ERROR: Failed to get release ID for $TAG"
|
||||
exit 1
|
||||
fi
|
||||
echo "Release ID: $RELEASE_ID"
|
||||
ARTIFACTS=$(find src-tauri/target/aarch64-unknown-linux-gnu/release/bundle -type f \
|
||||
\( -name "*.deb" -o -name "*.rpm" -o -name "*.AppImage" \))
|
||||
if [ -z "$ARTIFACTS" ]; then
|
||||
echo "ERROR: No Linux arm64 artifacts were found to upload."
|
||||
exit 1
|
||||
fi
|
||||
printf '%s\n' "$ARTIFACTS" | while IFS= read -r f; do
|
||||
NAME=$(basename "$f")
|
||||
UPLOAD_NAME="linux-arm64-$NAME"
|
||||
echo "Uploading $UPLOAD_NAME..."
|
||||
EXISTING_IDS=$(curl -sf "$API/releases/$RELEASE_ID" \
|
||||
-H "Authorization: token $RELEASE_TOKEN" \
|
||||
| jq -r --arg name "$UPLOAD_NAME" '.assets[]? | select(.name == $name) | .id')
|
||||
if [ -n "$EXISTING_IDS" ]; then
|
||||
printf '%s\n' "$EXISTING_IDS" | while IFS= read -r id; do
|
||||
[ -n "$id" ] || continue
|
||||
echo "Deleting existing asset id=$id name=$UPLOAD_NAME before upload..."
|
||||
curl -sf -X DELETE "$API/releases/$RELEASE_ID/assets/$id" \
|
||||
-H "Authorization: token $RELEASE_TOKEN"
|
||||
done
|
||||
fi
|
||||
RESP_FILE=$(mktemp)
|
||||
HTTP_CODE=$(curl -sS -o "$RESP_FILE" -w "%{http_code}" -X POST "$API/releases/$RELEASE_ID/assets" \
|
||||
-H "Authorization: token $RELEASE_TOKEN" \
|
||||
-F "attachment=@$f;filename=$UPLOAD_NAME")
|
||||
if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then
|
||||
echo "✓ Uploaded $UPLOAD_NAME"
|
||||
else
|
||||
echo "✗ Upload failed for $UPLOAD_NAME (HTTP $HTTP_CODE)"
|
||||
python -c 'import pathlib,sys;print(pathlib.Path(sys.argv[1]).read_text(errors="replace")[:2000])' "$RESP_FILE"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
66
.github/workflows/test.yml
vendored
@ -1,66 +0,0 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
rust-fmt-check:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: rust:1.88-slim
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- run: rustup component add rustfmt
|
||||
- run: cargo fmt --manifest-path src-tauri/Cargo.toml --check
|
||||
|
||||
rust-clippy:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: rust:1.88-slim
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- run: apt-get update -qq && apt-get install -y -qq libwebkit2gtk-4.1-dev libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev patchelf pkg-config perl
|
||||
- run: rustup component add clippy
|
||||
- run: cargo clippy --manifest-path src-tauri/Cargo.toml -- -D warnings
|
||||
|
||||
rust-tests:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: rust:1.88-slim
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- run: apt-get update -qq && apt-get install -y -qq libwebkit2gtk-4.1-dev libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev patchelf pkg-config perl
|
||||
- run: cargo test --manifest-path src-tauri/Cargo.toml
|
||||
|
||||
frontend-typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: node:22-alpine
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- run: npm ci --legacy-peer-deps
|
||||
- run: npx tsc --noEmit
|
||||
|
||||
frontend-tests:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: node:22-alpine
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- run: npm ci --legacy-peer-deps
|
||||
- run: npm run test:run
|
||||
5
.gitignore
vendored
@ -10,6 +10,9 @@ artifacts/
|
||||
*.png
|
||||
/screenshots/
|
||||
|
||||
# kubectl binaries (downloaded during build)
|
||||
src-tauri/binaries/
|
||||
|
||||
SECURITY_AUDIT.md
|
||||
|
||||
# Internal / private documents — never commit
|
||||
@ -20,3 +23,5 @@ TICKET_*.md
|
||||
BUGFIX_SUMMARY.md
|
||||
PR_DESCRIPTION.md
|
||||
docs/images/user-guide/
|
||||
*.bak
|
||||
.DS_Store
|
||||
|
||||
16
AGENTS.md
@ -91,7 +91,7 @@ TypeScript mirrors this shape exactly in `tauriCommands.ts`.
|
||||
**Artifacts**: `src-tauri/target/{target}/release/bundle/`
|
||||
|
||||
**Environments**:
|
||||
- Test CI images at `172.0.0.29:3000` (pull `trcaa-*:rust1.88-node22`)
|
||||
- Test CI images at `172.0.0.29:3000` (pull `tftsr-*:rust1.88-node22`)
|
||||
- Gitea instance: `http://172.0.0.29:3000`
|
||||
- Wiki: sync from `docs/wiki/*.md` → `https://gogs.tftsr.com/sarman/tftsr-devops_investigation/wiki`
|
||||
|
||||
@ -101,15 +101,15 @@ TypeScript mirrors this shape exactly in `tauriCommands.ts`.
|
||||
|
||||
| Variable | Default | Purpose |
|
||||
|----------|---------|---------|
|
||||
| `TFTSR_DATA_DIR` | Platform data dir | Override database location |
|
||||
| `TFTSR_DB_KEY` | Auto-generated | SQLCipher encryption key override |
|
||||
| `TFTSR_ENCRYPTION_KEY` | Auto-generated | Credential encryption key override |
|
||||
| `TRCAA_DATA_DIR` (or legacy `TRCAA_DATA_DIR`) | Platform data dir | Override database location |
|
||||
| `TRCAA_DB_KEY` (or legacy `TRCAA_DB_KEY`) | Auto-generated | SQLCipher encryption key override |
|
||||
| `TRCAA_ENCRYPTION_KEY` (or legacy `TRCAA_ENCRYPTION_KEY`) | Auto-generated | Credential encryption key override |
|
||||
| `RUST_LOG` | `info` | Tracing level (`debug`, `info`, `warn`, `error`) |
|
||||
|
||||
**Database path**:
|
||||
- Linux: `~/.local/share/trcaa/trcaa.db`
|
||||
- macOS: `~/Library/Application Support/trcaa/trcaa.db`
|
||||
- Windows: `%APPDATA%\trcaa\trcaa.db`
|
||||
- Linux: `~/.local/share/tftsr/tftsr.db`
|
||||
- macOS: `~/Library/Application Support/tftsr/tftsr.db`
|
||||
- Windows: `%APPDATA%\tftsr\tftsr.db`
|
||||
|
||||
---
|
||||
|
||||
@ -128,7 +128,7 @@ TypeScript mirrors this shape exactly in `tauriCommands.ts`.
|
||||
|
||||
### Security
|
||||
- **Database encryption**: AES-256 (SQLCipher in release builds)
|
||||
- **Credential encryption**: AES-256-GCM, keys stored in `TFTSR_ENCRYPTION_KEY` or auto-generated `.enckey` (mode 0600)
|
||||
- **Credential encryption**: AES-256-GCM, keys stored in `TRCAA_ENCRYPTION_KEY` (or legacy `TRCAA_ENCRYPTION_KEY`) or auto-generated `.enckey` (mode 0600)
|
||||
- **Audit trail**: Hash-chained entries (`prev_hash` + `entry_hash`) for tamper evidence
|
||||
- **PII protection**: 12-pattern detector → user approval gate → hash-chained audit entry
|
||||
|
||||
|
||||
50
CHANGELOG.md
@ -4,50 +4,6 @@ All notable changes to TRCAA are documented here.
|
||||
Commit types shown: feat, fix, perf, docs, refactor.
|
||||
CI, chore, and build changes are excluded.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Bug Fixes
|
||||
- Align Tauri npm packages with Rust crate versions
|
||||
- Pin plugin-stronghold npm version to match Rust crate (2.3.1)
|
||||
|
||||
## [0.3.12] — 2026-06-05
|
||||
|
||||
### Bug Fixes
|
||||
- **ci**: Fix YAML syntax error in test.yml
|
||||
- Address valid PR review findings
|
||||
- Add missing @testing-library/dom dependency and fix clippy warning
|
||||
|
||||
### Documentation
|
||||
- Add ADRs for shell safety, MCP transport, kubectl bundling
|
||||
- Update wiki with shell execution, Ollama function calling, and CI/CD changes
|
||||
- Add v1.0.7 and v1.0.8 release notes
|
||||
|
||||
### Features
|
||||
- Add three-tier shell execution with kubectl support
|
||||
- Add shell execution database migrations (migrations #24-28)
|
||||
- Add Ollama function calling and tool calling auto-detection
|
||||
- Add shell execution and kubeconfig management UI
|
||||
- Add kubectl binary bundling for cross-platform support
|
||||
|
||||
## [0.3.11] — 2026-06-01
|
||||
|
||||
### Bug Fixes
|
||||
- **mcp**: Treat missing resources/list as non-fatal for servers that don't implement it
|
||||
|
||||
### Documentation
|
||||
- **wiki**: Update MCP-Servers.md with env var support, PATH requirement, and new schema column
|
||||
|
||||
## [0.3.10] — 2026-06-01
|
||||
|
||||
### Bug Fixes
|
||||
- **mcp**: Add env encryption to store layer
|
||||
- **mcp**: Parse and merge env vars in discovery layer
|
||||
- **mcp**: Add environment variable and HTTP header support for MCP servers
|
||||
- **mcp**: Improve UX clarity for encrypted env vars during edit
|
||||
- **mcp**: Change plaintext env input to type=text
|
||||
- **mcp**: Add validation to block dangerous environment variables
|
||||
- **mcp**: Fix test_allows_safe_env_vars test failure
|
||||
|
||||
## [0.3.9] — 2026-06-01
|
||||
|
||||
### Bug Fixes
|
||||
@ -273,7 +229,7 @@ CI, chore, and build changes are excluded.
|
||||
- Update CHANGELOG.md for v0.2.71
|
||||
|
||||
### Features
|
||||
- Initial implementation of TFTSR IT Triage & RCA application
|
||||
- Initial implementation of TRCAA IT Triage & RCA application
|
||||
- Add Windows amd64 cross-compile to release pipeline; add arm64 QEMU agent
|
||||
- Add native linux/arm64 release build step
|
||||
- Add macOS arm64 act_runner and release build job
|
||||
@ -281,7 +237,7 @@ CI, chore, and build changes are excluded.
|
||||
- Inline file/screenshot attachment in triage chat
|
||||
- Close issues, restore history, auto-save resolution steps
|
||||
- Expand domains to 13 — add Telephony, Security/Vault, Public Safety, Application, Automation/CI-CD
|
||||
- Add HPE, Dell, Identity domains + expand k8s/security/observability/VESTA NXT
|
||||
- Add HPE, Dell, Identity domains + expand k8s/security/observability/DevOps Platform
|
||||
- Add AI disclaimer modal before creating new issues
|
||||
- Add database schema for integration credentials and config
|
||||
- Implement OAuth2 token exchange and AES-256-GCM encryption
|
||||
@ -295,7 +251,7 @@ CI, chore, and build changes are excluded.
|
||||
- Add multi-mode authentication for integrations (v0.2.10)
|
||||
- Complete webview cookie extraction implementation
|
||||
- Add custom_rest provider mode and rebrand application name
|
||||
- **rebrand**: Rename binary to trcaa and auto-generate DB key
|
||||
- **rebrand**: Rename binary to tftsr and auto-generate DB key
|
||||
- **ui**: Fix model dropdown, auth prefill, PII persistence, theme toggle, and Ollama bundle
|
||||
- **ci**: Add persistent pre-baked Docker builder images
|
||||
- **ai**: Add tool-calling and integration search as AI data source
|
||||
|
||||
68
CLAUDE.md
@ -77,8 +77,9 @@ cargo tauri build # Outputs to src-tauri/target/release/bundle/
|
||||
|
||||
### CI/CD
|
||||
|
||||
- **Test pipeline**: `.woodpecker/test.yml` — runs on every push/PR
|
||||
- **Release pipeline**: `.woodpecker/release.yml` — runs on `v*` tags, produces Linux amd64+arm64 bundles, uploads to Gogs release at `http://172.0.0.29:3000/api/v1`
|
||||
- **Test pipeline**: `.github/workflows/test.yml` — runs on every push/PR targeting `main`
|
||||
- **Release pipeline**: `.github/workflows/release.yml` — runs on every push to `main`, auto-tags, produces multi-platform bundles (Linux amd64+arm64, Windows, macOS arm64+Intel), uploads to GitHub Releases at `https://gogs.tftsr.com/sarman/apollo_nxt-tftsr/releases`
|
||||
- **Docker builder images**: `.github/workflows/build-images.yml` — rebuilds `ghcr.io/tftsr/tftsr-*` images when `.docker/**` changes on `main`
|
||||
|
||||
---
|
||||
|
||||
@ -117,9 +118,9 @@ All command handlers receive `State<'_, AppState>` as a Tauri-injected parameter
|
||||
|
||||
**AI provider factory**: `ai/provider.rs::create_provider(config)` dispatches on `config.name` to the matching struct. Adding a provider means implementing the `Provider` trait and adding a match arm.
|
||||
|
||||
**Database encryption**: `cfg!(debug_assertions)` → plain SQLite; release → SQLCipher AES-256. Key from `TFTSR_DB_KEY` env var (defaults to a dev placeholder). DB path from `TFTSR_DATA_DIR` or platform data dir.
|
||||
**Database encryption**: `cfg!(debug_assertions)` → plain SQLite; release → SQLCipher AES-256. Key from `TRCAA_DB_KEY` (or legacy `TRCAA_DB_KEY`) env var (defaults to a dev placeholder). DB path from `TRCAA_DATA_DIR` (or legacy `TRCAA_DATA_DIR`) or platform data dir.
|
||||
|
||||
**Credential encryption**: API keys stored in `AppSettings` are encrypted using AES-256-GCM via the `aes-gcm` crate. The encryption key is derived from `TFTSR_ENCRYPTION_KEY` env var. Credentials are encrypted on save and decrypted on load. See `commands/system.rs::save_settings()` for implementation.
|
||||
**Credential encryption**: API keys stored in `AppSettings` are encrypted using AES-256-GCM via the `aes-gcm` crate. The encryption key is derived from `TRCAA_ENCRYPTION_KEY` (or legacy `TRCAA_ENCRYPTION_KEY`) env var. Credentials are encrypted on save and decrypted on load. See `commands/system.rs::save_settings()` for implementation.
|
||||
|
||||
### Frontend (React / TypeScript)
|
||||
|
||||
@ -162,24 +163,60 @@ On the TypeScript side, `tauriCommands.ts` mirrors this shape exactly.
|
||||
|
||||
Before any text is sent to an AI provider, `apply_redactions` must be called and the resulting SHA-256 hash recorded via `audit::log::write_audit_event`.
|
||||
|
||||
### Woodpecker CI + Gogs Compatibility
|
||||
### Shell Command Execution (v1.0.0)
|
||||
|
||||
**Status**: Woodpecker CI v0.15.4 is deployed at `http://172.0.0.29:8084` (direct) and `http://172.0.0.29:8085` (nginx proxy). Webhook delivery from Gogs works, but CI builds are not yet triggering due to hook authentication issues. See `PLAN.md § Phase 11` for full details.
|
||||
**Status**: Production-ready agentic shell execution with three-tier safety classification.
|
||||
|
||||
Known issues with Woodpecker 0.15.4 + Gogs 0.14:
|
||||
- `token.ParseRequest()` does not read `?token=` URL params (only `Authorization` header and `user_sess` cookie)
|
||||
- The SPA login form uses `login=` field; Gogs backend reads `username=` — a custom login page is served by nginx at `/login` and `/login/form`
|
||||
- Gogs 0.14 has no OAuth2 provider support, blocking upgrade to Woodpecker 2.x
|
||||
**Features**:
|
||||
- kubectl commands with bundled binary (v1.30.0)
|
||||
- Proxmox tools (pvecm, pvesh, qm)
|
||||
- General shell diagnostics
|
||||
- Real-time approval modal for Tier 2 commands
|
||||
- Multiple kubeconfig support with AES-256 encrypted storage
|
||||
- Pipe/chain command analysis with tier escalation
|
||||
- Command execution history and audit logging
|
||||
|
||||
Gogs token quirk: the `sha1` value returned by `POST /api/v1/users/{user}/tokens` is the **actual bearer token**. The `sha1` and `sha256` columns in the Gogs DB are hashes of that token, not the token itself.
|
||||
**Three-Tier Safety System**:
|
||||
- **Tier 1** (Auto-execute): `kubectl get|describe|logs`, `cat|grep|ls`
|
||||
- **Tier 2** (User approval): `kubectl apply|delete|scale`, `ssh`, `systemctl restart`
|
||||
- **Tier 3** (Always deny): `rm -rf`, `shutdown`, `mkfs`
|
||||
|
||||
**Key Files**:
|
||||
- `src-tauri/src/shell/classifier.rs`: Command safety classification (19 tests, 100% coverage)
|
||||
- `src-tauri/src/shell/executor.rs`: Execution flow with approval gates
|
||||
- `src-tauri/src/shell/kubectl.rs`: kubectl binary management
|
||||
- `src-tauri/src/shell/kubeconfig.rs`: Kubeconfig parsing and encryption
|
||||
- `src-tauri/src/commands/shell.rs`: 7 Tauri commands for kubeconfig and execution management
|
||||
- `src-tauri/src/ai/tools.rs`: `execute_shell_command` tool registration
|
||||
- `src/components/ShellApprovalModal.tsx`: Real-time approval UI
|
||||
- `src/pages/Settings/ShellExecution.tsx`: Settings and history view
|
||||
- `src/pages/Settings/KubeconfigManager.tsx`: Multi-cluster management UI
|
||||
- `scripts/download-kubectl.sh`: Binary download for all platforms
|
||||
|
||||
**Database Tables** (Migrations 024-027):
|
||||
- `shell_commands`: Pre-defined command templates with tier definitions
|
||||
- `kubeconfig_files`: Encrypted kubeconfig storage
|
||||
- `command_executions`: Full audit trail (command, tier, status, exit code, stdout, stderr, timing)
|
||||
- `approval_decisions`: Session-based approval preferences
|
||||
|
||||
**Documentation**: `docs/wiki/Shell-Execution.md`
|
||||
|
||||
### GitHub Actions CI
|
||||
|
||||
All pipelines run on GitHub Actions at `https://gogs.tftsr.com/sarman/apollo_nxt-tftsr/actions`.
|
||||
|
||||
- `GITHUB_TOKEN` is the only credential needed — no external secrets required
|
||||
- Builder images are hosted on `ghcr.io/tftsr/` (GitHub Container Registry)
|
||||
- Branch protection on `main` requires `rust-test` and `frontend-test` checks to pass, plus Copilot code review, before merging
|
||||
- kubectl binaries downloaded during build via `scripts/download-kubectl.sh` for all platforms
|
||||
|
||||
---
|
||||
|
||||
## Wiki Maintenance
|
||||
|
||||
The project wiki lives at `https://gogs.tftsr.com/sarman/tftsr-devops_investigation/wiki`.
|
||||
The project wiki lives at `https://gogs.tftsr.com/sarman/apollo_nxt-tftsr/wiki`.
|
||||
|
||||
**Source of truth**: `docs/wiki/*.md` in this repo. The `wiki-sync` CI step (in `.woodpecker/test.yml`) automatically pushes any changes to the Gogs wiki on every push to master.
|
||||
**Source of truth**: `docs/wiki/*.md` in this repo. The `wiki-sync` job (in `.github/workflows/release.yml`) automatically pushes any changes to the GitHub wiki on every push to `main`.
|
||||
|
||||
**When making code changes, update the corresponding wiki file in `docs/wiki/` before committing:**
|
||||
|
||||
@ -189,16 +226,17 @@ The project wiki lives at `https://gogs.tftsr.com/sarman/tftsr-devops_investigat
|
||||
| DB schema or migrations (`db/migrations.rs`, `db/models.rs`) | `docs/wiki/Database.md` |
|
||||
| New/changed AI provider (`ai/*.rs`) | `docs/wiki/AI-Providers.md` |
|
||||
| PII patterns or detection logic (`pii/`) | `docs/wiki/PII-Detection.md` |
|
||||
| CI/CD pipeline changes (`.woodpecker/*.yml`) | `docs/wiki/CICD-Pipeline.md` |
|
||||
| CI/CD pipeline changes (`.github/workflows/*.yml`) | `docs/wiki/CICD-Pipeline.md` |
|
||||
| Rust architecture or module layout (`lib.rs`, `state.rs`) | `docs/wiki/Architecture.md` |
|
||||
| Security-relevant changes (capabilities, audit, Stronghold) | `docs/wiki/Security-Model.md` |
|
||||
| Dev setup, prerequisites, build commands | `docs/wiki/Development-Setup.md` |
|
||||
| Integration stubs or v0.2 progress (`integrations/`) | `docs/wiki/Integrations.md` |
|
||||
| Recurring bugs and fixes | `docs/wiki/Troubleshooting.md` |
|
||||
| Shell execution, kubectl, kubeconfig management (`shell/`) | `docs/wiki/Shell-Execution.md` |
|
||||
|
||||
To manually push wiki changes without waiting for CI:
|
||||
```bash
|
||||
cd /tmp/tftsr-wiki # local clone of the wiki git repo
|
||||
cd /tmp/apollo-wiki # local clone of the wiki git repo
|
||||
# edit *.md files, then:
|
||||
git add -A && git commit -m "docs: ..." && git push
|
||||
```
|
||||
|
||||
17
Makefile
@ -1,10 +1,9 @@
|
||||
GOGS_API := http://172.0.0.29:3000/api/v1
|
||||
GOGS_REPO := sarman/tftsr-devops_investigation
|
||||
TAG ?= v0.1.0-alpha
|
||||
TARGET := aarch64-unknown-linux-gnu
|
||||
|
||||
# Build linux/arm64 release artifact natively inside a Docker container,
|
||||
# then upload to the Gogs release for TAG.
|
||||
# then upload to the GitHub release for TAG.
|
||||
.PHONY: release-arm64
|
||||
release-arm64: build-arm64 upload-arm64
|
||||
|
||||
@ -36,14 +35,10 @@ build-arm64:
|
||||
.PHONY: upload-arm64
|
||||
upload-arm64:
|
||||
@test -n "$(GOGS_TOKEN)" || (echo "ERROR: set GOGS_TOKEN env var"; exit 1)
|
||||
@RELEASE_ID=$$(curl -sf "$(GOGS_API)/repos/$(GOGS_REPO)/releases/tags/$(TAG)" \
|
||||
-H "Authorization: token $(GOGS_TOKEN)" | \
|
||||
grep -o '"id":[0-9]*' | head -1 | cut -d: -f2); \
|
||||
echo "Release ID: $$RELEASE_ID"; \
|
||||
for f in artifacts/linux-arm64/*; do \
|
||||
@for f in artifacts/linux-arm64/*; do \
|
||||
[ -f "$$f" ] || continue; \
|
||||
echo "Uploading $$f..."; \
|
||||
curl -sf -X POST "$(GOGS_API)/repos/$(GOGS_REPO)/releases/$$RELEASE_ID/assets" \
|
||||
-H "Authorization: token $(GOGS_TOKEN)" \
|
||||
-F "attachment=@$$f;filename=$$(basename $$f)" && echo "OK" || echo "FAIL: $$f"; \
|
||||
NAME="linux-arm64-$$(basename $$f)"; \
|
||||
echo "Uploading $$NAME..."; \
|
||||
GOGS_TOKEN=$(GOGS_TOKEN) # gh release upload $(TAG) "$$f#$$NAME" \
|
||||
--repo $(GOGS_REPO) && echo "OK" || echo "FAIL: $$f"; \
|
||||
done
|
||||
|
||||
416
PLAN.md
@ -1,416 +0,0 @@
|
||||
# TFTSR — IT Triage & Root-Cause Analysis Desktop Application
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Overview
|
||||
|
||||
TFTSR is a **desktop-first, offline-capable** application that helps IT teams
|
||||
perform structured incident triage using the *5-Whys* methodology, backed by
|
||||
pluggable AI providers (Ollama local, OpenAI, Anthropic, Mistral, Gemini).
|
||||
It automates PII redaction, guides engineers through root-cause analysis, and
|
||||
produces post-mortem documents (Markdown / PDF / DOCX).
|
||||
|
||||
---
|
||||
|
||||
## Architecture Decisions
|
||||
|
||||
| Area | Choice | Rationale |
|
||||
|------|--------|-----------|
|
||||
| Desktop framework | **Tauri 2.x** | Small binary, native webview, Rust backend for security |
|
||||
| Frontend framework | **React 18** | Large ecosystem, component model fits wizard-style UX |
|
||||
| State management | **Zustand** | Minimal boilerplate, TypeScript-friendly, no context nesting |
|
||||
| Local database | **SQLCipher** (via `rusqlite` + `bundled-sqlcipher`) | Encrypted SQLite — secrets and PII at rest |
|
||||
| Secret storage | **Tauri Stronghold** | OS-keychain-grade encrypted vault for API keys |
|
||||
| AI providers | Ollama (local), OpenAI, Anthropic, Mistral, Gemini | User choice; local-first with cloud fallback |
|
||||
| Unit tests (frontend) | **Vitest** | Fast, Vite-native, first-class TS support |
|
||||
| E2E tests | **WebdriverIO + tauri-driver** | Official Tauri E2E path, cross-platform |
|
||||
| CI/CD | **Woodpecker CI** (Gogs at `172.0.0.29:3000`) | Self-hosted, Docker-native, YAML pipelines |
|
||||
| Bundling | Vite 6 | Dev server + production build, used by Tauri CLI |
|
||||
|
||||
---
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
tftsr/
|
||||
├── .woodpecker/
|
||||
│ ├── test.yml # lint + unit tests on push / PR
|
||||
│ └── release.yml # multi-platform build on tag
|
||||
├── cli/
|
||||
│ ├── package.json
|
||||
│ └── src/
|
||||
│ └── main.ts # minimal CLI entry point
|
||||
├── src/ # React frontend
|
||||
│ ├── assets/
|
||||
│ ├── components/
|
||||
│ │ ├── common/ # Button, Card, Modal, DropZone …
|
||||
│ │ ├── dashboard/ # IssueList, StatsCards
|
||||
│ │ ├── triage/ # WhyStep, ChatBubble, ProgressBar
|
||||
│ │ ├── rca/ # DocEditor, ExportBar
|
||||
│ │ ├── settings/ # ProviderForm, ThemeToggle
|
||||
│ │ └── pii/ # PiiHighlighter, RedactionPreview
|
||||
│ ├── hooks/ # useInvoke, useListener, useTheme …
|
||||
│ ├── lib/
|
||||
│ │ ├── tauriCommands.ts # typed invoke wrappers & TS types
|
||||
│ │ └── utils.ts # date formatting, debounce, etc.
|
||||
│ ├── pages/
|
||||
│ │ ├── DashboardPage.tsx
|
||||
│ │ ├── NewIssuePage.tsx
|
||||
│ │ ├── TriagePage.tsx
|
||||
│ │ ├── RcaPage.tsx
|
||||
│ │ ├── LogViewerPage.tsx
|
||||
│ │ └── SettingsPage.tsx
|
||||
│ ├── stores/
|
||||
│ │ ├── sessionStore.ts # current triage session state
|
||||
│ │ └── settingsStore.ts # theme, providers, preferences
|
||||
│ ├── App.tsx
|
||||
│ └── main.tsx
|
||||
├── src-tauri/
|
||||
│ ├── Cargo.toml
|
||||
│ ├── tauri.conf.json
|
||||
│ ├── capabilities/
|
||||
│ │ └── default.json
|
||||
│ ├── icons/
|
||||
│ ├── src/
|
||||
│ │ ├── main.rs # Tauri entry point
|
||||
│ │ ├── db.rs # SQLCipher connection & migrations
|
||||
│ │ ├── commands/ # IPC command modules
|
||||
│ │ │ ├── mod.rs
|
||||
│ │ │ ├── issues.rs
|
||||
│ │ │ ├── triage.rs
|
||||
│ │ │ ├── logs.rs
|
||||
│ │ │ ├── pii.rs
|
||||
│ │ │ ├── rca.rs
|
||||
│ │ │ ├── ai.rs
|
||||
│ │ │ └── settings.rs
|
||||
│ │ ├── ai/ # AI provider abstractions
|
||||
│ │ │ ├── mod.rs
|
||||
│ │ │ ├── ollama.rs
|
||||
│ │ │ ├── openai_compat.rs
|
||||
│ │ │ └── prompt_templates.rs
|
||||
│ │ ├── pii/ # PII detection engine
|
||||
│ │ │ ├── mod.rs
|
||||
│ │ │ └── patterns.rs
|
||||
│ │ └── export/ # Document export
|
||||
│ │ ├── mod.rs
|
||||
│ │ ├── markdown.rs
|
||||
│ │ ├── pdf.rs
|
||||
│ │ └── docx.rs
|
||||
│ └── migrations/
|
||||
│ └── 001_init.sql
|
||||
├── tests/
|
||||
│ ├── unit/
|
||||
│ │ ├── setup.ts
|
||||
│ │ ├── pii.test.ts
|
||||
│ │ ├── sessionStore.test.ts
|
||||
│ │ └── settingsStore.test.ts
|
||||
│ └── e2e/
|
||||
│ ├── wdio.conf.ts
|
||||
│ ├── helpers/
|
||||
│ │ └── app.ts
|
||||
│ └── specs/
|
||||
│ ├── onboarding.spec.ts
|
||||
│ ├── log-upload.spec.ts
|
||||
│ ├── triage-flow.spec.ts
|
||||
│ └── rca-export.spec.ts
|
||||
├── package.json
|
||||
├── tsconfig.json
|
||||
├── vite.config.ts
|
||||
└── PLAN.md # ← this file
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Schema (SQLCipher)
|
||||
|
||||
All tables live in a single encrypted `tftsr.db` file under the Tauri
|
||||
app-data directory.
|
||||
|
||||
### 1. `issues`
|
||||
```sql
|
||||
CREATE TABLE issues (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
domain TEXT NOT NULL CHECK(domain IN
|
||||
('linux','windows','network','k8s','db','virt','hw','obs')),
|
||||
status TEXT NOT NULL DEFAULT 'open'
|
||||
CHECK(status IN ('open','triaging','resolved','closed')),
|
||||
severity TEXT CHECK(severity IN ('p1','p2','p3','p4')),
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
### 2. `triage_messages`
|
||||
```sql
|
||||
CREATE TABLE triage_messages (
|
||||
id TEXT PRIMARY KEY,
|
||||
issue_id TEXT NOT NULL REFERENCES issues(id),
|
||||
role TEXT NOT NULL CHECK(role IN ('user','assistant','system')),
|
||||
content TEXT NOT NULL,
|
||||
why_level INTEGER NOT NULL DEFAULT 0,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE INDEX idx_triage_msg_issue ON triage_messages(issue_id);
|
||||
```
|
||||
|
||||
### 3. `log_files`
|
||||
```sql
|
||||
CREATE TABLE log_files (
|
||||
id TEXT PRIMARY KEY,
|
||||
issue_id TEXT NOT NULL REFERENCES issues(id),
|
||||
filename TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
mime_type TEXT,
|
||||
size_bytes INTEGER,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
### 4. `pii_spans`
|
||||
```sql
|
||||
CREATE TABLE pii_spans (
|
||||
id TEXT PRIMARY KEY,
|
||||
log_file_id TEXT NOT NULL REFERENCES log_files(id),
|
||||
pii_type TEXT NOT NULL,
|
||||
start_pos INTEGER NOT NULL,
|
||||
end_pos INTEGER NOT NULL,
|
||||
original TEXT NOT NULL,
|
||||
replacement TEXT NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
### 5. `rca_documents`
|
||||
```sql
|
||||
CREATE TABLE rca_documents (
|
||||
id TEXT PRIMARY KEY,
|
||||
issue_id TEXT NOT NULL REFERENCES issues(id) UNIQUE,
|
||||
content TEXT NOT NULL DEFAULT '',
|
||||
format TEXT NOT NULL DEFAULT 'markdown',
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
### 6. `ai_providers`
|
||||
```sql
|
||||
CREATE TABLE ai_providers (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
api_url TEXT NOT NULL,
|
||||
model TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
### 7. `settings`
|
||||
```sql
|
||||
CREATE TABLE settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
### 8. `export_history`
|
||||
```sql
|
||||
CREATE TABLE export_history (
|
||||
id TEXT PRIMARY KEY,
|
||||
issue_id TEXT NOT NULL REFERENCES issues(id),
|
||||
format TEXT NOT NULL CHECK(format IN ('md','pdf','docx')),
|
||||
file_path TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## IPC Command Interface
|
||||
|
||||
All frontend ↔ backend communication goes through Tauri's `invoke()`.
|
||||
|
||||
### Issue commands
|
||||
| Command | Payload | Returns |
|
||||
|---------|---------|---------|
|
||||
| `create_issue` | `{ title, domain, severity }` | `Issue` |
|
||||
| `list_issues` | `{ status?, domain? }` | `Issue[]` |
|
||||
| `get_issue` | `{ id }` | `Issue` |
|
||||
| `update_issue` | `{ id, title?, status?, severity? }` | `Issue` |
|
||||
| `delete_issue` | `{ id }` | `void` |
|
||||
|
||||
### Triage commands
|
||||
| Command | Payload | Returns |
|
||||
|---------|---------|---------|
|
||||
| `send_triage_message` | `{ issueId, content, whyLevel }` | `TriageMessage` (assistant reply) |
|
||||
| `get_triage_history` | `{ issueId }` | `TriageMessage[]` |
|
||||
| `set_why_level` | `{ issueId, level }` | `void` |
|
||||
|
||||
### Log commands
|
||||
| Command | Payload | Returns |
|
||||
|---------|---------|---------|
|
||||
| `upload_log` | `{ issueId, filename, content }` | `LogFile` |
|
||||
| `list_logs` | `{ issueId }` | `LogFile[]` |
|
||||
| `delete_log` | `{ id }` | `void` |
|
||||
|
||||
### PII commands
|
||||
| Command | Payload | Returns |
|
||||
|---------|---------|---------|
|
||||
| `detect_pii` | `{ logFileId }` | `PiiDetectionResult` |
|
||||
| `apply_redactions` | `{ logFileId, spanIds }` | `string` (redacted text) |
|
||||
|
||||
### RCA / Export commands
|
||||
| Command | Payload | Returns |
|
||||
|---------|---------|---------|
|
||||
| `generate_rca` | `{ issueId }` | `RcaDocument` |
|
||||
| `update_rca` | `{ id, content }` | `RcaDocument` |
|
||||
| `export_document` | `{ issueId, format }` | `string` (file path) |
|
||||
|
||||
### AI / Settings commands
|
||||
| Command | Payload | Returns |
|
||||
|---------|---------|---------|
|
||||
| `test_provider` | `{ name, apiUrl, apiKey?, model }` | `{ ok, message }` |
|
||||
| `save_provider` | `{ provider }` | `void` |
|
||||
| `get_settings` | `{}` | `Settings` |
|
||||
| `update_settings` | `{ key, value }` | `void` |
|
||||
|
||||
---
|
||||
|
||||
## CI/CD Approach
|
||||
|
||||
### Infrastructure
|
||||
- **Git server**: Gogs at `http://172.0.0.29:3000`
|
||||
- **CI runner**: Woodpecker CI with Docker executor
|
||||
- **Artifacts**: Uploaded to Gogs releases via API
|
||||
|
||||
### Pipelines
|
||||
|
||||
| Pipeline | Trigger | Steps |
|
||||
|----------|---------|-------|
|
||||
| `.woodpecker/test.yml` | push, PR | `rustfmt` check → Clippy → Rust tests → TS typecheck → Vitest → coverage (main only) |
|
||||
| `.woodpecker/release.yml` | `v*` tag | Build linux-amd64 → Build linux-arm64 → Upload to Gogs release |
|
||||
|
||||
---
|
||||
|
||||
## Security Implementation
|
||||
|
||||
1. **Database encryption** — SQLCipher with a key derived from Tauri Stronghold.
|
||||
2. **API key storage** — Stronghold vault, never stored in plaintext.
|
||||
3. **PII redaction** — Regex + heuristic engine runs before any text leaves the device.
|
||||
4. **CSP** — Strict Content-Security-Policy in `tauri.conf.json`; only allowlisted AI API origins.
|
||||
5. **Least-privilege capabilities** — `capabilities/default.json` grants only required Tauri permissions.
|
||||
6. **No remote code** — All assets bundled; no CDN scripts.
|
||||
|
||||
---
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
| Layer | Tool | Location | What it covers |
|
||||
|-------|------|----------|----------------|
|
||||
| Rust unit | `cargo test` | `src-tauri/src/**` | DB operations, PII regex, AI prompt building |
|
||||
| Frontend unit | Vitest | `tests/unit/` | Stores, command wrappers, component logic |
|
||||
| E2E | WebdriverIO + tauri-driver | `tests/e2e/` | Full user flows: onboarding, triage, export |
|
||||
| Lint | `rustfmt` + Clippy + `tsc --noEmit` | CI | Code style, type safety |
|
||||
|
||||
---
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1 — Project Scaffold & CI ✅ COMPLETE
|
||||
- [x] Initialise repo with Tauri 2.x + React 18 + Vite
|
||||
- [x] Configure `tauri.conf.json` and capabilities
|
||||
- [x] Set up Woodpecker CI pipelines (`test.yml`, `release.yml`)
|
||||
- [x] Write Vitest setup and mock harness
|
||||
- [x] Write initial unit tests (PII, sessionStore, settingsStore) — 13/13 passing
|
||||
- [x] Write E2E scaffolding (wdio config, helpers, skeleton specs)
|
||||
- [x] Create CLI stub (`cli/`)
|
||||
- [x] Push to Gogs at http://172.0.0.29:3000/sarman/tftsr-devops_investigation
|
||||
- [x] Write README.md
|
||||
- [x] Deploy Woodpecker CI v0.15.4 (server + agent + nginx proxy)
|
||||
- [ ] **BLOCKED**: Verify CI green on push (Woodpecker hook auth issue — see below)
|
||||
|
||||
### Phase 2 — Database & Migrations ✅ COMPLETE
|
||||
- [x] Integrate `rusqlite` + `bundled-sqlcipher`
|
||||
- [x] Write migrations (10 tables: issues, log_files, pii_spans, ai_conversations, ai_messages, resolution_steps, documents, audit_log, settings, integration_publishes)
|
||||
- [x] Implement migration runner in `db/migrations.rs`
|
||||
- [x] DB models with all required types
|
||||
|
||||
### Phase 3 — Stronghold Integration ✅ COMPLETE (scaffold)
|
||||
- [x] `tauri-plugin-stronghold` registered in `lib.rs`
|
||||
- [x] Password derivation function configured
|
||||
- [ ] Full key lifecycle tests (deferred to Phase 3 proper)
|
||||
|
||||
### Phase 4 — Issue CRUD ✅ COMPLETE
|
||||
- [x] All issue CRUD commands: create, get, list, update, delete, search
|
||||
- [x] 5-Whys tracking: add_five_why, update_five_why
|
||||
- [x] Timeline events: add_timeline_event
|
||||
- [x] Dashboard, NewIssue, History pages
|
||||
|
||||
### Phase 5 — Log Ingestion & PII Detection ✅ COMPLETE
|
||||
- [x] `upload_log_file`, `detect_pii`, `apply_redactions` commands
|
||||
- [x] PII engine: 11 regex patterns (IPv4, IPv6, email, phone, SSN, CC, MAC, bearer, password, API key, URL)
|
||||
- [x] PiiDiffViewer component
|
||||
- [x] LogUpload page
|
||||
|
||||
### Phase 6 — AI Provider Abstraction ✅ COMPLETE
|
||||
- [x] OpenAI-compatible, Anthropic, Gemini, Mistral, Ollama providers
|
||||
- [x] `analyze_logs`, `chat_message`, `list_providers` IPC commands
|
||||
- [x] Settings/AIProviders page
|
||||
- [x] 8 IT domain system prompts
|
||||
|
||||
### Phase 7 — 5-Whys Triage Engine ✅ COMPLETE
|
||||
- [x] Triage page with ChatWindow
|
||||
- [x] TriageProgress component (5-step indicator)
|
||||
- [x] Auto-detection of why level from AI responses
|
||||
- [x] Session store with message persistence
|
||||
|
||||
### Phase 8 — RCA & Post-Mortem Generation ✅ COMPLETE
|
||||
- [x] `generate_rca`, `generate_postmortem` commands
|
||||
- [x] RCA and post-mortem Markdown templates
|
||||
- [x] DocEditor component with export (MD, PDF)
|
||||
- [x] RCA and Postmortem pages
|
||||
|
||||
### Phase 9 — Document Export ✅ COMPLETE (MD + PDF)
|
||||
- [x] Markdown export
|
||||
- [x] PDF export via `printpdf`
|
||||
- [ ] DOCX export (not yet implemented — docx-rs dep removed for simplicity)
|
||||
|
||||
### Phase 10 — Polish & Settings ✅ COMPLETE
|
||||
- [x] Dark/light theme via Tailwind + CSS variables
|
||||
- [x] Ollama settings page with hardware detection + model management
|
||||
- [x] Security page with audit log
|
||||
- [x] Integrations page (v0.2 stubs)
|
||||
|
||||
### Phase 11 — Woodpecker CI Integration ✅ COMPLETE
|
||||
- [x] Woodpecker CI v0.15.4 deployed at http://172.0.0.29:8084
|
||||
- [x] Webhook delivery: Gogs pushes trigger Woodpecker via `?access_token=<JWT>`
|
||||
- [x] Repo activated (DB direct): `repo_active=1`, `repo_trusted=1`, `repo_config_path=.woodpecker/test.yml`
|
||||
- [x] Clone override: `CI_REPO_CLONE_URL` + `network_mode: gogs_default` for step containers
|
||||
- [x] All CI steps green (build #19): fmt → clippy → rust-tests (64/64) → ts-check → vitest
|
||||
- [x] Token security: old tokens rotated, removed from git history, `.gitignore` updated
|
||||
- [x] Gogs repo set to public (for unauthenticated clone from step containers)
|
||||
|
||||
### Phase 12 — Release Package 🔲 PENDING
|
||||
- [ ] Tag v0.1.0-alpha
|
||||
- [ ] Verify Woodpecker builds Linux amd64 + arm64
|
||||
- [ ] Verify artifacts upload to Gogs release
|
||||
- [ ] Smoke-test installed packages
|
||||
|
||||
---
|
||||
|
||||
## Known Issues & Gotchas
|
||||
|
||||
### Gogs Token Authentication
|
||||
- The `sha1` in the Gogs CREATE token API response IS the actual bearer token
|
||||
- Gogs stores `sha1(token)` and `sha256(token)` in the DB — these are HASHES, not the token itself
|
||||
- Woodpecker user token stored in Woodpecker SQLite DB only (never commit token values)
|
||||
|
||||
### Woodpecker CI + Gogs v0.15.4 Compatibility
|
||||
- The SPA form login uses `login=` field but Gogs backend reads `username=`
|
||||
- Workaround: nginx proxy at :8085 serves custom HTML login page
|
||||
- The webhook `?token=` URL param is NOT read by Woodpecker's `token.ParseRequest()`
|
||||
- Use `?access_token=<JWT>` instead (JWT must be HS256 signed with `repo_hash` as key)
|
||||
- Gogs 0.14 has no OAuth2 provider support — blocks upgrade to Woodpecker 2.x
|
||||
|
||||
### Rust/DB Type Notes
|
||||
- IssueDetail is NESTED: `{ issue: Issue, log_files, resolution_steps, conversations }`
|
||||
- DB uses TEXT timestamps for created_at/updated_at (not INTEGER)
|
||||
- All commands use the `and_then` pattern with rusqlite to avoid lifetime issues
|
||||
12
README.md
@ -1,3 +1,5 @@
|
||||

|
||||
|
||||
# Troubleshooting and RCA Assistant
|
||||
|
||||
A structured, AI-backed desktop tool for IT incident triage, 5-Whys root cause analysis, RCA document generation, and blameless post-mortems. Runs fully offline via Ollama local models, or connects to cloud AI providers.
|
||||
@ -258,7 +260,7 @@ The project uses **Gitea Actions** (act_runner v0.3.1) connected to the Gitea in
|
||||
|
||||
| Runner | Platform | Host | Purpose |
|
||||
|---|---|---|---|
|
||||
| `amd64-docker-runner` | linux/amd64 | 172.0.0.29 (Docker) | Test pipeline + amd64/windows release builds |
|
||||
| `amd64-docker-runner` | linux/amd64 | gitea.tftsr.com (Docker) | Test pipeline + amd64/windows release builds |
|
||||
| `arm64-native-runner` | linux/arm64 | Local arm64 machine | Native arm64 release builds |
|
||||
|
||||
**Branch protection:** master requires a PR approved by `sarman`, with all 5 CI checks passing before merge.
|
||||
@ -292,7 +294,7 @@ All data is stored locally in a SQLCipher-encrypted database at:
|
||||
| macOS | `~/Library/Application Support/tftsr/tftsr.db` |
|
||||
| Windows | `%APPDATA%\tftsr\tftsr.db` |
|
||||
|
||||
Override with the `TFTSR_DATA_DIR` environment variable.
|
||||
Override with the `TRCAA_DATA_DIR` (or legacy `TRCAA_DATA_DIR`) environment variable.
|
||||
|
||||
---
|
||||
|
||||
@ -300,9 +302,9 @@ Override with the `TFTSR_DATA_DIR` environment variable.
|
||||
|
||||
| Variable | Default | Purpose |
|
||||
|---|---|---|
|
||||
| `TFTSR_DATA_DIR` | Platform data dir | Override database location |
|
||||
| `TFTSR_DB_KEY` | _(none)_ | Database encryption key (required in release builds) |
|
||||
| `TFTSR_ENCRYPTION_KEY` | _(none)_ | Credential encryption key (required in release builds) |
|
||||
| `TRCAA_DATA_DIR` (or legacy `TRCAA_DATA_DIR`) | Platform data dir | Override database location |
|
||||
| `TRCAA_DB_KEY` (or legacy `TRCAA_DB_KEY`) | _(none)_ | Database encryption key (required in release builds) |
|
||||
| `TRCAA_ENCRYPTION_KEY` (or legacy `TRCAA_ENCRYPTION_KEY`) | _(none)_ | Credential encryption key (required in release builds) |
|
||||
| `RUST_LOG` | `info` | Tracing log level (`debug`, `info`, `warn`, `error`) |
|
||||
|
||||
---
|
||||
|
||||
@ -1,335 +0,0 @@
|
||||
# Security Audit Report
|
||||
|
||||
**Application**: Troubleshooting and RCA Assistant (TRCAA)
|
||||
**Audit Date**: 2026-04-06
|
||||
**Scope**: All git-tracked source files (159 files)
|
||||
**Context**: Pre-open-source release under MIT license
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
The codebase is generally well-structured with several positive security practices already in place: parameterized SQL queries, AES-256-GCM credential encryption, PKCE for OAuth flows, PII detection and redaction before AI transmission, hash-chained audit logs, and a restrictive CSP. However, the audit identified **3 CRITICAL**, **5 HIGH**, **5 MEDIUM**, and **5 LOW** findings that must be addressed before public release.
|
||||
|
||||
---
|
||||
|
||||
## CRITICAL Findings
|
||||
|
||||
### C1. Corporate-Internal Documents Shipped in Repository
|
||||
|
||||
**Files**:
|
||||
- `GenAI API User Guide.md` (entire file)
|
||||
- `HANDOFF-MSI-GENAI.md` (entire file)
|
||||
|
||||
**Issue**: These files contain proprietary Motorola Solutions / MSI internal documentation. `GenAI API User Guide.md` is authored by named MSI employees (Dipjyoti Bisharad, Jahnavi Alike, Sunil Vurandur, Anjali Kamath, Vibin Jacob, Girish Manivel) and documents internal API contracts at `genai-service.stage.commandcentral.com` and `genai-service.commandcentral.com`. `HANDOFF-MSI-GENAI.md` explicitly references "MSI GenAI API" integration details including internal endpoint URLs, header formats, and payload contracts.
|
||||
|
||||
Publishing these files under MIT license likely violates corporate IP agreements and exposes internal infrastructure details.
|
||||
|
||||
**Recommended Fix**: Remove both files from the repository entirely and scrub from git history using `git filter-repo` before making the repo public.
|
||||
|
||||
---
|
||||
|
||||
### C2. Internal Infrastructure URLs Hardcoded in CSP and Source
|
||||
|
||||
**File**: `src-tauri/tauri.conf.json`, line 13
|
||||
**Also**: `src-tauri/src/ai/openai.rs`, line 219
|
||||
|
||||
**Issue**: The CSP `connect-src` directive includes corporate-internal endpoints:
|
||||
```
|
||||
https://genai-service.stage.commandcentral.com
|
||||
https://genai-service.commandcentral.com
|
||||
```
|
||||
|
||||
Additionally, `openai.rs` line 219 sends `X-msi-genai-client: troubleshooting-rca-assistant` as a hardcoded header in the custom REST path, tying the application to an internal MSI service.
|
||||
|
||||
These expose internal service infrastructure to anyone reading the source and indicate the app was designed to interact with corporate systems.
|
||||
|
||||
**Recommended Fix**:
|
||||
- Remove the two `commandcentral.com` entries from the CSP.
|
||||
- Remove or make the `X-msi-genai-client` header configurable rather than hardcoded.
|
||||
- Audit the CSP to ensure only generic/public endpoints remain (OpenAI, Anthropic, Mistral, Google, Ollama, Atlassian, Microsoft are fine).
|
||||
|
||||
---
|
||||
|
||||
### C3. Private Gogs Server IP Exposed in All CI Workflows
|
||||
|
||||
**Files**:
|
||||
- `.gitea/workflows/test.yml` (lines 17, 44, 72, 99, 126)
|
||||
- `.gitea/workflows/auto-tag.yml` (lines 31, 52, 79, 95, 97, 141, 162, 227, 252, 313, 338, 401, 464)
|
||||
- `.gitea/workflows/build-images.yml` (lines 4, 10, 11, 16-18, 33, 46, 69, 92)
|
||||
|
||||
**Issue**: All CI workflow files reference `172.0.0.29:3000` (a private Gogs instance) and `sarman` username. While the IP is RFC1918 private address space, it reveals internal infrastructure topology and the developer's username across dozens of lines. The `build-images.yml` also exposes `REGISTRY_USER: sarman` and container registry details.
|
||||
|
||||
**Recommended Fix**: Before open-sourcing, replace all workflow files with GitHub Actions equivalents, or at minimum replace the hardcoded private IP and username with parameterized variables or remove the `.gitea/` directory entirely if moving to GitHub.
|
||||
|
||||
---
|
||||
|
||||
## HIGH Findings
|
||||
|
||||
### H1. Hardcoded Development Encryption Key in Auth Module
|
||||
|
||||
**File**: `src-tauri/src/integrations/auth.rs`, line 179
|
||||
|
||||
```rust
|
||||
return Ok("dev-key-change-me-in-production-32b".to_string());
|
||||
```
|
||||
|
||||
**Issue**: In debug builds, the credential encryption key is a well-known hardcoded string. Anyone reading the source can decrypt any credentials stored by a debug build. Since this is about to be open source, attackers know the exact key to use against any debug-mode installation.
|
||||
|
||||
**Also at**: `src-tauri/src/db/connection.rs`, line 39: `"dev-key-change-in-prod"`
|
||||
|
||||
While this is gated behind `cfg!(debug_assertions)`, open-sourcing the code means the development key is permanently public knowledge. If any user runs a debug build or if the release profile check is ever misconfigured, all stored credentials are trivially decryptable.
|
||||
|
||||
**Recommended Fix**:
|
||||
- Remove the hardcoded dev key entirely.
|
||||
- In debug mode, auto-generate and persist a random key the same way the release path does (lines 44-57 of `connection.rs` already implement this pattern).
|
||||
- Document in a `SECURITY.md` file that credentials are encrypted at rest and the key management approach.
|
||||
|
||||
---
|
||||
|
||||
### H2. Encryption Key Derivation Uses Raw SHA-256 Instead of a KDF
|
||||
|
||||
**File**: `src-tauri/src/integrations/auth.rs`, lines 185-191
|
||||
|
||||
```rust
|
||||
fn derive_aes_key() -> Result<[u8; 32], String> {
|
||||
let key_material = get_encryption_key_material()?;
|
||||
let digest = Sha256::digest(key_material.as_bytes());
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
**Issue**: The AES-256-GCM key is derived from the raw material by a single SHA-256 hash. There is no salt and no iteration count. This means if the key material has low entropy (as the dev key does), the derived key is trivially brute-forceable. In contrast, the database encryption properly uses PBKDF2-HMAC-SHA512 with 256,000 iterations (line 69 of `connection.rs`).
|
||||
|
||||
**Recommended Fix**: Use a proper KDF (PBKDF2, Argon2, or HKDF) with a persisted random salt and sufficient iteration count for deriving the AES key. The `db/connection.rs` module already demonstrates the correct approach.
|
||||
|
||||
---
|
||||
|
||||
### H3. Release Build Fails Open if TFTSR_ENCRYPTION_KEY is Unset
|
||||
|
||||
**File**: `src-tauri/src/integrations/auth.rs`, line 182
|
||||
|
||||
```rust
|
||||
Err("TFTSR_ENCRYPTION_KEY must be set in release builds".to_string())
|
||||
```
|
||||
|
||||
**Issue**: In release mode, if the `TFTSR_ENCRYPTION_KEY` environment variable is not set, any attempt to store or retrieve credentials will fail with an error. Unlike the database key management (which auto-generates and persists a key), credential encryption requires manual environment variable configuration. For a desktop app distributed to end users, this is an unworkable UX: users will never set this variable, meaning credential storage will be broken out of the box in release builds.
|
||||
|
||||
**Recommended Fix**: Mirror the database key management pattern: auto-generate a random key on first use, persist it to a file in the app data directory with 0600 permissions (as already done for `.dbkey`), and read it back on subsequent launches.
|
||||
|
||||
---
|
||||
|
||||
### H4. API Keys Transmitted to Frontend via IPC and Stored in Memory
|
||||
|
||||
**File**: `src/stores/settingsStore.ts`, lines 56-63
|
||||
**Also**: `src-tauri/src/state.rs`, line 12 (`api_key` field in `ProviderConfig`)
|
||||
|
||||
**Issue**: The `ProviderConfig` struct includes `api_key: String` which is serialized over Tauri's IPC bridge from Rust to TypeScript and back. The settings store correctly strips API keys before persisting to `localStorage` (line 60: `api_key: ""`), which is good. However, the full API key lives in the Zustand store in browser memory for the duration of the session. If the webview's JavaScript context is compromised (e.g., via a future XSS or a malicious Tauri plugin), the API key is accessible.
|
||||
|
||||
**Recommended Fix**: Store API keys exclusively in the Rust backend (encrypted in the database). The frontend should only send a provider identifier; the backend should look up the key internally before making API calls. This eliminates API keys from the IPC surface entirely.
|
||||
|
||||
---
|
||||
|
||||
### H5. Filesystem Capabilities Are Overly Broad
|
||||
|
||||
**File**: `src-tauri/capabilities/default.json`, lines 16-24
|
||||
|
||||
```json
|
||||
"fs:allow-read",
|
||||
"fs:allow-write",
|
||||
"fs:allow-mkdir",
|
||||
```
|
||||
|
||||
**Issue**: The capabilities include `fs:allow-read` and `fs:allow-write` without scope constraints (in addition to the properly scoped `fs:scope-app-recursive` and `fs:scope-temp-recursive`). The unscoped `fs:allow-read`/`fs:allow-write` permissions may override the scope restrictions, potentially allowing the frontend JavaScript to read or write arbitrary files on the filesystem depending on Tauri 2.x ACL resolution order.
|
||||
|
||||
**Recommended Fix**: Remove the unscoped `fs:allow-read`, `fs:allow-write`, and `fs:allow-mkdir` permissions. Keep only the scoped variants (`fs:allow-app-read-recursive`, `fs:allow-app-write-recursive`, `fs:allow-temp-read-recursive`, `fs:allow-temp-write-recursive`) plus the `fs:scope-*` directives. File dialog operations (`dialog:allow-open`, `dialog:allow-save`) already handle user-initiated file access.
|
||||
|
||||
---
|
||||
|
||||
## MEDIUM Findings
|
||||
|
||||
### M1. Export Document Accepts Arbitrary Output Directory Without Validation
|
||||
|
||||
**File**: `src-tauri/src/commands/docs.rs`, lines 154-162
|
||||
|
||||
```rust
|
||||
let base_dir = if output_dir.is_empty() || output_dir == "." {
|
||||
dirs::download_dir().unwrap_or_else(|| { ... })
|
||||
} else {
|
||||
PathBuf::from(&output_dir)
|
||||
};
|
||||
```
|
||||
|
||||
**Issue**: The `export_document` command accepts an `output_dir` string from the frontend and writes files to it without canonicalization or path validation. While the frontend likely provides a dialog-selected path, a compromised frontend could write files to arbitrary directories (e.g., `../../etc/cron.d/` on Linux). There is no check that `output_dir` is within an expected scope.
|
||||
|
||||
**Recommended Fix**: Canonicalize the path and validate it against an allowlist of directories (Downloads, app data, or user-selected via dialog). Reject paths containing `..` or pointing to system directories.
|
||||
|
||||
---
|
||||
|
||||
### M2. OAuth Callback Server Listens on Fixed Port Without CSRF Protection
|
||||
|
||||
**File**: `src-tauri/src/integrations/callback_server.rs`, lines 14-33
|
||||
|
||||
**Issue**: The OAuth callback server binds to `127.0.0.1:8765`. While binding to localhost is correct, the server accepts any HTTP GET to `/callback?code=...&state=...` without verifying the origin of the request. A malicious local process or a webpage with access to `localhost` could forge a callback request. The `state` parameter provides some CSRF protection, but it is stored in a global `HashMap` without TTL, meaning stale state values persist indefinitely.
|
||||
|
||||
**Recommended Fix**:
|
||||
- Add a TTL (e.g., 10 minutes) to OAuth state entries to prevent stale state accumulation.
|
||||
- Consider using a random high port instead of the fixed 8765 to reduce predictability.
|
||||
|
||||
---
|
||||
|
||||
### M3. Audit Log Hash Chain is Appendable but Not Verifiable
|
||||
|
||||
**File**: `src-tauri/src/audit/log.rs`, lines 4-16
|
||||
|
||||
**Issue**: The audit log implements a hash chain (each entry includes the hash of the previous entry), which is good for tamper detection. However, there is no command or function to verify the integrity of the chain. An attacker with database access could modify entries and recompute all subsequent hashes. Without an external anchor (e.g., periodic hash checkpoint to an external store), the chain only proves ordering, not immutability.
|
||||
|
||||
**Recommended Fix**: Add a `verify_audit_chain()` function and consider periodically exporting chain checkpoints to a file outside the database. Document the threat model in `SECURITY.md`.
|
||||
|
||||
---
|
||||
|
||||
### M4. Non-Windows Key File Permissions Not Enforced
|
||||
|
||||
**File**: `src-tauri/src/db/connection.rs`, lines 25-28
|
||||
|
||||
```rust
|
||||
#[cfg(not(unix))]
|
||||
fn write_key_file(path: &Path, key: &str) -> anyhow::Result<()> {
|
||||
std::fs::write(path, key)?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
**Issue**: On non-Unix platforms (Windows), the database key file is written with default permissions, potentially making it world-readable. The Unix path correctly uses mode `0o600`.
|
||||
|
||||
**Recommended Fix**: On Windows, use platform-specific ACL APIs to restrict the key file to the current user, or at minimum document this limitation.
|
||||
|
||||
---
|
||||
|
||||
### M5. `unsafe-inline` in Style CSP Directive
|
||||
|
||||
**File**: `src-tauri/tauri.conf.json`, line 13
|
||||
|
||||
```
|
||||
style-src 'self' 'unsafe-inline'
|
||||
```
|
||||
|
||||
**Issue**: The CSP allows `unsafe-inline` for styles. While this is common in React/Tailwind applications and the attack surface is lower than `unsafe-inline` for scripts, it still permits style-based data exfiltration attacks (e.g., CSS injection to leak attribute values).
|
||||
|
||||
**Recommended Fix**: If feasible, use nonce-based or hash-based style CSP. If not feasible due to Tailwind's runtime style injection, document this as an accepted risk.
|
||||
|
||||
---
|
||||
|
||||
## LOW Findings
|
||||
|
||||
### L1. `http:default` Capability Grants Broad Network Access
|
||||
|
||||
**File**: `src-tauri/capabilities/default.json`, line 28
|
||||
|
||||
**Issue**: The `http:default` permission allows the frontend to make arbitrary HTTP requests. Combined with the broad CSP `connect-src`, this gives the webview significant network access. For a desktop app this is often necessary, but it should be documented and reviewed.
|
||||
|
||||
**Recommended Fix**: Consider restricting `http` permissions to specific URL patterns matching only the known AI provider APIs and integration endpoints.
|
||||
|
||||
---
|
||||
|
||||
### L2. IntelliJ IDEA Config Files Tracked in Git
|
||||
|
||||
**Files**:
|
||||
- `.idea/.gitignore`
|
||||
- `.idea/copilot.data.migration.ask2agent.xml`
|
||||
- `.idea/misc.xml`
|
||||
- `.idea/modules.xml`
|
||||
- `.idea/tftsr-devops_investigation.iml`
|
||||
- `.idea/vcs.xml`
|
||||
|
||||
**Issue**: IDE configuration files are tracked. These may leak editor preferences and do not belong in an open-source repository.
|
||||
|
||||
**Recommended Fix**: Add `.idea/` to `.gitignore` and remove from tracking with `git rm -r --cached .idea/`.
|
||||
|
||||
---
|
||||
|
||||
### L3. Placeholder OAuth Client IDs in Source
|
||||
|
||||
**File**: `src-tauri/src/commands/integrations.rs`, lines 181, 187
|
||||
|
||||
```rust
|
||||
"confluence-client-id-placeholder"
|
||||
"ado-client-id-placeholder"
|
||||
```
|
||||
|
||||
**Issue**: These placeholder strings are used as fallbacks when environment variables are not set. While they are obviously not real credentials, they could confuse users or be mistaken for actual client IDs in bug reports.
|
||||
|
||||
**Recommended Fix**: Make the OAuth flow fail explicitly with a clear error message when the client ID environment variable is not set, rather than falling back to a placeholder.
|
||||
|
||||
---
|
||||
|
||||
### L4. Username `sarman` Embedded in CI Workflows and Makefile
|
||||
|
||||
**Files**: `.gitea/workflows/*.yml`, `Makefile` line 2
|
||||
|
||||
**Issue**: The developer's username appears throughout CI configuration. While not a security vulnerability per se, it is a privacy concern for open-source release.
|
||||
|
||||
**Recommended Fix**: Parameterize the username in CI workflows. Update the Makefile to use a generic repository reference.
|
||||
|
||||
---
|
||||
|
||||
### L5. `shell:allow-open` Capability Enabled
|
||||
|
||||
**File**: `src-tauri/capabilities/default.json`, line 27
|
||||
|
||||
**Issue**: The `shell:allow-open` permission allows the frontend to open URLs in the system browser. This is used for OAuth flows and external links. While convenient, a compromised frontend could open arbitrary URLs.
|
||||
|
||||
**Recommended Fix**: This is acceptable for the app's functionality but should be documented. Consider restricting to specific URL patterns if Tauri 2.x supports it.
|
||||
|
||||
---
|
||||
|
||||
## Positive Security Observations
|
||||
|
||||
The following practices are already well-implemented:
|
||||
|
||||
1. **Parameterized SQL queries**: All database operations use `rusqlite::params![]` with positional parameters. No string interpolation in SQL. The dynamic query builder in `list_issues` and `get_audit_log` correctly uses indexed parameter placeholders.
|
||||
|
||||
2. **SQLCipher encryption at rest**: Release builds encrypt the database using AES-256-CBC via SQLCipher with PBKDF2-HMAC-SHA512 (256k iterations).
|
||||
|
||||
3. **PII detection and mandatory redaction**: Log files must pass PII detection and redaction before being sent to AI providers (`redacted_path_for()` enforces this check).
|
||||
|
||||
4. **PKCE for OAuth**: The OAuth implementation uses PKCE (S256) with cryptographically random verifiers.
|
||||
|
||||
5. **Hash-chained audit log**: Every security-relevant action is logged with a SHA-256 hash chain.
|
||||
|
||||
6. **Path traversal prevention**: `upload_log_file` uses `std::fs::canonicalize()` and validates the result is a regular file with size limits.
|
||||
|
||||
7. **No `dangerouslySetInnerHTML` or `eval()`**: The frontend renders AI responses as plain text via `{msg.content}` in JSX, preventing XSS from AI model output.
|
||||
|
||||
8. **API key scrubbing from localStorage**: The settings store explicitly strips `api_key` before persisting (line 60 of `settingsStore.ts`).
|
||||
|
||||
9. **No shell command injection**: All `std::process::Command` calls use hardcoded binary names with literal arguments. No user input is passed to shell commands.
|
||||
|
||||
10. **No secrets in git history**: `.gitignore` properly excludes `.env`, `.secrets`, `secrets.yml`, and related files. No private keys or certificates are tracked.
|
||||
|
||||
11. **Mutex guards not held across await points**: The codebase correctly drops `MutexGuard` before `.await` by scoping locks inside `{ }` blocks.
|
||||
|
||||
---
|
||||
|
||||
## Recommendations Summary (Priority Order)
|
||||
|
||||
| Priority | Action | Effort |
|
||||
|----------|--------|--------|
|
||||
| **P0** | Remove `GenAI API User Guide.md` and `HANDOFF-MSI-GENAI.md` from repo and git history | Small |
|
||||
| **P0** | Remove `commandcentral.com` URLs from CSP and hardcoded MSI headers from `openai.rs` | Small |
|
||||
| **P0** | Replace or parameterize private IP (`172.0.0.29`) and username in all `.gitea/` workflows | Medium |
|
||||
| **P1** | Replace hardcoded dev encryption keys with auto-generated per-install keys | Small |
|
||||
| **P1** | Use proper KDF (PBKDF2/HKDF) for AES key derivation in `auth.rs` | Small |
|
||||
| **P1** | Auto-generate encryption key for credential storage (mirror `connection.rs` pattern) | Small |
|
||||
| **P1** | Remove unscoped `fs:allow-read`/`fs:allow-write` from capabilities | Small |
|
||||
| **P2** | Move API key storage to backend-only (remove from IPC surface) | Medium |
|
||||
| **P2** | Add path validation to `export_document` output directory | Small |
|
||||
| **P2** | Add TTL to OAuth state entries | Small |
|
||||
| **P2** | Add audit chain verification function | Small |
|
||||
| **P3** | Remove `.idea/` from git tracking | Trivial |
|
||||
| **P3** | Replace placeholder OAuth client IDs with explicit errors | Trivial |
|
||||
| **P3** | Parameterize username in CI/Makefile | Small |
|
||||
|
||||
---
|
||||
|
||||
*Report generated by security audit of git-tracked source files at commit HEAD on feature/ai-tool-calling-integration-search branch.*
|
||||
@ -1,9 +1,9 @@
|
||||
{
|
||||
"name": "tftsr-cli",
|
||||
"name": "trcaa-cli",
|
||||
"version": "0.1.0",
|
||||
"description": "TFTSR IT Triage & RCA CLI",
|
||||
"description": "TRCAA IT Triage & RCA CLI",
|
||||
"type": "module",
|
||||
"bin": { "tftsr-cli": "./src/main.js" },
|
||||
"bin": { "trcaa-cli": "./src/main.js" },
|
||||
"scripts": {
|
||||
"start": "node src/main.js",
|
||||
"build": "tsc"
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* TFTSR CLI - Command-line interface for TFTSR IT Triage & RCA
|
||||
* TRCAA CLI - Command-line interface for TRCAA IT Triage & RCA
|
||||
*
|
||||
* Note: The CLI provides basic operations. For full functionality,
|
||||
* use the TFTSR desktop GUI application.
|
||||
* use the TRCAA desktop GUI application.
|
||||
*/
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
@ -11,9 +11,9 @@ const command = args[0];
|
||||
|
||||
function printHelp() {
|
||||
console.log(`
|
||||
TFTSR CLI v0.1.0 — IT Triage & RCA Tool
|
||||
TRCAA CLI v0.1.0 — IT Triage & RCA Tool
|
||||
|
||||
Usage: tftsr <command> [options]
|
||||
Usage: trcaa-cli <command> [options]
|
||||
|
||||
Commands:
|
||||
analyze <log-file> Analyze a log file for issues
|
||||
@ -31,17 +31,17 @@ Commands:
|
||||
help Show this help message
|
||||
|
||||
Examples:
|
||||
tftsr analyze /var/log/syslog --domain linux
|
||||
tftsr export abc-123 pdf
|
||||
tftsr config set active_provider ollama
|
||||
trcaa-cli analyze /var/log/syslog --domain linux
|
||||
trcaa-cli export abc-123 pdf
|
||||
trcaa-cli config set active_provider ollama
|
||||
|
||||
Note: For full AI-powered triage, launch the TFTSR desktop application.
|
||||
Note: For full AI-powered triage, launch the TRCAA desktop application.
|
||||
`);
|
||||
}
|
||||
|
||||
function printVersion() {
|
||||
console.log("TFTSR CLI v0.1.0");
|
||||
console.log("Part of the TFTSR IT Triage & RCA Desktop Application");
|
||||
console.log("TRCAA CLI v0.1.0");
|
||||
console.log("Part of the TRCAA IT Triage & RCA Desktop Application");
|
||||
}
|
||||
|
||||
switch (command) {
|
||||
@ -49,14 +49,14 @@ switch (command) {
|
||||
const logFile = args[1];
|
||||
if (!logFile) {
|
||||
console.error("Error: log file path required");
|
||||
console.error("Usage: tftsr analyze <log-file>");
|
||||
console.error("Usage: trcaa-cli analyze <log-file>");
|
||||
process.exit(1);
|
||||
}
|
||||
const domainIdx = args.findIndex((a) => a === "--domain" || a === "-d");
|
||||
const domain = domainIdx >= 0 ? args[domainIdx + 1] : "linux";
|
||||
console.log(`Analyzing: ${logFile}`);
|
||||
console.log(`Domain: ${domain}`);
|
||||
console.log("\nFor AI-powered analysis, launch the TFTSR desktop application.");
|
||||
console.log("\nFor AI-powered analysis, launch the TRCAA desktop application.");
|
||||
console.log("The GUI provides: PII detection, 5-whys triage, RCA generation.");
|
||||
break;
|
||||
}
|
||||
@ -65,7 +65,7 @@ switch (command) {
|
||||
const issueId = args[1];
|
||||
const format = args[2];
|
||||
if (!issueId || !format) {
|
||||
console.error("Usage: tftsr export <issue-id> <format>");
|
||||
console.error("Usage: trcaa-cli export <issue-id> <format>");
|
||||
process.exit(1);
|
||||
}
|
||||
if (!["md", "pdf", "docx"].includes(format)) {
|
||||
@ -73,7 +73,7 @@ switch (command) {
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`Export issue ${issueId} as ${format.toUpperCase()}`);
|
||||
console.log("Launch the TFTSR app to access the export functionality.");
|
||||
console.log("Launch the TRCAA app to access the export functionality.");
|
||||
break;
|
||||
}
|
||||
|
||||
@ -82,13 +82,13 @@ switch (command) {
|
||||
switch (subcommand) {
|
||||
case "set":
|
||||
console.log(`Configuration: ${args[2]} = ${args[3]}`);
|
||||
console.log("Note: Configuration is managed by the TFTSR desktop application.");
|
||||
console.log("Note: Configuration is managed by the TRCAA desktop application.");
|
||||
break;
|
||||
case "get":
|
||||
console.log(`Getting config key: ${args[2]}`);
|
||||
break;
|
||||
case "list":
|
||||
console.log("Configuration is stored in the TFTSR app data directory.");
|
||||
console.log("Configuration is stored in the TRCAA app data directory.");
|
||||
console.log("Launch the app and go to Settings to view/edit configuration.");
|
||||
break;
|
||||
default:
|
||||
|
||||
1834
docs/2026-hackathon_AgenticFeature.md
Normal file
84
docs/MCP_SERVER_SUPPORT.md
Normal file
@ -0,0 +1,84 @@
|
||||
# MCP Server Support — Ticket Summary
|
||||
|
||||
## Description
|
||||
|
||||
Adds MCP (Model Context Protocol) server management to the application, allowing the AI assistant
|
||||
to discover and call tools from external MCP servers during triage conversations.
|
||||
|
||||
The implementation covers:
|
||||
- Settings page at `/settings/mcp` for managing server connections
|
||||
- Support for `stdio` (local processes) and `http` (Streamable HTTP) transports
|
||||
- Auth types: `none`, `api_key`, `bearer`, `oauth2`
|
||||
- Auto-discovery of enabled servers at application startup
|
||||
- Transparent injection of discovered tools into every AI chat session
|
||||
- Security-first design: encrypted credential storage, mandatory audit logging, PII scanning
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] Users can add, edit, enable/disable, and delete MCP server configurations
|
||||
- [x] "Discover Now" connects to the server, lists tools and resources, and persists results
|
||||
- [x] Enabled servers auto-connect on app launch via `.setup()` hook
|
||||
- [x] MCP tools appear in the AI chat tool list and are callable by the AI
|
||||
- [x] `auth_value` is always AES-256-GCM encrypted at rest; never returned to frontend
|
||||
- [x] `write_audit_event()` is called before every MCP tool execution
|
||||
- [x] PII scan on tool call arguments (non-blocking warning on detection)
|
||||
- [x] stdio transport rejects relative paths; never uses `sh -c`
|
||||
- [x] All existing tests continue to pass (185 Rust, 94 Vitest)
|
||||
- [x] Zero clippy warnings; zero TypeScript errors
|
||||
|
||||
---
|
||||
|
||||
## Work Implemented
|
||||
|
||||
### Backend (Rust)
|
||||
|
||||
| Phase | Files | Description |
|
||||
|-------|-------|-------------|
|
||||
| 0 | `Cargo.toml` | Added `rmcp = "1.7.0"` with client + transport features; version → 0.3.0 |
|
||||
| 1 | `db/migrations.rs` | Migration 018: `mcp_servers`, `mcp_tools`, `mcp_resources` tables with CHECK constraints |
|
||||
| 2a | `mcp/models.rs`, `mcp/store.rs` | Data types; full CRUD with encrypted auth storage |
|
||||
| 2b | `mcp/transport/stdio.rs`, `mcp/transport/http.rs` | Transport builders for subprocess and Streamable HTTP |
|
||||
| 2c | `mcp/client.rs` | `McpConnection` type alias; connect/list/call wrappers |
|
||||
| 2d | `mcp/adapter.rs` | `sanitize_name`, `build_tool_key`, `mcp_tools_to_ai_tools`, `get_enabled_mcp_tools` |
|
||||
| 2e | `mcp/discovery.rs` | `discover_server`, `init_all_servers` |
|
||||
| 2f | `mcp/commands.rs`, `state.rs`, `lib.rs` | 8 Tauri commands; `mcp_connections` field on `AppState`; `.setup()` hook |
|
||||
| 5 | `ai/tools.rs`, `commands/ai.rs` | `get_enabled_mcp_tools` async helper; `execute_mcp_tool_call` with PII scan + audit |
|
||||
|
||||
### Frontend (TypeScript / React)
|
||||
|
||||
| Phase | Files | Description |
|
||||
|-------|-------|-------------|
|
||||
| 3 | `src/lib/tauriCommands.ts` | `McpServer`, `McpTool`, `McpResource`, `McpServerStatus`, request types; 8 command wrappers |
|
||||
| 4 | `src/pages/Settings/MCPServers.tsx` | Full settings page: server list, status badges, Discover Now, Add/Edit modal |
|
||||
| 4 | `src/App.tsx` | Added `Plug` icon, `/settings/mcp` route and nav entry |
|
||||
|
||||
### Wiki
|
||||
|
||||
- `docs/wiki/MCP-Servers.md` — new
|
||||
- `docs/wiki/Database.md` — migration 018 documented
|
||||
- `docs/wiki/IPC-Commands.md` — 8 new commands
|
||||
- `docs/wiki/Security-Model.md` — MCP security section
|
||||
|
||||
---
|
||||
|
||||
## Testing Needed
|
||||
|
||||
### Automated (all passing)
|
||||
- Rust: 185 tests (64 existing + 5 migration 018 + 5 store + 3 adapter + 5 migration idempotency + misc)
|
||||
- Vitest: 94 tests (all existing + 3 new MCP frontend tests)
|
||||
- `cargo clippy -- -D warnings`: zero warnings
|
||||
- `npx tsc --noEmit`: zero errors
|
||||
|
||||
### Manual verification checklist
|
||||
- [ ] Add an HTTP MCP server → click Discover Now → tools appear in list
|
||||
- [ ] Add a stdio MCP server → Discover Now → process spawns, tools appear
|
||||
- [ ] Disable a server → its tools absent from next triage chat session
|
||||
- [ ] Start a triage chat → MCP tools visible in AI tool suggestions
|
||||
- [ ] AI calls an MCP tool → audit log entry written in Security page
|
||||
- [ ] Delete a server → live connection removed, tools gone from next session
|
||||
- [ ] Enter an invalid command path (relative) for stdio → error shown in UI
|
||||
|
||||
### Branch
|
||||
`feature/mcp-server-support`
|
||||
9
docs/RELEASE_NOTES.md
Normal file
@ -0,0 +1,9 @@
|
||||
# Release v1.0.1
|
||||
|
||||
This release ensures the domain prompt fix is cleanly packaged.
|
||||
|
||||
## Changes since v1.0.0
|
||||
- Domain prompts now instruct AI to use execute_shell_command tool
|
||||
- UI contrast improvements for kubeconfig file upload
|
||||
- ARM64 Linux build fix
|
||||
|
||||
118
docs/TICKET-attachment-db-storage-recall.md
Normal file
@ -0,0 +1,118 @@
|
||||
# Ticket: Attachment DB Storage & Cross-Incident Recall
|
||||
|
||||
**Branch:** `feature/attachment-db-storage-recall`
|
||||
**Base:** `master`
|
||||
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Log file and image attachment records previously stored only metadata and filesystem paths, making content volatile — if the source file moved or was deleted, the attachment record became orphaned. There was also no mechanism to search or recall attachments across incidents.
|
||||
|
||||
This feature:
|
||||
1. Stores **gzip-compressed** log text and **raw image bytes** directly in the database, making attachments fully self-contained and portable.
|
||||
2. Surfaces a new **Attachments tab** on the History page for cross-incident search and recall.
|
||||
3. Exposes content-retrieval commands so the AI chat context can reference log content from DB on demand, with no disk dependency.
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] Uploading a log file stores gzip-compressed text in `log_files.content_compressed` (BLOB)
|
||||
- [x] Uploading an image stores raw bytes in `image_attachments.image_data` (BLOB)
|
||||
- [x] `get_log_file_content` returns decompressed text from DB; falls back to disk for pre-migration records
|
||||
- [x] `get_image_attachment_data` returns base64 data URL from DB; falls back to disk for pre-migration records
|
||||
- [x] `list_all_log_files` returns cross-incident log summaries with joined issue title, supports search and issueId filter
|
||||
- [x] `list_all_image_attachments` returns cross-incident image summaries with joined issue title, supports search and issueId filter
|
||||
- [x] History page shows two tabs: **Issues** (existing, unchanged) and **Attachments** (new)
|
||||
- [x] Attachments tab: Log Files section with filename, incident link, date, size, type badge, View button
|
||||
- [x] Attachments tab: Images section with 48px thumbnail, filename, incident link, date, View button
|
||||
- [x] "View" on log file → modal showing decompressed plain text
|
||||
- [x] "View" on image → modal showing full-size image
|
||||
- [x] Existing records with NULL content fall back to disk read — no breakage for pre-migration data
|
||||
- [x] All new DB changes tracked via migrations 020–022 with idempotency guarantees
|
||||
- [x] Wiki documentation updated: IPC-Commands.md and Database.md
|
||||
|
||||
---
|
||||
|
||||
## Work Implemented
|
||||
|
||||
### Database (`src-tauri/src/db/`)
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `migrations.rs` | Migrations 020 (`content_compressed BLOB`), 021 (`image_data BLOB`), 022 (views `v_log_files_with_issue` + `v_image_attachments_with_issue`). Extended duplicate-column graceful handling for new ALTER TABLE migrations. |
|
||||
| `models.rs` | Added `LogFileSummary` and `ImageAttachmentSummary` structs for lightweight cross-incident list views (no BLOB fields — content stays out of IPC). |
|
||||
|
||||
### Rust Backend (`src-tauri/src/commands/`)
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `analysis.rs` | Private `compress_text` / `decompress_text` helpers (flate2/miniz_oxide — pure Rust, no system binary). Updated `upload_log_file` and `upload_log_file_by_content` INSERTs to store `content_compressed`. New commands: `get_log_file_content`, `list_all_log_files`. |
|
||||
| `image.rs` | Updated `upload_image_attachment`, `upload_image_attachment_by_content`, `upload_paste_image` INSERTs to store `image_data`. New commands: `get_image_attachment_data`, `list_all_image_attachments`. |
|
||||
| `lib.rs` | Registered all 4 new commands. |
|
||||
|
||||
### Dependencies (`src-tauri/Cargo.toml`)
|
||||
|
||||
- Added `flate2 = { version = "1", features = ["rust_backend"] }` — pure-Rust gzip, portable cross-platform.
|
||||
|
||||
### Frontend (`src/`)
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `lib/tauriCommands.ts` | Added `LogFileSummary`, `ImageAttachmentSummary` interfaces and 4 typed command wrappers. |
|
||||
| `stores/attachmentStore.ts` | New Zustand store: `loadAttachments`, `searchAttachments`, `setSearchQuery`. |
|
||||
| `pages/History/index.tsx` | Added tab bar; extracted `IssuesTab` (existing content, unchanged); added `AttachmentsTab` with log/image tables, search, View modals, and lazy `ImageThumbnail` component. |
|
||||
|
||||
### Documentation (`docs/wiki/`)
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `IPC-Commands.md` | Documented `get_log_file_content`, `list_all_log_files`, `get_image_attachment_data`, `list_all_image_attachments` with TypeScript signatures and interface shapes. Updated upload command notes. |
|
||||
| `Database.md` | Updated migration count (18 → 22). Documented migrations 020, 021, 022 with SQL, rationale, and usage notes. |
|
||||
|
||||
---
|
||||
|
||||
## Testing Needed
|
||||
|
||||
### Automated (already passing)
|
||||
|
||||
| Suite | Count | Status |
|
||||
|---|---|---|
|
||||
| Rust unit tests (`cargo test`) | 226 | ✅ All pass |
|
||||
| Frontend unit tests (`npm run test:run`) | 103 | ✅ All pass |
|
||||
| TypeScript type check (`tsc --noEmit`) | — | ✅ Clean |
|
||||
| Rust clippy (`clippy -- -D warnings`) | — | ✅ Zero warnings |
|
||||
| Rust format (`fmt --check`) | — | ✅ Clean |
|
||||
|
||||
New tests added:
|
||||
- `test_compress_decompress_roundtrip`, `test_compress_large_text_is_smaller`, `test_decompress_invalid_bytes_returns_error` (Rust, `analysis.rs`)
|
||||
- `test_get_image_attachment_data_base64_format` (Rust, `image.rs`)
|
||||
- `test_020_log_content_compressed_column`, `test_021_image_data_column`, `test_022_attachment_views_exist`, `test_022_views_join_issue_title`, `test_020_021_idempotent` (Rust, `migrations.rs`)
|
||||
- 9 attachment store tests (`tests/unit/attachmentStore.test.ts`)
|
||||
|
||||
### Manual Smoke Testing Required
|
||||
|
||||
1. **Log upload → DB content storage**
|
||||
- Create issue → upload `.log` file → inspect SQLite: `SELECT id, LENGTH(content_compressed) FROM log_files` — verify non-NULL non-zero value
|
||||
|
||||
2. **Content retrieval from DB**
|
||||
- History → Attachments tab → Log Files → click "View" → confirm readable decompressed text appears in modal
|
||||
|
||||
3. **Fallback for pre-migration records**
|
||||
- Manually `UPDATE log_files SET content_compressed = NULL WHERE id = '<id>'` → View should still load from disk path
|
||||
|
||||
4. **Image upload → DB byte storage**
|
||||
- Upload image → `SELECT id, LENGTH(image_data) FROM image_attachments` — verify non-NULL
|
||||
|
||||
5. **Image display**
|
||||
- History → Attachments tab → Images → thumbnails should render, View → full-size image modal
|
||||
|
||||
6. **Cross-incident search**
|
||||
- Create 2+ issues with different log files → Attachments tab → search by partial filename → correct files appear
|
||||
|
||||
7. **Issue link navigation**
|
||||
- Click incident title in Attachments tab → navigates to correct triage page
|
||||
|
||||
8. **Issue tab unchanged**
|
||||
- Verify existing Issues tab retains all functionality (search, filter, sort, open, export buttons)
|
||||
102
docs/TICKET-pii-bypass-chat-attachments.md
Normal file
@ -0,0 +1,102 @@
|
||||
# TICKET: PII Detection Bypass in AI Chat
|
||||
|
||||
**Branch**: `fix/pii-detection-bypass`
|
||||
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
Two PII detection bypasses were identified and fixed in the AI triage chat interface.
|
||||
|
||||
### Bypass 1 — File Attachments (Critical)
|
||||
|
||||
When a user attached a file to a chat message, its content was read via `readTextFile()`, sliced to 8 KB, and embedded directly into the AI message string — bypassing the PII pipeline entirely. The message was forwarded to the configured AI provider in plaintext with no redaction marker in the audit log.
|
||||
|
||||
**Root cause**: `handleAttach` stored raw file content in React state. `handleSend` concatenated it into `aiMessage` with no PII check. The backend `chat_message` command applied no validation.
|
||||
|
||||
### Bypass 2 — Typed Chat Messages (High)
|
||||
|
||||
Plain typed chat messages were sent to the AI provider without any PII scan. A user typing `How secure is my password: abc123!!` would have the password forwarded to the AI and persisted in the audit log in plaintext.
|
||||
|
||||
### Related Fix — Wrong Return Type on `detect_pii`
|
||||
|
||||
`detect_pii` was serialising `pii::PiiDetectionResult` (`spans`, `original_text`) while the TypeScript interface expected `db::models::PiiDetectionResult` (`detections`, `total_pii_found`). All frontend code reading `result.detections` received `undefined`, meaning the LogUpload PII review workflow was silently broken.
|
||||
|
||||
---
|
||||
|
||||
## Design Decision: Auto-Redact, Not Block
|
||||
|
||||
After initial implementation explored a blocking/warn-then-proceed approach, the product decision was made to **auto-redact PII in-place and send**:
|
||||
|
||||
- File attachments: PII is detected on full file content and replaced with type tokens (`[Password]`, `[Email]`, etc.) before the content is embedded in the AI message. The redacted form is stored in the DB and audit log.
|
||||
- Typed messages: Same auto-redact applied to the user's typed text before the message is sent to the AI provider.
|
||||
- The user's chat bubble is updated after the response to show the redacted form — users can see exactly what reached the AI.
|
||||
- The audit log records `was_pii_redacted: bool` and `pii_types_redacted: [...]` alongside the redacted message.
|
||||
- No user blocking or acknowledgment flow. PII is handled transparently.
|
||||
|
||||
---
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] Attaching a text file containing PII sends successfully; content is auto-redacted before the AI sees it
|
||||
- [x] Attaching a clean text file proceeds normally with no modification
|
||||
- [x] PII detection runs on the full file content before truncating to the 8 KB embed limit (no PII straddling the boundary)
|
||||
- [x] Typed messages containing PII are auto-redacted before being sent to the AI provider
|
||||
- [x] The chat bubble is updated post-send to show the redacted form of the user's message
|
||||
- [x] The audit log records `was_pii_redacted`, `pii_types_redacted`, and the full redacted `user_message`
|
||||
- [x] `detectPiiCmd` returns `detections: PiiSpan[]` and `total_pii_found: number` matching the TypeScript contract
|
||||
- [x] `chatMessageCmd` passes `logFileIds` as `undefined` (not `null`) when no files are attached
|
||||
- [x] `scan_text_for_pii` rejects inputs over 32 KB to prevent DoS
|
||||
- [x] `response.user_message ?? message` used as bubble fallback — no `"undefined..."` concatenation
|
||||
- [x] All Rust and frontend tests pass; zero clippy warnings; `cargo fmt --check` clean; tsc clean
|
||||
|
||||
---
|
||||
|
||||
## Work Implemented
|
||||
|
||||
### `src-tauri/src/ai/mod.rs`
|
||||
- Added `user_message: Option<String>` to `ChatResponse` — set by `chat_message`, absent from direct provider calls
|
||||
|
||||
### `src-tauri/src/ai/anthropic.rs`, `gemini.rs`, `mistral.rs`, `ollama.rs`, `openai.rs`
|
||||
- Added `user_message: None` to all `ChatResponse { ... }` constructors
|
||||
|
||||
### `src-tauri/src/commands/ai.rs`
|
||||
- `chat_message` now accepts `log_file_ids: Option<Vec<String>>`
|
||||
- Step 1: auto-redacts the typed message text with `PiiDetector` + `apply_redactions`
|
||||
- Step 2: loads each attachment from DB, detects PII on **full file content**, applies redactions, then truncates to 8 KB at a valid UTF-8 char boundary
|
||||
- Tracks `was_pii_redacted` and `redacted_pii_types` across both steps
|
||||
- Audit log includes `was_pii_redacted: bool` and `pii_types_redacted: [...]`
|
||||
- Returns `user_message: Some(stored_user_message)` in `ChatResponse`
|
||||
|
||||
### `src-tauri/src/commands/analysis.rs`
|
||||
- Fixed `detect_pii` return type from `pii::PiiDetectionResult` to `db::models::PiiDetectionResult`
|
||||
- Added `scan_text_for_pii(text: String)` with 32 KB input cap
|
||||
|
||||
### `src-tauri/src/lib.rs`
|
||||
- Registered `scan_text_for_pii`
|
||||
|
||||
### `src/lib/tauriCommands.ts`
|
||||
- `ChatResponse` interface: added `user_message?: string`
|
||||
- `chatMessageCmd` signature: added `logFileIds: string[]`; passes `undefined` when empty
|
||||
- Added `scanTextForPiiCmd` wrapper
|
||||
|
||||
### `src/stores/sessionStore.ts`
|
||||
- Added `updateMessageContent(id, content)` action
|
||||
|
||||
### `src/pages/Triage/index.tsx`
|
||||
- `PendingFile` type: `{ name: string; logFileId: string }` — no raw content stored
|
||||
- `handleAttach`: only uploads the file and stores `logFileId`; no `readTextFile`
|
||||
- `handleSend`: passes `logFileIds` to backend; after response updates the bubble with `(response.user_message ?? message) + suffix`
|
||||
|
||||
---
|
||||
|
||||
## Testing Needed
|
||||
|
||||
1. Attach a file containing `password: secret123` → message sends; chat bubble shows `[Password]` in the embedded content; no plaintext credential in bubble or DB
|
||||
2. Attach a clean text file → content appears unmodified in the chat context
|
||||
3. Attach a file where PII appears near the 8000-byte mark → content is fully redacted before truncation
|
||||
4. Type `My password is abc123!!` → message sends; bubble shows `My [Password] is [Password]`
|
||||
5. On LogUpload page, upload a file with a known IP/email → PII spans appear in the review UI
|
||||
6. Check audit log after a PII-containing message: `was_pii_redacted: true`, `pii_types_redacted` populated
|
||||
7. Check audit log after a clean message: `was_pii_redacted: false`, `pii_types_redacted: []`
|
||||
8. `cargo test` → 228/228 pass; `npm run test:run` → 103/103 pass; `cargo fmt --check` clean; `npx tsc --noEmit` clean
|
||||
@ -90,6 +90,335 @@ C4Container
|
||||
|
||||
## Component Architecture
|
||||
|
||||
### Shell Execution System (v1.0.0+)
|
||||
|
||||
**Status**: Production-ready agentic shell command execution with three-tier safety classification.
|
||||
|
||||
**Architecture**: Three-tier safety system with automatic classification, approval gates, and audit logging.
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Shell Execution Architecture"
|
||||
AI[AI Agent] -->|tool_call| ToolRegistry[Tool Registry]
|
||||
ToolRegistry -->|execute_shell_command| Classifier[Command Classifier]
|
||||
|
||||
Classifier -->|analyze| Parser[Command Parser]
|
||||
Parser -->|components| RiskAnalyzer[Risk Analyzer]
|
||||
|
||||
RiskAnalyzer -->|Tier 1| AutoExec[Auto Execute]
|
||||
RiskAnalyzer -->|Tier 2| ApprovalGate[Approval Gate]
|
||||
RiskAnalyzer -->|Tier 3| Deny[Always Deny]
|
||||
|
||||
ApprovalGate -->|user decision| ApprovalModal[Approval Modal UI]
|
||||
ApprovalModal -->|allow| Executor[Command Executor]
|
||||
ApprovalModal -->|deny| AuditLog[Audit Log]
|
||||
|
||||
AutoExec --> Executor
|
||||
Deny --> AuditLog
|
||||
|
||||
Executor -->|kubectl| KubectlBinary[kubectl Binary v1.30.0]
|
||||
Executor -->|shell| SystemShell[System Shell]
|
||||
|
||||
Executor --> ExecutionRecord[Execution Record]
|
||||
ExecutionRecord --> AuditLog
|
||||
ExecutionRecord --> Database[(Database)]
|
||||
|
||||
Database --> ExecutionHistory[Execution History UI]
|
||||
end
|
||||
|
||||
style Classifier fill:#e1f5ff
|
||||
style ApprovalGate fill:#fff4e6
|
||||
style Deny fill:#ffe6e6
|
||||
style AutoExec fill:#e6f7e6
|
||||
style KubectlBinary fill:#f0e6ff
|
||||
```
|
||||
|
||||
**Three-Tier Safety Classification**:
|
||||
|
||||
- **Tier 1 (Auto-execute)**: Read-only operations with no side effects
|
||||
- Examples: `kubectl get`, `kubectl describe`, `kubectl logs`, `cat`, `grep`, `ls`, `pvecm status`
|
||||
- Executes immediately without user interaction
|
||||
|
||||
- **Tier 2 (User approval required)**: Potentially mutating operations
|
||||
- Examples: `kubectl apply`, `kubectl delete`, `kubectl scale`, `chmod`, `systemctl restart`, `ssh`
|
||||
- Shows real-time approval modal with command details
|
||||
- Supports "Allow Once", "Allow for Session", and "Deny"
|
||||
|
||||
- **Tier 3 (Always deny)**: Destructive operations
|
||||
- Examples: `rm -rf`, `shutdown`, `mkfs`, `dd`, `:(){:|:&};:`
|
||||
- Automatically rejected with explanation to user
|
||||
|
||||
**Key Modules**:
|
||||
|
||||
| Module | Responsibility | Key Features |
|
||||
|--------|---------------|--------------|
|
||||
| `shell/classifier.rs` | Command safety classification | 19 unit tests, pipe/chain analysis, command substitution detection |
|
||||
| `shell/executor.rs` | Execution flow with approval gates | Timeout handling, kubeconfig injection, exit code capture |
|
||||
| `shell/kubectl.rs` | kubectl binary management | Cross-platform binary bundling, version v1.30.0 |
|
||||
| `shell/kubeconfig.rs` | Kubeconfig parsing and encryption | AES-256-GCM encryption, context extraction, cluster URL parsing |
|
||||
| `commands/shell.rs` | 7 Tauri IPC commands | kubeconfig CRUD, execution, history retrieval |
|
||||
| `ai/tools.rs` | Tool registration | `execute_shell_command` tool definition with parameters |
|
||||
|
||||
**Database Schema** (Migrations 024-027):
|
||||
|
||||
```sql
|
||||
-- Pre-defined command templates with tier definitions
|
||||
CREATE TABLE shell_commands (
|
||||
id TEXT PRIMARY KEY,
|
||||
command_template TEXT NOT NULL,
|
||||
tier INTEGER NOT NULL CHECK(tier IN (1, 2, 3)),
|
||||
description TEXT,
|
||||
category TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- Encrypted kubeconfig storage
|
||||
CREATE TABLE kubeconfig_files (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
encrypted_content TEXT NOT NULL,
|
||||
context TEXT NOT NULL,
|
||||
cluster_url TEXT,
|
||||
is_active INTEGER NOT NULL DEFAULT 0,
|
||||
uploaded_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- Full audit trail for all executions
|
||||
CREATE TABLE command_executions (
|
||||
id TEXT PRIMARY KEY,
|
||||
issue_id TEXT,
|
||||
command TEXT NOT NULL,
|
||||
tier INTEGER NOT NULL,
|
||||
approval_status TEXT NOT NULL,
|
||||
kubeconfig_id TEXT,
|
||||
exit_code INTEGER,
|
||||
stdout TEXT,
|
||||
stderr TEXT,
|
||||
execution_time_ms INTEGER,
|
||||
executed_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
-- Session-based approval preferences
|
||||
CREATE TABLE approval_decisions (
|
||||
id TEXT PRIMARY KEY,
|
||||
command_pattern TEXT NOT NULL,
|
||||
decision TEXT NOT NULL CHECK(decision IN ('allow_once', 'allow_session', 'deny')),
|
||||
session_id TEXT,
|
||||
decided_at TEXT NOT NULL,
|
||||
expires_at TEXT
|
||||
);
|
||||
```
|
||||
|
||||
**Security Features**:
|
||||
- AES-256-GCM encryption for kubeconfig files
|
||||
- Command tier escalation for pipes and command substitution
|
||||
- Full audit logging of all commands (approved, denied, executed)
|
||||
- Session-based approval memory with expiration
|
||||
- kubectl binary bundled and verified (no system dependency)
|
||||
|
||||
**Frontend Components**:
|
||||
- `ShellApprovalModal.tsx`: Real-time approval UI with command preview
|
||||
- `Settings/ShellExecution.tsx`: Settings and execution history viewer
|
||||
- `Settings/KubeconfigManager.tsx`: Multi-cluster kubeconfig management
|
||||
|
||||
**Documentation**: `docs/wiki/Shell-Execution.md`
|
||||
|
||||
---
|
||||
|
||||
### MCP Server Integration (v1.0.0+)
|
||||
|
||||
**Status**: Production-ready Model Context Protocol integration for external tool protocols.
|
||||
|
||||
**Architecture**: Client-server protocol adapter for stdio and HTTP transports.
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "MCP Integration Architecture"
|
||||
AI[AI Agent] -->|needs tools| Adapter[MCP Adapter]
|
||||
|
||||
Adapter -->|fetch tools| Store[MCP Store]
|
||||
Store -->|load servers| Database[(Database)]
|
||||
|
||||
Adapter -->|for each enabled server| Discovery[Discovery Service]
|
||||
Discovery -->|connect| Client[MCP Client]
|
||||
|
||||
Client -->|stdio| StdioTransport[Stdio Transport]
|
||||
Client -->|http| HttpTransport[HTTP Transport]
|
||||
|
||||
StdioTransport -->|spawn process| ExternalServer1[MCP Server Process]
|
||||
HttpTransport -->|HTTP POST| ExternalServer2[MCP HTTP Server]
|
||||
|
||||
Client -->|list_tools| ServerCapabilities[Server Capabilities]
|
||||
ServerCapabilities -->|return tools| ToolRegistry[Tool Registry]
|
||||
|
||||
AI -->|call tool| ToolExecutor[Tool Executor]
|
||||
ToolExecutor -->|invoke| Client
|
||||
Client -->|call_tool| ExternalServer1
|
||||
ExternalServer1 -->|result| Client
|
||||
Client -->|30s timeout| ToolExecutor
|
||||
|
||||
Discovery -->|update status| Database
|
||||
end
|
||||
|
||||
style Discovery fill:#e1f5ff
|
||||
style Client fill:#fff4e6
|
||||
style Database fill:#e6f7e6
|
||||
```
|
||||
|
||||
**Key Modules**:
|
||||
|
||||
| Module | Responsibility | Key Features |
|
||||
|--------|---------------|--------------|
|
||||
| `mcp/client.rs` | Connect to MCP servers | Stdio/HTTP transports, 30s tool call timeout |
|
||||
| `mcp/adapter.rs` | Tool registry integration | Fetch tools from all enabled servers, merge with static tools |
|
||||
| `mcp/discovery.rs` | Server health checks | Connection status updates, error tracking |
|
||||
| `mcp/store.rs` | Database CRUD | Server config, tool/resource persistence |
|
||||
| `mcp/models.rs` | Data models | McpServer, McpTool, McpResource types |
|
||||
| `mcp/transport/stdio.rs` | Stdio transport | Process spawning, environment variables |
|
||||
| `mcp/transport/http.rs` | HTTP transport | Custom headers, auth support |
|
||||
| `mcp/commands.rs` | 7 Tauri IPC commands | Server CRUD, discovery, tool/resource listing |
|
||||
|
||||
**Database Schema** (Migration 018):
|
||||
|
||||
```sql
|
||||
CREATE TABLE mcp_servers (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
transport_type TEXT NOT NULL CHECK(transport_type IN ('stdio', 'http')),
|
||||
transport_config TEXT NOT NULL DEFAULT '{}',
|
||||
auth_type TEXT NOT NULL CHECK(auth_type IN ('none', 'api_key', 'bearer', 'oauth2')),
|
||||
auth_value TEXT,
|
||||
enabled INTEGER NOT NULL DEFAULT 1,
|
||||
last_discovered_at TEXT,
|
||||
discovery_status TEXT NOT NULL DEFAULT 'pending'
|
||||
CHECK(discovery_status IN ('pending','connected','unreachable','error')),
|
||||
discovery_error TEXT,
|
||||
env_config TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE mcp_tools (
|
||||
id TEXT PRIMARY KEY,
|
||||
server_id TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
tool_key TEXT NOT NULL,
|
||||
description TEXT,
|
||||
parameters TEXT NOT NULL DEFAULT '{}',
|
||||
FOREIGN KEY(server_id) REFERENCES mcp_servers(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE mcp_resources (
|
||||
id TEXT PRIMARY KEY,
|
||||
server_id TEXT NOT NULL,
|
||||
uri TEXT NOT NULL,
|
||||
name TEXT,
|
||||
description TEXT,
|
||||
FOREIGN KEY(server_id) REFERENCES mcp_servers(id) ON DELETE CASCADE
|
||||
);
|
||||
```
|
||||
|
||||
**Tool Calling Flow**:
|
||||
1. AI agent requests available tools
|
||||
2. Adapter fetches static tools (`ai/tools.rs::get_available_tools()`)
|
||||
3. Adapter fetches MCP tools from all enabled servers
|
||||
4. Tools merged and returned to AI agent
|
||||
5. AI agent calls tool by name (e.g., `server_name.tool_name`)
|
||||
6. Adapter routes to correct MCP client
|
||||
7. Client invokes tool with 30-second timeout
|
||||
8. Result returned to AI agent
|
||||
|
||||
**Security**:
|
||||
- Auth credentials stored with AES-256-GCM encryption
|
||||
- Environment variables isolated per server process
|
||||
- 30-second hard timeout prevents indefinite hangs
|
||||
- Server connection status tracked and displayed
|
||||
|
||||
**Frontend Components**:
|
||||
- `Settings/MCPServers.tsx`: Server configuration and discovery UI
|
||||
- `Settings/MCPTools.tsx`: Tool browser and tester
|
||||
|
||||
---
|
||||
|
||||
### AI Tool Calling & Auto-Detection (v1.0.8+)
|
||||
|
||||
**Status**: Production-ready automatic tool calling support detection.
|
||||
|
||||
**Architecture**: Test-based detection with graceful degradation.
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Tool Calling Detection"
|
||||
User[User] -->|clicks detect| UI[Auto-Detect Button]
|
||||
UI -->|invoke| Command[detect_tool_calling_support]
|
||||
|
||||
Command -->|create test tool| TestTool[test_tool definition]
|
||||
Command -->|override config| DetectionConfig[Detection Config]
|
||||
|
||||
DetectionConfig -->|max_tokens: 100| Optimization1[Cost Optimization]
|
||||
DetectionConfig -->|temperature: 0.0| Optimization2[Deterministic]
|
||||
|
||||
Command -->|send test call| Provider[AI Provider]
|
||||
Provider -->|response| Parser[Response Parser]
|
||||
|
||||
Parser -->|has tool_calls array?| Decision{Supports Tools?}
|
||||
Decision -->|yes, contains test_tool| Success[Return true]
|
||||
Decision -->|no tool_calls| NotSupported[Return false]
|
||||
Decision -->|503 / tool error| Blocked[Return false]
|
||||
Decision -->|connection error| Error[Throw error]
|
||||
|
||||
Success -->|update UI| Checkbox[Enable Checkbox]
|
||||
NotSupported -->|update UI| DisableCheckbox[Disable Checkbox]
|
||||
Blocked -->|update UI| DisableCheckbox
|
||||
Error -->|display| ErrorMessage[Error Message]
|
||||
end
|
||||
|
||||
style Success fill:#e6f7e6
|
||||
style NotSupported fill:#fff4e6
|
||||
style Blocked fill:#ffe6e6
|
||||
style Error fill:#ffe6e6
|
||||
```
|
||||
|
||||
**Test Tool Definition**:
|
||||
|
||||
```rust
|
||||
Tool {
|
||||
name: "test_tool".to_string(),
|
||||
description: "A test tool that returns 'success'. Call this tool with no arguments.".to_string(),
|
||||
parameters: ToolParameters {
|
||||
param_type: "object".to_string(),
|
||||
properties: HashMap::new(),
|
||||
required: vec![],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
**Detection Criteria**:
|
||||
|
||||
| Scenario | Result | Action |
|
||||
|----------|--------|--------|
|
||||
| Provider returns `tool_calls` array with `test_tool` | ✅ Supported | Enable checkbox, show success message |
|
||||
| Provider responds without `tool_calls` | ⚠️ Not supported | Disable checkbox, show warning |
|
||||
| Gateway returns 503 / "tool" error (e.g., TFTSR GenAI) | ⚠️ Blocked | Disable checkbox, show warning |
|
||||
| Connection/auth/timeout error | ❌ Error | Show error message, don't change checkbox |
|
||||
|
||||
**Optimizations**:
|
||||
- `max_tokens: 100` (reduces cost for detection test)
|
||||
- `temperature: 0.0` (deterministic responses)
|
||||
- Error pattern matching for gateway-level blocks
|
||||
|
||||
**Key Files**:
|
||||
- `commands/ai.rs::detect_tool_calling_support()`: Backend detection logic (5 unit tests)
|
||||
- `pages/Settings/AIProviders.tsx::handleAutoDetectToolCalling()`: Frontend UI (7 unit tests)
|
||||
- `lib/tauriCommands.ts::detectToolCallingSupportCmd()`: TypeScript wrapper
|
||||
|
||||
**Database**: Uses `ai_providers.supports_tool_calling` column (Migration 028)
|
||||
|
||||
**Documentation**: `docs/wiki/AI-Providers.md` section "Tool Calling Auto-Detection"
|
||||
|
||||
---
|
||||
|
||||
### Backend Components
|
||||
|
||||
```mermaid
|
||||
@ -100,18 +429,24 @@ graph TD
|
||||
|
||||
subgraph "Command Handlers (commands/)"
|
||||
CMD_DB[db.rs\nIssue CRUD\nTimeline Events\n5-Whys Entries]
|
||||
CMD_AI[ai.rs\nChat Message\nLog Analysis\nProvider Test]
|
||||
CMD_AI[ai.rs\nChat Message\nLog Analysis\nProvider Test\nTool Calling Detection]
|
||||
CMD_ANALYSIS[analysis.rs\nLog Upload\nPII Detection\nRedaction Apply]
|
||||
CMD_DOCS[docs.rs\nRCA Generation\nPostmortem Gen\nDocument Export]
|
||||
CMD_INTEGRATIONS[integrations.rs\nConfluence\nServiceNow\nAzure DevOps\nOAuth Flow]
|
||||
CMD_SYSTEM[system.rs\nSettings CRUD\nOllama Mgmt\nAI Provider Mgmt\nAudit Log]
|
||||
CMD_SHELL[shell.rs\nKubeconfig CRUD\nCommand Execution\nExecution History]
|
||||
CMD_MCP[mcp/commands.rs\nMCP Server CRUD\nDiscovery\nTool/Resource Listing]
|
||||
end
|
||||
|
||||
subgraph "Domain Services"
|
||||
AI[AI Layer\nai/provider.rs\nTrait + Factory]
|
||||
TOOLS[AI Tools\nai/tools.rs\nStatic Tools Registry]
|
||||
AGENTS[AI Agents\nai/agents.rs\nAgent Registry]
|
||||
PII[PII Engine\npii/detector.rs\n12 Pattern Detectors]
|
||||
AUDIT[Audit Logger\naudit/log.rs\nHash-chained entries]
|
||||
DOCS_GEN[Doc Generator\ndocs/rca.rs\ndocs/postmortem.rs]
|
||||
SHELL[Shell System\nshell/classifier.rs\nshell/executor.rs\nshell/kubectl.rs]
|
||||
MCP[MCP Integration\nmcp/client.rs\nmcp/adapter.rs\nmcp/discovery.rs]
|
||||
end
|
||||
|
||||
subgraph "AI Providers (ai/)"
|
||||
@ -131,8 +466,8 @@ graph TD
|
||||
end
|
||||
|
||||
subgraph "Data Layer (db/)"
|
||||
MIGRATIONS[migrations.rs\n14 Schema Versions]
|
||||
MODELS[models.rs\nIssue / LogFile\nAiMessage / Document\nAuditEntry / Credential]
|
||||
MIGRATIONS[migrations.rs\n28 Schema Versions]
|
||||
MODELS[models.rs\nIssue / LogFile\nAiMessage / Document\nAuditEntry / Credential\nShellCommand / KubeconfigFile\nCommandExecution\nMcpServer / McpTool]
|
||||
CONNECTION[connection.rs\nSQLCipher Connect\nKey Auto-gen\nPlain→Encrypted Migration]
|
||||
end
|
||||
|
||||
@ -142,10 +477,15 @@ graph TD
|
||||
IPC --> CMD_DOCS
|
||||
IPC --> CMD_INTEGRATIONS
|
||||
IPC --> CMD_SYSTEM
|
||||
IPC --> CMD_SHELL
|
||||
IPC --> CMD_MCP
|
||||
|
||||
CMD_AI --> AI
|
||||
CMD_AI --> TOOLS
|
||||
CMD_ANALYSIS --> PII
|
||||
CMD_DOCS --> DOCS_GEN
|
||||
CMD_SHELL --> SHELL
|
||||
CMD_MCP --> MCP
|
||||
CMD_INTEGRATIONS --> CONFLUENCE
|
||||
CMD_INTEGRATIONS --> SERVICENOW
|
||||
CMD_INTEGRATIONS --> AZUREDEVOPS
|
||||
@ -158,9 +498,14 @@ graph TD
|
||||
AI --> GEMINI
|
||||
AI --> MISTRAL
|
||||
|
||||
TOOLS --> SHELL
|
||||
TOOLS --> MCP
|
||||
MCP --> AGENTS
|
||||
|
||||
CMD_DB --> MODELS
|
||||
CMD_AI --> AUDIT
|
||||
CMD_ANALYSIS --> AUDIT
|
||||
CMD_SHELL --> AUDIT
|
||||
MODELS --> MIGRATIONS
|
||||
MIGRATIONS --> CONNECTION
|
||||
|
||||
@ -351,20 +696,87 @@ erDiagram
|
||||
TEXT encrypted_api_key
|
||||
TEXT model
|
||||
TEXT config_json
|
||||
INTEGER supports_tool_calling
|
||||
}
|
||||
issues_fts {
|
||||
TEXT rowid FK
|
||||
TEXT title
|
||||
TEXT description
|
||||
}
|
||||
shell_commands {
|
||||
TEXT id PK
|
||||
TEXT command_template
|
||||
INTEGER tier
|
||||
TEXT description
|
||||
TEXT category
|
||||
}
|
||||
kubeconfig_files {
|
||||
TEXT id PK
|
||||
TEXT name
|
||||
TEXT encrypted_content
|
||||
TEXT context
|
||||
TEXT cluster_url
|
||||
INTEGER is_active
|
||||
}
|
||||
command_executions {
|
||||
TEXT id PK
|
||||
TEXT issue_id FK
|
||||
TEXT command
|
||||
INTEGER tier
|
||||
TEXT approval_status
|
||||
TEXT kubeconfig_id FK
|
||||
INTEGER exit_code
|
||||
TEXT stdout
|
||||
TEXT stderr
|
||||
INTEGER execution_time_ms
|
||||
TEXT executed_at
|
||||
}
|
||||
approval_decisions {
|
||||
TEXT id PK
|
||||
TEXT command_pattern
|
||||
TEXT decision
|
||||
TEXT session_id
|
||||
TEXT decided_at
|
||||
TEXT expires_at
|
||||
}
|
||||
mcp_servers {
|
||||
TEXT id PK
|
||||
TEXT name
|
||||
TEXT url
|
||||
TEXT transport_type
|
||||
TEXT auth_type
|
||||
TEXT auth_value
|
||||
INTEGER enabled
|
||||
TEXT discovery_status
|
||||
TEXT env_config
|
||||
}
|
||||
mcp_tools {
|
||||
TEXT id PK
|
||||
TEXT server_id FK
|
||||
TEXT name
|
||||
TEXT tool_key
|
||||
TEXT description
|
||||
TEXT parameters
|
||||
}
|
||||
mcp_resources {
|
||||
TEXT id PK
|
||||
TEXT server_id FK
|
||||
TEXT uri
|
||||
TEXT name
|
||||
TEXT description
|
||||
}
|
||||
|
||||
issues ||--o{ log_files : "has"
|
||||
issues ||--o{ ai_conversations : "has"
|
||||
issues ||--o{ resolution_steps : "has"
|
||||
issues ||--o{ documents : "has"
|
||||
issues ||--o{ command_executions : "has"
|
||||
issues ||--|| issues_fts : "indexed by"
|
||||
log_files ||--o{ pii_spans : "contains"
|
||||
ai_conversations ||--o{ ai_messages : "contains"
|
||||
command_executions }o--|| kubeconfig_files : "uses"
|
||||
mcp_servers ||--o{ mcp_tools : "exposes"
|
||||
mcp_servers ||--o{ mcp_resources : "exposes"
|
||||
```
|
||||
|
||||
### Data Flow — Issue Triage Lifecycle
|
||||
@ -457,7 +869,7 @@ graph TB
|
||||
subgraph "Layer 3: Key Management"
|
||||
DB_KEY[.dbkey file\nPer-install random 256-bit key\nMode 0600 — owner only]
|
||||
ENC_KEY[.enckey file\nPer-install random 256-bit key\nMode 0600 — owner only]
|
||||
ENV_OVERRIDE[TFTSR_DB_KEY / TFTSR_ENCRYPTION_KEY\nOptional env var override]
|
||||
ENV_OVERRIDE[TRCAA_DB_KEY / TRCAA_ENCRYPTION_KEY\nOptional env var override]
|
||||
end
|
||||
|
||||
subgraph "Layer 4: PII Protection"
|
||||
@ -486,6 +898,76 @@ graph TB
|
||||
style USER_APPROVE fill:#27ae60,color:#fff
|
||||
```
|
||||
|
||||
### Shell Execution Security (v1.0.0+)
|
||||
|
||||
**Three-tier safety classification protects against accidental or malicious command execution.**
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[AI Agent calls execute_shell_command] --> B[Parse command string]
|
||||
B --> C{Contains pipes or command substitution?}
|
||||
C -->|Yes| D[Parse into components]
|
||||
C -->|No| E[Single command]
|
||||
|
||||
D --> F[Classify each component]
|
||||
E --> F
|
||||
|
||||
F --> G{Highest tier?}
|
||||
|
||||
G -->|Tier 1| H[Read-only operations]
|
||||
G -->|Tier 2| I[Mutating operations]
|
||||
G -->|Tier 3| J[Destructive operations]
|
||||
|
||||
H --> K[Execute automatically]
|
||||
K --> L[Record to command_executions]
|
||||
L --> M[Return output to AI]
|
||||
|
||||
I --> N[Show approval modal to user]
|
||||
N --> O{User decision?}
|
||||
O -->|Allow Once| K
|
||||
O -->|Allow for Session| P[Store approval_decision]
|
||||
O -->|Deny| Q[Record denial]
|
||||
P --> K
|
||||
Q --> R[Return error to AI]
|
||||
|
||||
J --> S[Always reject]
|
||||
S --> Q
|
||||
|
||||
L --> T[Audit Log]
|
||||
Q --> T
|
||||
|
||||
style H fill:#e6f7e6
|
||||
style I fill:#fff4e6
|
||||
style J fill:#ffe6e6
|
||||
style S fill:#c0392b,color:#fff
|
||||
```
|
||||
|
||||
**Tier Classification Rules**:
|
||||
|
||||
| Tier | Safety Level | Examples | Action |
|
||||
|------|--------------|----------|--------|
|
||||
| Tier 1 | Read-only | `kubectl get`, `cat`, `grep`, `ls`, `pvecm status` | Auto-execute |
|
||||
| Tier 2 | Mutating | `kubectl apply`, `chmod`, `systemctl restart`, `ssh` | User approval |
|
||||
| Tier 3 | Destructive | `rm -rf`, `shutdown`, `mkfs`, `dd`, fork bombs | Always deny |
|
||||
|
||||
**Escalation Rules**:
|
||||
- Command with pipe (`|`) or chain (`&&`, `||`, `;`) → highest tier wins
|
||||
- Command substitution (`` `...` `` or `$(...)`) → escalate Tier 1 to Tier 2
|
||||
- Single Tier 3 command in chain → entire command becomes Tier 3
|
||||
|
||||
**Kubeconfig Encryption**:
|
||||
- All kubeconfig files encrypted with AES-256-GCM before storage
|
||||
- Decrypted on-demand for kubectl execution
|
||||
- Encryption key from `TRCAA_ENCRYPTION_KEY` env var or `.enckey` file
|
||||
|
||||
**Audit Trail**:
|
||||
- All commands logged to `command_executions` table
|
||||
- Includes: command text, tier, approval status, exit code, stdout, stderr, execution time
|
||||
- Linked to issue_id for incident context
|
||||
- Session-based approval decisions stored separately with expiration
|
||||
|
||||
---
|
||||
|
||||
### Authentication Flow — OAuth2 Integration
|
||||
|
||||
```mermaid
|
||||
@ -685,7 +1167,7 @@ graph LR
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Source Control"
|
||||
GOGS[Gogs / Gitea\ngogs.tftsr.com\nSarman Repository]
|
||||
GOGS[Gogs / Gitea\ngogs.trcaa.com\nSarman Repository]
|
||||
end
|
||||
|
||||
subgraph "CI/CD Triggers"
|
||||
@ -748,6 +1230,7 @@ graph TB
|
||||
MAC_PROC[trcaa process\nMach-O arm64 binary]
|
||||
WEBKIT[WKWebView\nSafari WebKit engine]
|
||||
MAC_DATA[~/Library/Application Support/trcaa/\n.dbkey mode 0600\n.enckey mode 0600\ntrcaa.db SQLCipher]
|
||||
MAC_KUBECTL[Bundled kubectl v1.30.0\narm64 binary]
|
||||
MAC_BUNDLE[Troubleshooting and RCA Assistant.app\n/Applications/]
|
||||
end
|
||||
|
||||
@ -755,6 +1238,7 @@ graph TB
|
||||
LINUX_PROC[trcaa process\nELF amd64/arm64]
|
||||
WEBKIT2[WebKitGTK WebView\nwebkit2gtk4.1]
|
||||
LINUX_DATA[~/.local/share/trcaa/\n.dbkey .enckey\ntrcaa.db]
|
||||
LINUX_KUBECTL[Bundled kubectl v1.30.0\namd64/arm64 binary]
|
||||
LINUX_PKG[.deb / .rpm / .AppImage]
|
||||
end
|
||||
|
||||
@ -762,20 +1246,24 @@ graph TB
|
||||
WIN_PROC[trcaa.exe\nPE amd64]
|
||||
WEBVIEW2[Microsoft WebView2\nChromium-based]
|
||||
WIN_DATA[%APPDATA%\trcaa\\\n.dbkey .enckey\ntrcaa.db]
|
||||
WIN_KUBECTL[Bundled kubectl.exe v1.30.0\namd64 binary]
|
||||
WIN_PKG[NSIS .exe / .msi]
|
||||
end
|
||||
|
||||
MAC_BUNDLE --> MAC_PROC
|
||||
MAC_PROC --> WEBKIT
|
||||
MAC_PROC --> MAC_DATA
|
||||
MAC_PROC --> MAC_KUBECTL
|
||||
|
||||
LINUX_PKG --> LINUX_PROC
|
||||
LINUX_PROC --> WEBKIT2
|
||||
LINUX_PROC --> LINUX_DATA
|
||||
LINUX_PROC --> LINUX_KUBECTL
|
||||
|
||||
WIN_PKG --> WIN_PROC
|
||||
WIN_PROC --> WEBVIEW2
|
||||
WIN_PROC --> WIN_DATA
|
||||
WIN_PROC --> WIN_KUBECTL
|
||||
```
|
||||
|
||||
---
|
||||
@ -820,7 +1308,7 @@ flowchart TD
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[App Launch] --> B{TFTSR_DB_KEY env var set?}
|
||||
A[App Launch] --> B{TRCAA_DB_KEY env var set?}
|
||||
B -->|Yes| C[Use env var key]
|
||||
B -->|No| D{Release build?}
|
||||
D -->|Debug| E[Use hardcoded dev key]
|
||||
@ -861,3 +1349,6 @@ See the [adrs/](./adrs/) directory for all Architecture Decision Records.
|
||||
| [ADR-004](./adrs/ADR-004-pii-regex-aho-corasick.md) | Regex + Aho-Corasick for PII Detection | Accepted |
|
||||
| [ADR-005](./adrs/ADR-005-auto-generate-encryption-keys.md) | Auto-generate Encryption Keys at Runtime | Accepted |
|
||||
| [ADR-006](./adrs/ADR-006-zustand-state-management.md) | Zustand for Frontend State Management | Accepted |
|
||||
| [ADR-007](./adrs/ADR-007-three-tier-shell-safety.md) | Three-Tier Shell Command Safety Classification | Accepted |
|
||||
| [ADR-008](./adrs/ADR-008-mcp-protocol-integration.md) | Model Context Protocol for External Tools | Accepted |
|
||||
| [ADR-009](./adrs/ADR-009-bundled-kubectl-binary.md) | Bundle kubectl Binary for Cross-Platform Consistency | Accepted |
|
||||
|
||||
@ -53,7 +53,7 @@ The `cipher_page_size = 16384` is specifically tuned for Apple Silicon (M-series
|
||||
Per ADR-005, encryption keys are auto-generated at runtime:
|
||||
- **Release builds**: Random 256-bit key generated at first launch, stored in `.dbkey` (mode 0600)
|
||||
- **Debug builds**: Hardcoded dev key (`dev-key-change-in-prod`)
|
||||
- **Override**: `TFTSR_DB_KEY` environment variable
|
||||
- **Override**: `TRCAA_DB_KEY` (or legacy `TRCAA_DB_KEY`) environment variable
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -9,12 +9,12 @@
|
||||
## Context
|
||||
|
||||
The application uses two encryption keys:
|
||||
1. **Database key** (`TFTSR_DB_KEY`): SQLCipher AES-256 key for the full database
|
||||
2. **Credential key** (`TFTSR_ENCRYPTION_KEY`): AES-256-GCM key for token/API key encryption
|
||||
1. **Database key** (`TRCAA_DB_KEY` (or legacy `TRCAA_DB_KEY`)): SQLCipher AES-256 key for the full database
|
||||
2. **Credential key** (`TRCAA_ENCRYPTION_KEY` (or legacy `TRCAA_ENCRYPTION_KEY`)): AES-256-GCM key for token/API key encryption
|
||||
|
||||
The original design required both to be set as environment variables in release builds. This caused:
|
||||
- **Critical failure on Mac**: Fresh installs would crash at startup with "file is not a database" error
|
||||
- **Silent failure on save**: Saving AI providers would fail with "TFTSR_ENCRYPTION_KEY must be set in release builds"
|
||||
- **Silent failure on save**: Saving AI providers would fail with "TRCAA_ENCRYPTION_KEY must be set in release builds"
|
||||
- **Developer friction**: Switching from `cargo tauri dev` (debug, plain SQLite) to a release build would crash because the existing plain database couldn't be opened as encrypted
|
||||
|
||||
---
|
||||
@ -29,8 +29,8 @@ Auto-generate cryptographically secure 256-bit keys at first launch and persist
|
||||
|
||||
| Key | File | Permissions | Location |
|
||||
|-----|------|-------------|----------|
|
||||
| Database | `.dbkey` | `0600` (owner r/w only) | `$TFTSR_DATA_DIR/` |
|
||||
| Credentials | `.enckey` | `0600` (owner r/w only) | `$TFTSR_DATA_DIR/` |
|
||||
| Database | `.dbkey` | `0600` (owner r/w only) | `$TRCAA_DATA_DIR/` |
|
||||
| Credentials | `.enckey` | `0600` (owner r/w only) | `$TRCAA_DATA_DIR/` |
|
||||
|
||||
**Platform data directories:**
|
||||
- macOS: `~/Library/Application Support/trcaa/`
|
||||
@ -42,7 +42,7 @@ Auto-generate cryptographically secure 256-bit keys at first launch and persist
|
||||
## Key Resolution Order
|
||||
|
||||
For both keys:
|
||||
1. Check environment variable (`TFTSR_DB_KEY` / `TFTSR_ENCRYPTION_KEY`) — use if set and non-empty
|
||||
1. Check environment variable (`TRCAA_DB_KEY` (or legacy `TRCAA_DB_KEY`) / `TRCAA_ENCRYPTION_KEY` (or legacy `TRCAA_ENCRYPTION_KEY`)) — use if set and non-empty
|
||||
2. If debug build — use hardcoded dev key (never touches filesystem)
|
||||
3. If `.dbkey` / `.enckey` exists and is non-empty — load from file
|
||||
4. Otherwise — generate 32 random bytes via `OsRng`, hex-encode to 64-char string, write to file with `mode 0600`
|
||||
@ -95,4 +95,4 @@ The `tauri-plugin-stronghold` already provides a keychain-like abstraction for c
|
||||
- Not suitable for multi-user scenarios where different users need isolated key material (single-user desktop app — acceptable)
|
||||
|
||||
**Mitigation for key loss:**
|
||||
Document clearly that backing up `$TFTSR_DATA_DIR` (including hidden files) preserves both key files and database. Loss of keys without losing the database = data loss.
|
||||
Document clearly that backing up `$TRCAA_DATA_DIR` (including hidden files) preserves both key files and database. Loss of keys without losing the database = data loss.
|
||||
|
||||
@ -40,7 +40,7 @@ Use **Zustand** for all three state categories, with selective persistence via `
|
||||
- Session is per-issue; loading a different issue should reset all session state
|
||||
- `reset()` method called on navigation away from triage
|
||||
|
||||
**`settingsStore`** — Persisted to localStorage as `"tftsr-settings"`:
|
||||
**`settingsStore`** — Persisted to localStorage as `"trcaa-settings"`:
|
||||
- Theme, active provider, PII pattern toggles — user preference, should survive restart
|
||||
- AI providers themselves are NOT persisted here — only `active_provider` string
|
||||
- Actual `ProviderConfig` (with encrypted API keys) lives in the backend DB, loaded via `load_ai_providers()`
|
||||
@ -59,7 +59,7 @@ The settings store persists to localStorage:
|
||||
persist(
|
||||
(set, get) => ({ ...storeImpl }),
|
||||
{
|
||||
name: 'tftsr-settings',
|
||||
name: 'trcaa-settings',
|
||||
partialize: (state) => ({
|
||||
theme: state.theme,
|
||||
active_provider: state.active_provider,
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
|
||||
## Context
|
||||
|
||||
TFTSR DevOps Investigation v1.0.0 introduced agentic shell command execution, allowing AI agents to execute kubectl, Proxmox, and general shell commands during troubleshooting conversations. This capability creates a significant security risk: malicious or hallucinated commands could cause data loss, service disruption, or unauthorized system access.
|
||||
TRCAA v1.0.0 introduced agentic shell command execution, allowing AI agents to execute kubectl, Proxmox, and general shell commands during troubleshooting conversations. This capability creates a significant security risk: malicious or hallucinated commands could cause data loss, service disruption, or unauthorized system access.
|
||||
|
||||
**Requirements**:
|
||||
- AI agents need shell access for diagnostics (kubectl, pvecm, qm, etc.)
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
|
||||
## Context
|
||||
|
||||
TFTSR DevOps Investigation v1.0.0 introduced agentic shell execution with statically-defined tools (`execute_shell_command`, `add_ado_comment`). As the application grows, we need a way to integrate external tools and services without hardcoding every integration into the Rust backend.
|
||||
TRCAA v1.0.0 introduced agentic shell execution with statically-defined tools (`execute_shell_command`, `add_ado_comment`). As the application grows, we need a way to integrate external tools and services without hardcoding every integration into the Rust backend.
|
||||
|
||||
**Requirements**:
|
||||
- AI agents need access to third-party tools (GitHub, Slack, monitoring systems, etc.)
|
||||
@ -106,7 +106,7 @@ CREATE TABLE mcp_tools (
|
||||
**Tool Calling Flow**:
|
||||
|
||||
1. User configures MCP server in Settings (name, URL/command, transport type, auth)
|
||||
2. Application connects and calls `list_tools()` to discover available tools
|
||||
2. TRCAA connects and calls `list_tools()` to discover available tools
|
||||
3. Tools stored in `mcp_tools` table with namespaced key (`server_name.tool_name`)
|
||||
4. AI agent requests tools via `get_enabled_mcp_tools()`
|
||||
5. MCP tools merged with static tools (`execute_shell_command`, `add_ado_comment`)
|
||||
@ -141,7 +141,7 @@ CREATE TABLE mcp_tools (
|
||||
- **Protocol churn risk**: MCP is new (May 2024), spec may evolve
|
||||
- **Dependency**: Relies on `rmcp` crate maintenance
|
||||
- **Stdio complexity**: Process spawning platform-dependent (Windows cmd.exe vs Unix bash)
|
||||
- **Debugging**: Tool call failures require inspecting both application logs and MCP server logs
|
||||
- **Debugging**: Tool call failures require inspecting both TRCAA logs and MCP server logs
|
||||
|
||||
### Trade-offs
|
||||
|
||||
@ -162,7 +162,7 @@ Args: @modelcontextprotocol/server-github
|
||||
Env: GITHUB_TOKEN=ghp_...
|
||||
```
|
||||
|
||||
Application spawns process, sends JSON-RPC 2.0 requests over stdin/stdout:
|
||||
TRCAA spawns process, sends JSON-RPC 2.0 requests over stdin/stdout:
|
||||
|
||||
```json
|
||||
{"jsonrpc":"2.0","method":"tools/list","id":1}
|
||||
@ -194,7 +194,7 @@ Auth Type: bearer
|
||||
Auth Value: eyJ...
|
||||
```
|
||||
|
||||
Application sends HTTP POST to `/mcp` with `Authorization: Bearer eyJ...` header.
|
||||
TRCAA sends HTTP POST to `/mcp` with `Authorization: Bearer eyJ...` header.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
|
||||
## Context
|
||||
|
||||
TFTSR DevOps Investigation v1.0.0 introduced `execute_shell_command` tool for AI agents, with kubectl as a primary use case (diagnosing Kubernetes pod failures, checking deployments, viewing logs). kubectl is a critical tool for IT troubleshooting but has several challenges:
|
||||
TRCAA v1.0.0 introduced `execute_shell_command` tool for AI agents, with kubectl as a primary use case (diagnosing Kubernetes pod failures, checking deployments, viewing logs). kubectl is a critical tool for IT troubleshooting but has several challenges:
|
||||
|
||||
**Problems with system kubectl**:
|
||||
- Version skew: User's kubectl may be v1.25 while cluster is v1.30 (API changes)
|
||||
@ -187,7 +187,7 @@ pub async fn execute_kubectl(command: &str, kubeconfig_id: Option<String>) -> Re
|
||||
### Negative
|
||||
|
||||
- **Installer size**: Increases by ~50MB per platform (150MB total for all platforms)
|
||||
- **Update lag**: kubectl version frozen until release
|
||||
- **Update lag**: kubectl version frozen until TRCAA release
|
||||
- **Disk usage**: Each install includes kubectl binary (no sharing across users)
|
||||
- **Maintenance**: Need to periodically update kubectl version
|
||||
|
||||
|
||||
74
docs/ticket-git-cliff-changelog.md
Normal file
@ -0,0 +1,74 @@
|
||||
# feat: Automated Changelog via git-cliff
|
||||
|
||||
## Description
|
||||
|
||||
Introduces automated changelog generation using **git-cliff**, a tool that parses
|
||||
conventional commits and produces formatted Markdown changelogs.
|
||||
|
||||
Previously, every Gitea release body contained only the static text `"Release vX.Y.Z"`.
|
||||
With this change, releases display a categorised, human-readable list of all commits
|
||||
since the previous version.
|
||||
|
||||
**Root cause / motivation:** No changelog tooling existed. The project follows
|
||||
Conventional Commits throughout but the information was never surfaced to end-users.
|
||||
|
||||
**Files changed:**
|
||||
- `cliff.toml` (new) — git-cliff configuration; defines commit parsers, ignored tags,
|
||||
output template, and which commit types appear in the changelog
|
||||
- `CHANGELOG.md` (new) — bootstrapped from all existing tags; maintained by CI going forward
|
||||
- `.gitea/workflows/auto-tag.yml` — new `changelog` job that runs after `autotag`
|
||||
- `docs/wiki/CICD-Pipeline.md` — "Changelog Generation" section added
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] `cliff.toml` present at repo root with working Tera template
|
||||
- [ ] `CHANGELOG.md` present at repo root, bootstrapped from all existing semver tags
|
||||
- [ ] `changelog` job in `auto-tag.yml` runs after `autotag` (parallel with build jobs)
|
||||
- [ ] Each Gitea release body shows grouped conventional-commit entries instead of
|
||||
static `"Release vX.Y.Z"`
|
||||
- [ ] `CHANGELOG.md` committed to master on every release with `[skip ci]` suffix
|
||||
(no infinite re-trigger loop)
|
||||
- [ ] `CHANGELOG.md` uploaded as a downloadable release asset
|
||||
- [ ] CI/chore/build/test/style commits excluded from changelog output
|
||||
- [ ] `docs/wiki/CICD-Pipeline.md` documents the changelog generation process
|
||||
|
||||
## Work Implemented
|
||||
|
||||
### `cliff.toml`
|
||||
- Tera template with proper whitespace control (`-%}` / `{%- `) for clean output
|
||||
- Included commit types: `feat`, `fix`, `perf`, `docs`, `refactor`
|
||||
- Excluded commit types: `ci`, `chore`, `build`, `test`, `style`
|
||||
- `ignore_tags = "rc|alpha|beta"` — pre-release tags excluded from version boundaries
|
||||
- `filter_unconventional = true` — non-conventional commits dropped silently
|
||||
- `sort_commits = "oldest"` — chronological order within each version
|
||||
|
||||
### `CHANGELOG.md`
|
||||
- Bootstrapped locally using git-cliff v2.7.0 (aarch64 musl binary)
|
||||
- Covers all tagged versions from `v0.1.0` through `v0.2.49` plus `[Unreleased]`
|
||||
- 267 lines covering the full project history
|
||||
|
||||
### `.gitea/workflows/auto-tag.yml` — `changelog` job
|
||||
- `needs: autotag` — waits for the new tag to exist before running
|
||||
- Full history clone: `git fetch --tags --depth=2147483647` so git-cliff can resolve
|
||||
all version boundaries
|
||||
- git-cliff v2.7.0 downloaded as a static x86_64 musl binary (~5 MB); no custom
|
||||
image required
|
||||
- Generates full `CHANGELOG.md` and per-release notes (`--latest --strip all`)
|
||||
- PATCHes the Gitea release body via API with JSON-safe escaping (`jq -Rs .`)
|
||||
- Commits `CHANGELOG.md` to master with `[skip ci]` to prevent workflow re-trigger
|
||||
- Deletes any existing `CHANGELOG.md` asset before re-uploading (rerun-safe)
|
||||
- Runs in parallel with all build jobs — no added wall-clock latency
|
||||
|
||||
### `docs/wiki/CICD-Pipeline.md`
|
||||
- Added "Changelog Generation" section before "Known Issues & Fixes"
|
||||
- Describes the five-step process, cliff.toml settings, and loop prevention mechanism
|
||||
|
||||
## Testing Needed
|
||||
|
||||
- [ ] Merge this PR to master; verify `changelog` CI job succeeds in Gitea Actions
|
||||
- [ ] Check Gitea release body for the new version tag — should show grouped commit list
|
||||
- [ ] Verify `CHANGELOG.md` was committed to master (check git log after CI runs)
|
||||
- [ ] Verify `CHANGELOG.md` appears as a downloadable asset on the release page
|
||||
- [ ] Push a subsequent commit to master; confirm the `[skip ci]` CHANGELOG commit does
|
||||
NOT trigger a second run of `auto-tag.yml`
|
||||
- [ ] Confirm CI/chore commits are absent from the release body
|
||||
@ -1,224 +0,0 @@
|
||||
# Version 1.0.7 Release Summary
|
||||
|
||||
**Release Date**: 2026-06-03
|
||||
**Type**: Bug Fix
|
||||
**Focus**: Ollama Function Calling Support
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Version 1.0.7 adds function calling (tool use) support to the Ollama AI provider, enabling local Ollama models to execute shell commands and interact with system tools just like OpenAI-compatible providers.
|
||||
|
||||
---
|
||||
|
||||
## What Changed
|
||||
|
||||
### Function Calling Support for Ollama
|
||||
|
||||
**Problem**: The Ollama provider was ignoring the `tools` parameter and could not execute function calls (like `execute_shell_command`). Models would output text descriptions of tool calls instead of actually invoking them.
|
||||
|
||||
**Solution**: Implemented full function calling support in the Ollama provider:
|
||||
|
||||
1. **Tool Registration**: Ollama provider now accepts and formats tools in the request
|
||||
2. **Tool Call Parsing**: Response handler parses `tool_calls` from Ollama API responses
|
||||
3. **Arguments Handling**: Supports both object and string argument formats
|
||||
4. **ID Generation**: Generates fallback IDs when Ollama doesn't provide them
|
||||
|
||||
**Files Changed**:
|
||||
- `src-tauri/src/ai/ollama.rs` - Added function calling support
|
||||
|
||||
---
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Ollama API Integration
|
||||
|
||||
The Ollama provider now sends tools in the request body:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "llama3.1:8b",
|
||||
"messages": [...],
|
||||
"stream": false,
|
||||
"tools": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "execute_shell_command",
|
||||
"description": "Execute shell commands...",
|
||||
"parameters": {...}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Response Parsing
|
||||
|
||||
Parses tool calls from Ollama's response format:
|
||||
|
||||
```json
|
||||
{
|
||||
"message": {
|
||||
"content": "...",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_123",
|
||||
"function": {
|
||||
"name": "execute_shell_command",
|
||||
"arguments": {"command": "kubectl get pods"}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Before vs After
|
||||
|
||||
### Before (v1.0.6)
|
||||
|
||||
**User**: "Can you tell me all the namespaces in my cluster?"
|
||||
|
||||
**Ollama Response** (broken):
|
||||
```
|
||||
tool_calls:
|
||||
- command: kubectl get ns --all-namespaces=false
|
||||
output_format: table
|
||||
```
|
||||
*Output is just text, no actual command execution*
|
||||
|
||||
### After (v1.0.7)
|
||||
|
||||
**User**: "Can you tell me all the namespaces in my cluster?"
|
||||
|
||||
**Ollama Response** (working):
|
||||
- Executes: `kubectl get namespaces`
|
||||
- Returns: Actual namespace list from cluster
|
||||
- Format: Natural language summary with data
|
||||
|
||||
---
|
||||
|
||||
## Impact
|
||||
|
||||
### User Benefits
|
||||
|
||||
- ✅ **Local Ollama models now work properly** with diagnostic commands
|
||||
- ✅ **No cloud API required** for function calling (privacy benefit)
|
||||
- ✅ **Consistent behavior** across OpenAI and Ollama providers
|
||||
- ✅ **Lower costs** by using local models for incident response
|
||||
|
||||
### Developer Benefits
|
||||
|
||||
- ✅ **Unified tool interface** across all providers
|
||||
- ✅ **Easier testing** with local models
|
||||
- ✅ **Better debugging** without API dependencies
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
### Test Cases
|
||||
|
||||
1. **Simple Information Query**:
|
||||
- Input: "What pods are running in my namespace?"
|
||||
- Expected: Executes `kubectl get pods -n <namespace>` and returns results
|
||||
|
||||
2. **Diagnostic Investigation**:
|
||||
- Input: "Investigate telemetry issues in cluster"
|
||||
- Expected: Executes multiple kubectl commands, analyzes results
|
||||
|
||||
3. **Tool Call Arguments**:
|
||||
- Test both object and string argument formats
|
||||
- Verify proper JSON serialization
|
||||
|
||||
### Verified Models
|
||||
|
||||
- ✅ `llama3.1:8b` - Full function calling support
|
||||
- ✅ `gemma4:e2b` - Full function calling support
|
||||
- ⚠️ Other models may require testing (phi3, mistral, codellama)
|
||||
|
||||
---
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### For Users
|
||||
|
||||
**No configuration changes required**. If you're using Ollama provider, function calling will now work automatically.
|
||||
|
||||
### For Developers
|
||||
|
||||
**No code changes required**. The Ollama provider signature matches the existing `Provider` trait.
|
||||
|
||||
---
|
||||
|
||||
## Known Limitations
|
||||
|
||||
1. **Model Support**: Function calling availability depends on the Ollama model's capabilities. Not all models support tools.
|
||||
|
||||
2. **Response Format**: Ollama's tool call format may vary slightly from OpenAI's. The provider handles common variations.
|
||||
|
||||
3. **Error Handling**: If Ollama returns malformed tool calls, they are skipped and the response content is returned instead.
|
||||
|
||||
---
|
||||
|
||||
## Related Issues
|
||||
|
||||
- Fixes: Tool calls not working with local Ollama
|
||||
- Related to: PR #40 (removed JSON examples from agent prompts)
|
||||
- Complements: liteLLM timeout fixes for remote models
|
||||
|
||||
---
|
||||
|
||||
## Upgrade Instructions
|
||||
|
||||
1. **Pull latest code**: `git pull origin main`
|
||||
2. **Rebuild application**: `npm run tauri build`
|
||||
3. **Install updated app**: Replace existing installation
|
||||
4. **Test function calling**: Use Ollama provider with diagnostic queries
|
||||
|
||||
---
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Potential Improvements
|
||||
|
||||
1. **Streaming Support**: Add function calling for streaming responses
|
||||
2. **Tool Choice Control**: Support `tool_choice` parameter (auto/required/none)
|
||||
3. **Parallel Tool Calls**: Handle multiple simultaneous tool invocations
|
||||
4. **Model Capability Detection**: Auto-detect which Ollama models support tools
|
||||
|
||||
### Compatibility
|
||||
|
||||
This release maintains backward compatibility with:
|
||||
- OpenAI provider function calling
|
||||
- Anthropic provider function calling
|
||||
- Gemini provider function calling
|
||||
- Custom provider formats
|
||||
|
||||
---
|
||||
|
||||
## Credits
|
||||
|
||||
- **Issue Identification**: Testing revealed Ollama tool calling regression after PR #40
|
||||
- **Root Cause Analysis**: Ollama provider was ignoring tools parameter entirely
|
||||
- **Implementation**: Added full function calling support matching OpenAI format
|
||||
- **Testing**: Verified with llama3.1:8b and gemma4:e2b models
|
||||
|
||||
---
|
||||
|
||||
## Version History
|
||||
|
||||
- **v1.0.7** (2026-06-03): Added Ollama function calling support
|
||||
- **v1.0.6** (2026-06-03): Removed JSON examples from agent prompts
|
||||
- **v1.0.5** (2026-06-03): Agent output quality improvements
|
||||
|
||||
---
|
||||
|
||||
**Release Type**: Bug Fix
|
||||
**Breaking Changes**: None
|
||||
**API Changes**: None (internal implementation only)
|
||||
**Documentation Updated**: Yes
|
||||
@ -1,279 +0,0 @@
|
||||
# Version 1.0.8 Release Summary
|
||||
|
||||
**Release Date**: 2026-06-03
|
||||
**Type**: Bug Fix + Enhancements
|
||||
**Focus**: Ollama Connection Reliability
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Version 1.0.8 improves Ollama provider connection reliability with extended timeouts, retry logic, and health checks. Also updates model recommendations to require ≥3B parameters for reliable tool calling.
|
||||
|
||||
---
|
||||
|
||||
## What Changed
|
||||
|
||||
### Connection Reliability Improvements
|
||||
|
||||
**Problem**: Users experiencing intermittent "cannot be reached" errors and timeouts when using Ollama for tool calling.
|
||||
|
||||
**Solution**: Comprehensive connection reliability improvements:
|
||||
|
||||
1. **Extended Timeouts**
|
||||
- 180s timeout for tool calling (vs 60s for regular chat)
|
||||
- 10s connect timeout to fail fast on unreachable servers
|
||||
- Tool calling requires more time for structured output generation
|
||||
|
||||
2. **Health Check Before Requests**
|
||||
- Quick `/api/tags` endpoint check before attempting chat
|
||||
- Prevents wasted time on requests to unresponsive servers
|
||||
- Better error messages distinguishing connection vs API failures
|
||||
|
||||
3. **Retry Logic**
|
||||
- 3 attempts total with 2s delay between retries
|
||||
- Retries on: connection errors, server errors (5xx), JSON parse errors
|
||||
- Last error captured and reported for debugging
|
||||
|
||||
4. **Auto-Start Improvements**
|
||||
- 2s initialization delay after auto-start to allow Ollama to fully start
|
||||
- Prevents immediate connection failures after service start
|
||||
|
||||
### Model Recommendations Update (Breaking)
|
||||
|
||||
**Problem**: Models <3B parameters cannot reliably follow tool calling instructions.
|
||||
|
||||
**Testing Results**:
|
||||
- ✅ `llama3.2:3b` and larger: Properly invoke tools
|
||||
- ❌ `llama3.2:1b`: Describes tools in text instead of calling them
|
||||
|
||||
**Updated Default Model List**:
|
||||
|
||||
| Model | Size | Min RAM | Notes |
|
||||
|-------|------|---------|-------|
|
||||
| `llama3.2:3b` | 2.0 GB | 6 GB | Balanced performance |
|
||||
| `phi3.5:3.8b` | 2.2 GB | 6 GB | Excellent reasoning |
|
||||
| `llama3.1:8b` | 4.7 GB | 10 GB | **RECOMMENDED** |
|
||||
| `qwen2.5:14b` | 9.0 GB | 16 GB | Best for complex analysis |
|
||||
| `gemma2:9b` | 5.5 GB | 12 GB | Google's efficient model |
|
||||
|
||||
**Removed Models**: Generic model names without size tags (`llama3.1`, `llama3`, `mistral`, `codellama`, `phi3`)
|
||||
|
||||
---
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Retry Logic Implementation
|
||||
|
||||
```rust
|
||||
let max_retries = 2;
|
||||
for attempt in 0..=max_retries {
|
||||
if attempt > 0 {
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
}
|
||||
|
||||
match client.post(&url).send().await {
|
||||
Ok(resp) if resp.status().is_success() => {
|
||||
// Success - parse and return
|
||||
}
|
||||
Ok(resp) if resp.status().is_server_error() && attempt < max_retries => {
|
||||
continue; // Retry on 5xx
|
||||
}
|
||||
Err(e) if attempt < max_retries => {
|
||||
continue; // Retry connection errors
|
||||
}
|
||||
_ => {
|
||||
// Final failure - report error
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Health Check
|
||||
|
||||
```rust
|
||||
let health_check_result = client
|
||||
.get(format!("{base_url}/api/tags"))
|
||||
.send()
|
||||
.await;
|
||||
|
||||
match health_check_result {
|
||||
Ok(resp) if resp.status().is_success() => {
|
||||
// Ollama is ready
|
||||
}
|
||||
_ => {
|
||||
anyhow::bail!("Cannot connect to Ollama. Please ensure Ollama is running.");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Files Changed
|
||||
|
||||
1. **src-tauri/src/ai/ollama.rs** (+100 lines, -90 lines)
|
||||
- Extended timeout: 180s for tool calling, 60s for chat
|
||||
- Added connect_timeout: 10s
|
||||
- Implemented retry logic with 3 attempts
|
||||
- Added health check before chat requests
|
||||
- Added 2s delay after auto-start
|
||||
- Updated model list to ≥3B parameters
|
||||
|
||||
2. **docs/wiki/AI-Providers.md** (+60 lines)
|
||||
- Updated Ollama section with tool calling details
|
||||
- Added model recommendations table with size/RAM requirements
|
||||
- Added troubleshooting section
|
||||
- Added performance tips
|
||||
|
||||
3. **package.json, src-tauri/Cargo.toml, src-tauri/tauri.conf.json**
|
||||
- Version: 1.0.7 → 1.0.8
|
||||
|
||||
4. **src-tauri/Cargo.lock** (auto-updated)
|
||||
|
||||
---
|
||||
|
||||
## Before vs After
|
||||
|
||||
### Before (v1.0.7)
|
||||
|
||||
**User Experience:**
|
||||
- Intermittent connection failures
|
||||
- 60s timeout insufficient for tool calling
|
||||
- No retry on transient errors
|
||||
- Generic error: "Failed to connect to Ollama"
|
||||
|
||||
**Model Issues:**
|
||||
- Users could select 1B models
|
||||
- Models would describe tools instead of calling them
|
||||
- Confusing experience with no clear guidance
|
||||
|
||||
### After (v1.0.8)
|
||||
|
||||
**User Experience:**
|
||||
- Health check prevents wasted requests
|
||||
- 180s timeout sufficient for tool calling
|
||||
- 3 retry attempts handle transient failures
|
||||
- Clear error messages: "Ollama is not ready" vs "Connection error"
|
||||
|
||||
**Model Guidance:**
|
||||
- Only ≥3B models shown in dropdown
|
||||
- Clear RAM requirements in documentation
|
||||
- Working tool calling for all recommended models
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
### Connection Reliability
|
||||
|
||||
1. ✅ **Health Check**: Ollama service stopped → immediate clear error
|
||||
2. ✅ **Retry Logic**: Simulated network glitch → 3 attempts with 2s delay
|
||||
3. ✅ **Extended Timeout**: Tool calling with llama3.1:8b → completes within 180s
|
||||
4. ✅ **Auto-Start**: First request → Ollama starts, 2s delay, successful connection
|
||||
|
||||
### Model Testing
|
||||
|
||||
1. ✅ **llama3.2:3b**: Proper tool calls, reasonable response time
|
||||
2. ✅ **phi3.5:3.8b**: Excellent tool calling, fast responses
|
||||
3. ✅ **llama3.1:8b**: Best overall performance, recommended
|
||||
4. ✅ **qwen2.5:14b**: Excellent for complex queries, slower but thorough
|
||||
5. ✅ **gemma2:9b**: Good balance of size and capability
|
||||
6. ⚠️ **llama3.2:1b**: Correctly describes tools in text (as expected for <3B model)
|
||||
|
||||
---
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### For Users
|
||||
|
||||
**No configuration changes required** if using recommended models (≥3B).
|
||||
|
||||
**If using 1B models:**
|
||||
1. Open Settings → AI Providers → Ollama
|
||||
2. Select a model ≥3B parameters (e.g., `llama3.2:3b`)
|
||||
3. Ensure model is pulled: `ollama pull llama3.2:3b`
|
||||
|
||||
### For Developers
|
||||
|
||||
**No code changes required**. Timeout and retry improvements are automatic.
|
||||
|
||||
**Model list now enforces ≥3B**: Update `ollama.rs::info()` if custom models needed.
|
||||
|
||||
---
|
||||
|
||||
## Known Limitations
|
||||
|
||||
### Ollama Provider
|
||||
|
||||
1. **Model Loading Time**: First request loads model into VRAM (5-10s delay)
|
||||
2. **Memory Usage**: Larger models use significant RAM/VRAM
|
||||
3. **Quantization Trade-offs**: Lower quantization (Q3_K_M) faster but less accurate
|
||||
4. **Concurrent Requests**: Ollama processes requests sequentially
|
||||
|
||||
### Tool Calling (Applies to ALL Providers)
|
||||
|
||||
1. **Model Size**: <3B parameters insufficient for reliable structured output
|
||||
2. **Response Time**: Tool calling 2-3x slower than regular chat
|
||||
3. **Multi-turn Complexity**: Deep tool conversations may hit iteration limits
|
||||
|
||||
---
|
||||
|
||||
## Performance Impact
|
||||
|
||||
### Positive
|
||||
|
||||
- ✅ Retry logic improves success rate by ~15% (transient failures recovered)
|
||||
- ✅ Health check prevents wasted 60-180s timeouts on down servers
|
||||
- ✅ Extended timeout eliminates premature failures on tool calling
|
||||
|
||||
### Neutral
|
||||
|
||||
- Health check adds ~50-100ms per request (negligible)
|
||||
- Auto-start delay adds 2s on first request only (one-time per session)
|
||||
|
||||
### Trade-offs
|
||||
|
||||
- Retry logic can extend failed requests from 60s to 186s (3 × 60s + 2 × 2s delay)
|
||||
- Users get result instead of error, so perceived as improvement
|
||||
|
||||
---
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Potential Improvements
|
||||
|
||||
1. **Adaptive Timeout**: Detect model size and adjust timeout dynamically
|
||||
2. **Model Caching**: Pre-load models on application start
|
||||
3. **Streaming Support**: Real-time token streaming for faster perceived responses
|
||||
4. **Parallel Requests**: Queue multiple Ollama requests (requires Ollama enhancement)
|
||||
5. **GPU Detection**: Recommend models based on available VRAM
|
||||
|
||||
### Compatibility
|
||||
|
||||
This release maintains backward compatibility with:
|
||||
- v1.0.7 Ollama function calling
|
||||
- All other AI providers (OpenAI, Anthropic, Gemini, Mistral, LiteLLM)
|
||||
- Existing model configurations (users can still manually type 1B model names)
|
||||
|
||||
---
|
||||
|
||||
## Related Issues
|
||||
|
||||
- Builds on: PR #41 (v1.0.7 - Ollama function calling support)
|
||||
- Fixes: Intermittent "cannot be reached" errors during testing
|
||||
|
||||
---
|
||||
|
||||
## Version History
|
||||
|
||||
- **v1.0.8** (2026-06-03): Connection reliability + model recommendations
|
||||
- **v1.0.7** (2026-06-03): Ollama function calling support
|
||||
- **v1.0.6** (2026-06-03): Removed JSON examples from agent prompts
|
||||
- **v1.0.5** (2026-06-03): Agent output quality improvements
|
||||
|
||||
---
|
||||
|
||||
**Release Type**: Bug Fix + Enhancements
|
||||
**Breaking Changes**: None (model list updated but user can still type 1B models)
|
||||
**API Changes**: None (internal implementation only)
|
||||
**Documentation Updated**: Yes (wiki + v1.0.8-summary.md)
|
||||
@ -1,6 +1,6 @@
|
||||
# AI Providers
|
||||
|
||||
TFTSR supports 6+ AI providers, including custom providers with flexible authentication and API formats. API keys are stored encrypted with AES-256-GCM.
|
||||
TRCAA supports 6+ AI providers, including custom providers with flexible authentication and API formats. API keys are stored encrypted with AES-256-GCM.
|
||||
|
||||
## Provider Factory
|
||||
|
||||
@ -154,11 +154,11 @@ The domain prompt is injected as the first `system` role message in every new co
|
||||
|
||||
---
|
||||
|
||||
## 6. Custom Provider (Custom REST & Others)
|
||||
## 6. Custom Provider (Multiple API Formats)
|
||||
|
||||
**Status:** ✅ **Implemented** (v0.2.6)
|
||||
|
||||
Custom providers allow integration with non-OpenAI-compatible APIs. The application supports two API formats:
|
||||
Custom providers allow integration with non-OpenAI-compatible APIs. The application supports multiple API formats:
|
||||
|
||||
### Format: OpenAI Compatible (Default)
|
||||
|
||||
@ -178,9 +178,42 @@ Standard OpenAI `/chat/completions` endpoint with Bearer authentication.
|
||||
|
||||
---
|
||||
|
||||
### Format: Custom REST
|
||||
### Format: TFTSR GenAI
|
||||
|
||||
**Enterprise AI Gateway** — For AI platforms that use a non-OpenAI request/response format with centralized cost tracking and model access.
|
||||
**TFTSR GenAI Gateway** — Enterprise AI gateway with model proxying and cost tracking.
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| `config.provider_type` | `"custom"` |
|
||||
| `config.api_format` | `"generic-genai"` |
|
||||
| Status | ⚠️ **Limited compatibility** |
|
||||
|
||||
**Known Limitations:**
|
||||
- ❌ **Tool calling not supported**: Gateway returns `503 Service Unavailable` with error `"Gemini Filter Triggered: UNEXPECTED_TOOL_CALL"`
|
||||
- ❌ **Shell execution unavailable**: Cannot use `execute_shell_command` or other function calling features
|
||||
- ✅ **Basic chat works**: Text-only conversations function correctly
|
||||
- ✅ **Workaround parser included**: Attempts to extract tool calls from malformed responses (ChatGPT JSON in `msg` field, Claude XML wrapper)
|
||||
|
||||
**Recommendation**: Use **LiteLLM + AWS Bedrock** (see [LiteLLM Setup Guide](LiteLLM-Bedrock-Setup)) or **Ollama** for full tool calling support.
|
||||
|
||||
**Root Cause**: TFTSR GenAI gateway applies content filtering that blocks structured tool call responses before they reach the client. This is a gateway-level restriction that cannot be worked around from the client side.
|
||||
|
||||
**Configuration (if needed for text-only use):**
|
||||
```
|
||||
Name: TFTSR GenAI
|
||||
Type: Custom
|
||||
API Format: TFTSR GenAI
|
||||
API URL: https://your-gateway/api/v2/chat
|
||||
Model: your-model-name
|
||||
API Key: (your API key)
|
||||
User ID: user@example.com (optional, for cost tracking)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Format: Custom REST (Generic)
|
||||
|
||||
**Generic Enterprise AI Gateway** — For AI platforms that use a non-OpenAI request/response format with centralized cost tracking and model access.
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
@ -259,12 +292,67 @@ All providers support the following optional configuration fields (v0.2.6+):
|
||||
| `api_format` | `Option<String>` | API format (`openai` or `custom_rest`) | `openai` |
|
||||
| `session_id` | `Option<String>` | Session ID for stateful APIs | None |
|
||||
| `user_id` | `Option<String>` | User ID for cost tracking (Custom REST gateways) | None |
|
||||
| `supports_tool_calling` | `Option<bool>` | Enable function/tool calling | `true` for built-in providers, `false` for custom |
|
||||
|
||||
**Backward Compatibility:**
|
||||
All fields are optional and default to OpenAI-compatible behavior. Existing provider configurations are unaffected.
|
||||
|
||||
---
|
||||
|
||||
## Tool Calling Auto-Detection
|
||||
|
||||
**Status:** ✅ **Implemented** (v1.0.9+)
|
||||
|
||||
TRCAA can automatically detect whether a custom AI provider supports tool calling (function calling) by sending a test tool call and analyzing the response.
|
||||
|
||||
### How It Works
|
||||
|
||||
1. Navigate to **Settings → AI Providers** → Add/Edit Custom Provider
|
||||
2. Configure your provider (API URL, key, model)
|
||||
3. Click **"Auto-Detect Tool Calling Support"** button
|
||||
4. System sends a simple test tool call to the provider
|
||||
5. Checkbox automatically enabled/disabled based on result
|
||||
6. Success/warning message displayed
|
||||
|
||||
### Detection Criteria
|
||||
|
||||
| Scenario | Result | Explanation |
|
||||
|----------|--------|-------------|
|
||||
| Provider returns `tool_calls` array with test tool | ✅ Tool calling supported | Checkbox enabled automatically |
|
||||
| Provider responds without tool_calls | ⚠️ Not supported | Checkbox disabled automatically |
|
||||
| Gateway returns 503 / "tool" error | ⚠️ Blocked at gateway level | Checkbox disabled (e.g., TFTSR GenAI) |
|
||||
| Connection/auth/timeout error | ❌ Error displayed | User must fix connection issue |
|
||||
|
||||
### Test Tool
|
||||
|
||||
The auto-detection sends this minimal tool:
|
||||
|
||||
```rust
|
||||
{
|
||||
"name": "test_tool",
|
||||
"description": "A test tool that returns 'success'. Call this tool with no arguments.",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Known Limitations
|
||||
|
||||
- **TFTSR GenAI**: Gateway blocks tool calls with `503 UNEXPECTED_TOOL_CALL` before they reach the model. Auto-detect correctly identifies this as "not supported."
|
||||
- **Small Models**: Models <3B parameters (e.g., `llama3.2:1b`) may respond but describe tools instead of calling them. Auto-detect may return `true` (model capability) but runtime behavior will fail.
|
||||
- **Timeout**: Detection uses same timeout as regular chat (60-180s depending on provider). Slow providers may timeout during detection.
|
||||
|
||||
### Manual Override
|
||||
|
||||
You can always manually toggle the `supports_tool_calling` checkbox:
|
||||
- ✅ Enable: For providers you know support tool calling
|
||||
- ❌ Disable: For text-only chat without shell execution or integrations
|
||||
|
||||
---
|
||||
|
||||
## Adding a New Provider
|
||||
|
||||
1. Create `src-tauri/src/ai/{name}.rs` implementing the `Provider` trait
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
## Overview
|
||||
|
||||
TFTSR uses a Tauri 2.x architecture: a Rust backend runs natively, and a React/TypeScript frontend runs in an embedded WebView. Communication between them happens exclusively via typed IPC (`invoke()`).
|
||||
TRCAA uses a Tauri 2.x architecture: a Rust backend runs natively, and a React/TypeScript frontend runs in an embedded WebView. Communication between them happens exclusively via typed IPC (`invoke()`).
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
@ -229,7 +229,7 @@ Timeline events are stored in the `timeline_events` table (indexed by issue_id a
|
||||
|
||||
```
|
||||
1. Initialize tracing (RUST_LOG controls level)
|
||||
2. Determine data directory (~/.local/share/tftsr or TFTSR_DATA_DIR)
|
||||
2. Determine data directory (~/.local/share/tftsr or TRCAA_DATA_DIR)
|
||||
3. Open / create SQLite database (run migrations)
|
||||
4. Create AppState (db + settings + app_data_dir)
|
||||
5. Register Tauri plugins (stronghold, dialog, fs, shell, http, cli, updater)
|
||||
|
||||
@ -5,17 +5,16 @@
|
||||
| Component | URL | Notes |
|
||||
|-----------|-----|-------|
|
||||
| Gitea | `https://gogs.tftsr.com` / `http://172.0.0.29:3000` | Git server (migrated from Gogs 0.14) |
|
||||
| Gitea Actions | Built into Gitea | Native GitHub Actions-compatible CI/CD |
|
||||
| Gitea Actions (direct) | `http://gitea.tftsr.com:8084` | v2.x |
|
||||
| Gitea Actions (proxy) | `http://gitea.tftsr.com:8085` | nginx reverse proxy |
|
||||
| PostgreSQL (Gitea DB) | Container: `gogs_postgres_db` | DB: `gogsdb`, User: `gogs` |
|
||||
|
||||
**CI/CD System:** Gitea Actions (v1.22+) with native GitHub Actions API compatibility. Uses `.gitea/workflows/*.yml` for workflow definitions.
|
||||
|
||||
### CI Agents
|
||||
|
||||
| Agent | Platform | Host | Purpose |
|
||||
|-------|----------|------|---------|
|
||||
| `gitea_act_runner_amd64` (Docker) | `linux-amd64` | 172.0.0.29 | Native x86_64 — test builds + amd64/windows release |
|
||||
| `act_runner` (systemd) | `linux-arm64` | 172.0.0.29 | Native aarch64 — arm64 release builds |
|
||||
| `gitea_act_runner_amd64` (Docker) | `linux-amd64` | gitea.tftsr.com | Native x86_64 — test builds + amd64/windows release |
|
||||
| `act_runner` (systemd) | `linux-arm64` | gitea.tftsr.com | Native aarch64 — arm64 release builds |
|
||||
| `act_runner` (launchd) | `macos-arm64` | sarman's local Mac | Native Apple Silicon — macOS `.dmg` release builds |
|
||||
|
||||
Agent labels configured in `~/.config/act_runner/config.yaml`:
|
||||
@ -47,7 +46,7 @@ Rust toolchain, cross-compilers) so that CI jobs skip package installation entir
|
||||
2. Confirm all 3 images appear in the Gitea package/container registry at `172.0.0.29:3000`
|
||||
3. Only then merge workflow changes that depend on the new image contents
|
||||
|
||||
**Server prerequisite — insecure registry** (one-time, on 172.0.0.29):
|
||||
**Server prerequisite — insecure registry** (one-time, on gitea.tftsr.com):
|
||||
```sh
|
||||
echo '{"insecure-registries":["172.0.0.29:3000"]}' | sudo tee /etc/docker/daemon.json
|
||||
sudo systemctl restart docker
|
||||
@ -60,7 +59,7 @@ daemon to pull from the local HTTP registry.
|
||||
## Cargo and npm Caching
|
||||
|
||||
All Rust and build jobs use `actions/cache@v3` to cache downloaded package artifacts.
|
||||
Gitea 1.22 implements the GitHub Actions cache API natively.
|
||||
Gitea 1.22 implements the Gitea Actions cache API natively.
|
||||
|
||||
**Cargo cache** (Rust jobs):
|
||||
```yaml
|
||||
@ -136,7 +135,7 @@ Jobs (run in parallel after autotag):
|
||||
build-macos-arm64 → cargo tauri build (aarch64-apple-darwin) — runs on local Mac
|
||||
→ {.dmg} uploaded to Gitea release
|
||||
→ existing same-name assets are deleted before upload (rerun-safe)
|
||||
→ unsigned; after install run: xattr -cr /Applications/TFTSR.app
|
||||
→ unsigned; after install run: xattr -cr /Applications/TRCAA.app
|
||||
```
|
||||
|
||||
**Per-step agent routing (Woodpecker 2.x labels):**
|
||||
@ -145,7 +144,7 @@ Jobs (run in parallel after autotag):
|
||||
steps:
|
||||
- name: build-linux-amd64
|
||||
labels:
|
||||
platform: linux/amd64 # → woodpecker_agent on 172.0.0.29
|
||||
platform: linux/amd64 # → woodpecker_agent on gitea.tftsr.com
|
||||
|
||||
- name: build-linux-arm64
|
||||
labels:
|
||||
@ -235,7 +234,7 @@ No DB config path switching needed (unlike Woodpecker 0.15.4).
|
||||
After migration, Woodpecker 2.x registers webhooks automatically when a repo is
|
||||
activated via the UI. No manual JWT-signed webhook setup required.
|
||||
|
||||
1. Log in at `http://172.0.0.29:8085` via Gitea OAuth2
|
||||
1. Log in at `http://gitea.tftsr.com:8085` via Gitea OAuth2
|
||||
2. Add repo `sarman/tftsr-devops_investigation`
|
||||
3. Woodpecker creates webhook in Gitea automatically
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
## Overview
|
||||
|
||||
TFTSR uses **SQLite** via `rusqlite` with the `bundled-sqlcipher` feature for AES-256 encryption in production. 22 versioned migrations are tracked in the `_migrations` table.
|
||||
TRCAA uses **SQLite** via `rusqlite` with the `bundled-sqlcipher` feature for AES-256 encryption in production. 22 versioned migrations are tracked in the `_migrations` table.
|
||||
|
||||
**DB file location:** `{app_data_dir}/tftsr.db`
|
||||
|
||||
@ -13,7 +13,7 @@ TFTSR uses **SQLite** via `rusqlite` with the `bundled-sqlcipher` feature for AE
|
||||
| Build type | Encryption | Key |
|
||||
|-----------|-----------|-----|
|
||||
| Debug (`debug_assertions`) | None (plain SQLite) | — |
|
||||
| Release | SQLCipher AES-256 | `TFTSR_DB_KEY` env var |
|
||||
| Release | SQLCipher AES-256 | `TRCAA_DB_KEY` (or legacy `TRCAA_DB_KEY`) env var |
|
||||
|
||||
**SQLCipher settings (production):**
|
||||
- Cipher: AES-256-CBC
|
||||
@ -24,7 +24,7 @@ TFTSR uses **SQLite** via `rusqlite` with the `bundled-sqlcipher` feature for AE
|
||||
```rust
|
||||
// Simplified init logic
|
||||
pub fn init_db(data_dir: &Path) -> anyhow::Result<Connection> {
|
||||
let key = env::var("TFTSR_DB_KEY")
|
||||
let key = env::var("TRCAA_DB_KEY")
|
||||
.unwrap_or_else(|_| "dev-key-change-in-prod".to_string());
|
||||
let conn = if cfg!(debug_assertions) {
|
||||
Connection::open(db_path)? // plain SQLite
|
||||
@ -236,7 +236,7 @@ CREATE TABLE image_attachments (
|
||||
|
||||
**Encryption:**
|
||||
- OAuth2 tokens encrypted with AES-256-GCM
|
||||
- Key derived from `TFTSR_DB_KEY` environment variable
|
||||
- Key derived from `TRCAA_DB_KEY` (or legacy `TRCAA_DB_KEY`) environment variable
|
||||
- Random 96-bit nonce per encryption
|
||||
- Format: `base64(nonce || ciphertext || tag)`
|
||||
|
||||
@ -389,96 +389,6 @@ CREATE VIEW IF NOT EXISTS v_image_attachments_with_issue AS
|
||||
|
||||
Used by `list_all_log_files` and `list_all_image_attachments` to power the cross-incident Attachments tab in the History page. Explicitly selects named columns (not `SELECT *`) to avoid including the BLOB data in list queries.
|
||||
|
||||
### 023 — MCP Resources table (MCP Integration v0.3.0+)
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS mcp_resources (
|
||||
id TEXT PRIMARY KEY,
|
||||
server_id TEXT NOT NULL,
|
||||
uri TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
mime_type TEXT,
|
||||
discovered_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
FOREIGN KEY(server_id) REFERENCES mcp_servers(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX idx_mcp_resources_server ON mcp_resources(server_id);
|
||||
```
|
||||
|
||||
Stores resources (files, data sources) exposed by MCP servers for AI agent access.
|
||||
|
||||
### 024 — shell_commands table (Shell Execution v1.0.0+)
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS shell_commands (
|
||||
id TEXT PRIMARY KEY,
|
||||
command_template TEXT NOT NULL,
|
||||
tier INTEGER NOT NULL CHECK(tier IN (1, 2, 3)),
|
||||
description TEXT,
|
||||
category TEXT NOT NULL, -- 'kubectl', 'proxmox', 'general'
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
```
|
||||
|
||||
Pre-defined command templates with tier classification for the three-tier safety system. See [[Shell-Execution]] for details.
|
||||
|
||||
### 025 — kubeconfig_files table (Shell Execution v1.0.0+)
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS kubeconfig_files (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
encrypted_content TEXT NOT NULL,
|
||||
context TEXT NOT NULL,
|
||||
cluster_url TEXT,
|
||||
is_active INTEGER NOT NULL DEFAULT 0,
|
||||
uploaded_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
CREATE INDEX idx_kubeconfig_active ON kubeconfig_files(is_active);
|
||||
```
|
||||
|
||||
Encrypted storage for kubectl configuration files. Content encrypted with AES-256-GCM. Only one config can be active at a time.
|
||||
|
||||
### 026 — command_executions table (Shell Execution v1.0.0+)
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS command_executions (
|
||||
id TEXT PRIMARY KEY,
|
||||
issue_id TEXT,
|
||||
command TEXT NOT NULL,
|
||||
tier INTEGER NOT NULL,
|
||||
approval_status TEXT NOT NULL, -- 'auto', 'approved', 'denied'
|
||||
kubeconfig_id TEXT,
|
||||
exit_code INTEGER,
|
||||
stdout TEXT,
|
||||
stderr TEXT,
|
||||
execution_time_ms INTEGER,
|
||||
executed_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (kubeconfig_id) REFERENCES kubeconfig_files(id) ON DELETE SET NULL
|
||||
);
|
||||
CREATE INDEX idx_command_executions_issue ON command_executions(issue_id);
|
||||
CREATE INDEX idx_command_executions_executed ON command_executions(executed_at);
|
||||
```
|
||||
|
||||
Complete audit trail of all shell command executions with exit codes, stdout/stderr capture, and execution timing.
|
||||
|
||||
### 027 — approval_decisions table (Shell Execution v1.0.0+)
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS approval_decisions (
|
||||
id TEXT PRIMARY KEY,
|
||||
command_pattern TEXT NOT NULL,
|
||||
decision TEXT NOT NULL CHECK(decision IN ('allow_once', 'allow_session', 'deny')),
|
||||
session_id TEXT,
|
||||
decided_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
expires_at TEXT
|
||||
);
|
||||
CREATE INDEX idx_approval_decisions_session ON approval_decisions(session_id);
|
||||
```
|
||||
|
||||
Session-based approval preferences for Tier 2 commands. Allows users to approve similar commands for the duration of a session.
|
||||
|
||||
---
|
||||
|
||||
## Key Design Notes
|
||||
|
||||
@ -28,29 +28,15 @@ Node **v22** required. Install via nvm or system package manager.
|
||||
npm install --legacy-peer-deps
|
||||
```
|
||||
|
||||
### kubectl Binary (for Shell Execution)
|
||||
|
||||
kubectl v1.30.0 is bundled with the application. To download binaries for development:
|
||||
|
||||
```bash
|
||||
./scripts/download-kubectl.sh linux amd64
|
||||
./scripts/download-kubectl.sh linux arm64
|
||||
./scripts/download-kubectl.sh darwin arm64
|
||||
./scripts/download-kubectl.sh darwin amd64
|
||||
./scripts/download-kubectl.sh windows amd64
|
||||
```
|
||||
|
||||
Binaries are placed in `binaries/kubectl-{os}-{arch}` and bundled via `tauri.conf.json` resources. See [[Shell-Execution]] for runtime usage details.
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Default | Purpose |
|
||||
|----------|---------|---------|
|
||||
| `TFTSR_DATA_DIR` | Platform data dir | Override DB location |
|
||||
| `TFTSR_DB_KEY` | _(none)_ | DB encryption key (required in release builds) |
|
||||
| `TFTSR_ENCRYPTION_KEY` | _(none)_ | Credential encryption key (required in release builds) |
|
||||
| `TRCAA_DATA_DIR` (or legacy `TRCAA_DATA_DIR`) | Platform data dir | Override DB location |
|
||||
| `TRCAA_DB_KEY` (or legacy `TRCAA_DB_KEY`) | _(none)_ | DB encryption key (required in release builds) |
|
||||
| `TRCAA_ENCRYPTION_KEY` (or legacy `TRCAA_ENCRYPTION_KEY`) | _(none)_ | Credential encryption key (required in release builds) |
|
||||
| `RUST_LOG` | `info` | Tracing verbosity: `debug`, `info`, `warn`, `error` |
|
||||
|
||||
Application data is stored at:
|
||||
@ -135,7 +121,7 @@ cargo tauri build
|
||||
# Outputs: .deb, .rpm, .AppImage (Linux)
|
||||
```
|
||||
|
||||
Release builds enforce secure key configuration. Set both `TFTSR_DB_KEY` and `TFTSR_ENCRYPTION_KEY` before building.
|
||||
Release builds enforce secure key configuration. Set both `TRCAA_DB_KEY` (or legacy `TRCAA_DB_KEY`) and `TRCAA_ENCRYPTION_KEY` (or legacy `TRCAA_ENCRYPTION_KEY`) before building.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -603,86 +603,6 @@ interface TicketResult {
|
||||
|
||||
---
|
||||
|
||||
## Shell Execution Commands
|
||||
|
||||
> **Status:** Fully Implemented (v1.0.0+)
|
||||
>
|
||||
> See [[Shell-Execution]] for complete documentation of the three-tier safety system.
|
||||
|
||||
### `upload_kubeconfig`
|
||||
```typescript
|
||||
uploadKubeconfigCmd(name: string, content: string) → string
|
||||
```
|
||||
Upload and encrypt a kubeconfig file. Returns the kubeconfig ID.
|
||||
|
||||
### `list_kubeconfigs`
|
||||
```typescript
|
||||
listKubeconfigsCmd() → KubeconfigInfo[]
|
||||
```
|
||||
List all uploaded kubeconfig files with metadata.
|
||||
```typescript
|
||||
interface KubeconfigInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
context: string;
|
||||
cluster_url?: string;
|
||||
is_active: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### `activate_kubeconfig`
|
||||
```typescript
|
||||
activateKubeconfigCmd(id: string) → void
|
||||
```
|
||||
Set a kubeconfig as active for kubectl commands.
|
||||
|
||||
### `delete_kubeconfig`
|
||||
```typescript
|
||||
deleteKubeconfigCmd(id: string) → void
|
||||
```
|
||||
Delete a kubeconfig file permanently.
|
||||
|
||||
### `respond_to_shell_approval`
|
||||
```typescript
|
||||
respondToShellApprovalCmd(approvalId: string, decision: string) → void
|
||||
```
|
||||
Respond to a Tier 2 command approval request.
|
||||
- `decision`: `"deny"`, `"allow_once"`, or `"allow_session"`
|
||||
|
||||
### `list_command_executions`
|
||||
```typescript
|
||||
listCommandExecutionsCmd(issueId?: string) → CommandExecution[]
|
||||
```
|
||||
List recent command executions, optionally filtered by issue ID.
|
||||
```typescript
|
||||
interface CommandExecution {
|
||||
id: string;
|
||||
command: string;
|
||||
tier: number; // 1, 2, or 3
|
||||
approval_status: string; // 'auto', 'approved', 'denied'
|
||||
exit_code?: number;
|
||||
stdout?: string;
|
||||
stderr?: string;
|
||||
execution_time_ms?: number;
|
||||
executed_at: string;
|
||||
}
|
||||
```
|
||||
|
||||
### `check_kubectl_installed`
|
||||
```typescript
|
||||
checkKubectlInstalledCmd() → KubectlStatus
|
||||
```
|
||||
Check if kubectl is installed and return version info.
|
||||
```typescript
|
||||
interface KubectlStatus {
|
||||
installed: boolean;
|
||||
path?: string;
|
||||
version?: string;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Authentication Storage
|
||||
|
||||
All integration credentials are stored in the `credentials` table:
|
||||
@ -700,7 +620,7 @@ CREATE TABLE credentials (
|
||||
|
||||
**Encryption:**
|
||||
- Algorithm: AES-256-GCM
|
||||
- Key derivation: From `TFTSR_DB_KEY` environment variable
|
||||
- Key derivation: From `TRCAA_DB_KEY` (or legacy `TRCAA_DB_KEY`) environment variable
|
||||
- Nonce: Random 96-bit per encryption
|
||||
- Format: `base64(nonce || ciphertext || tag)`
|
||||
|
||||
|
||||
@ -83,7 +83,7 @@ Password: (encrypted with AES-256-GCM)
|
||||
### Implementation Details
|
||||
- **API**: ServiceNow Table API (`/api/now/table/incident`)
|
||||
- **Auth**: HTTP Basic authentication
|
||||
- **Severity mapping**: TFTSR P1-P4 → ServiceNow urgency/impact (1-3)
|
||||
- **Severity mapping**: TRCAA P1-P4 → ServiceNow urgency/impact (1-3)
|
||||
- **Incident lookup**: Supports both sys_id (UUID) and incident number (INC0010001)
|
||||
- **TDD Tests**: 7 tests with mockito HTTP mocking
|
||||
|
||||
@ -152,7 +152,7 @@ All integrations using OAuth2 (Confluence, Azure DevOps) follow the same flow:
|
||||
|
||||
**Security:**
|
||||
- Tokens encrypted at rest with AES-256-GCM (256-bit key)
|
||||
- Key derived from environment variable `TFTSR_DB_KEY`
|
||||
- Key derived from environment variable `TRCAA_DB_KEY` (or legacy `TRCAA_DB_KEY`)
|
||||
- PKCE prevents authorization code interception
|
||||
- Callback server only accepts from `localhost`
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
# LiteLLM + AWS Bedrock Setup
|
||||
|
||||
This guide covers how to use **Claude via AWS Bedrock** with TFTSR through the LiteLLM proxy, providing an OpenAI-compatible API gateway.
|
||||
This guide covers how to use **Claude via AWS Bedrock** with TRCAA through the LiteLLM proxy, providing an OpenAI-compatible API gateway.
|
||||
|
||||
## Why LiteLLM + Bedrock?
|
||||
|
||||
@ -89,7 +89,7 @@ Expected response:
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Configure TFTSR
|
||||
### 4. Configure TRCAA
|
||||
|
||||
In **Settings → AI Providers → Add Provider**:
|
||||
|
||||
@ -182,7 +182,7 @@ curl -s http://localhost:8000/v1/chat/completions \
|
||||
-d '{"model": "bedrock-business", "messages": [{"role": "user", "content": "test"}]}'
|
||||
```
|
||||
|
||||
### 5. Configure in TFTSR
|
||||
### 5. Configure in TRCAA
|
||||
|
||||
Add both models as separate providers:
|
||||
|
||||
@ -232,7 +232,7 @@ model_list:
|
||||
aws_profile_name: ClaudeCodeLP # Same as Claude Code
|
||||
```
|
||||
|
||||
Now both Claude Code and TFTSR use the same Bedrock account without duplicate credential management.
|
||||
Now both Claude Code and TRCAA use the same Bedrock account without duplicate credential management.
|
||||
|
||||
---
|
||||
|
||||
@ -263,7 +263,7 @@ lsof -i :8000
|
||||
litellm --config ~/.litellm/config.yaml --port 8080
|
||||
```
|
||||
|
||||
Update the Base URL in TFTSR to match: `http://localhost:8080/v1`
|
||||
Update the Base URL in TRCAA to match: `http://localhost:8080/v1`
|
||||
|
||||
### AWS Credentials Not Found
|
||||
|
||||
@ -385,7 +385,7 @@ Pricing is identical, but Bedrock provides:
|
||||
1. **Master Key** — The `master_key` in config is required but doesn't need to be complex since LiteLLM runs locally
|
||||
2. **AWS Credentials** — Never commit `.aws/credentials` or credential process scripts to git
|
||||
3. **Local Only** — LiteLLM proxy should only bind to `127.0.0.1` (localhost) — never expose to network
|
||||
4. **Audit Logs** — TFTSR logs all AI requests with SHA-256 hashes in the audit table
|
||||
4. **Audit Logs** — TRCAA logs all AI requests with SHA-256 hashes in the audit table
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
## Overview
|
||||
|
||||
**Model Context Protocol (MCP)** is an open standard that allows AI models to invoke external tools and access external resources through a standardised JSON-RPC interface. TFTSR integrates MCP as a first-class feature, enabling the AI triage assistant to call tools exposed by any compliant MCP server — file search, database queries, monitoring APIs, runbook automation, and more.
|
||||
**Model Context Protocol (MCP)** is an open standard that allows AI models to invoke external tools and access external resources through a standardised JSON-RPC interface. TRCAA integrates MCP as a first-class feature, enabling the AI triage assistant to call tools exposed by any compliant MCP server — file search, database queries, monitoring APIs, runbook automation, and more.
|
||||
|
||||
MCP support extends the AI's capabilities beyond conversation: during incident triage, the model can autonomously invoke registered tools to gather diagnostic data, check system status, or execute remediation steps — all within the app's security and audit framework.
|
||||
|
||||
@ -12,7 +12,7 @@ MCP support extends the AI's capabilities beyond conversation: during incident t
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ TFTSR App │
|
||||
│ TRCAA App │
|
||||
│ │
|
||||
│ ┌────────┐ ┌──────────┐ ┌───────────┐ │
|
||||
│ │Frontend│──▶│ Commands │──▶│ Store │ │
|
||||
@ -127,7 +127,7 @@ The process is spawned via Tokio and wrapped with `rmcp::transport::TokioChildPr
|
||||
|
||||
#### Important: PATH for npx/node-based servers
|
||||
|
||||
When TFTSR spawns a stdio process from a macOS `.app` bundle, it runs in a **stripped environment** — the system `PATH` is not inherited. Any server that relies on `node`, `npx`, `python`, or other tools found via `PATH` must have it explicitly set.
|
||||
When TRCAA spawns a stdio process from a macOS `.app` bundle, it runs in a **stripped environment** — the system `PATH` is not inherited. Any server that relies on `node`, `npx`, `python`, or other tools found via `PATH` must have it explicitly set.
|
||||
|
||||
In the **Environment Variables (Plaintext)** field, add:
|
||||
|
||||
@ -208,7 +208,7 @@ Navigate to **Settings > MCP Servers** (`/settings/mcp`) to manage servers.
|
||||
- **Auth Value** — The token/key (will be encrypted on save). Leave blank for `none`.
|
||||
- **Environment Variables (Plaintext)** (stdio only) — Space-separated `KEY=value` pairs for non-sensitive values. **Always include `PATH=...` for `npx`/node/python-based servers** — the app bundle does not inherit the system PATH.
|
||||
- **Secure Environment Variables (Encrypted)** (stdio only) — Space-separated `KEY=value` pairs for sensitive values (API keys, tokens). Stored AES-256-GCM encrypted. Leave blank when editing to preserve existing values.
|
||||
- **Custom Headers** (HTTP only) — Space-separated `KEY:value` pairs for custom HTTP headers.
|
||||
- **Custom Headers** (HTTP only) — Not yet supported by the backend transport (currently ignored); do not use for secrets yet.
|
||||
- **Enabled** — Toggle on/off.
|
||||
3. Click **Save**. The server record is persisted.
|
||||
4. Click **Discover** to connect and enumerate available tools and resources.
|
||||
@ -293,7 +293,7 @@ See [IPC Commands](IPC-Commands#mcp-servers) for full type signatures.
|
||||
|
||||
## Security
|
||||
|
||||
- **Encrypted auth values** — AES-256-GCM, same key derivation as integration credentials (`TFTSR_ENCRYPTION_KEY`)
|
||||
- **Encrypted auth values** — AES-256-GCM, same key derivation as integration credentials (`TRCAA_ENCRYPTION_KEY` (or legacy `TRCAA_ENCRYPTION_KEY`))
|
||||
- **Server-side scrubbing** — `auth_value` set to `None` before any response to the frontend
|
||||
- **Audit logging** — `write_audit_event` called before every MCP tool execution
|
||||
- **PII scan** — Tool call arguments are scanned for PII patterns (non-blocking warning to user)
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
## Overview
|
||||
|
||||
Before any text is sent to an AI provider, TFTSR scans it for personally identifiable information (PII). Users must review and approve each detected span before the redacted text is transmitted.
|
||||
Before any text is sent to an AI provider, TRCAA scans it for personally identifiable information (PII). Users must review and approve each detected span before the redacted text is transmitted.
|
||||
|
||||
## Detection Flow
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
## Threat Model Summary
|
||||
|
||||
TFTSR handles sensitive IT incident data including log files that may contain credentials, PII, and internal infrastructure details. The security model addresses:
|
||||
TRCAA handles sensitive IT incident data including log files that may contain credentials, PII, and internal infrastructure details. The security model addresses:
|
||||
|
||||
1. **Data at rest** — Database encryption
|
||||
2. **Data in transit** — PII redaction before AI send, TLS for all outbound requests
|
||||
@ -19,22 +19,22 @@ Production builds use SQLCipher:
|
||||
- **KDF:** PBKDF2-HMAC-SHA512, 256,000 iterations
|
||||
- **HMAC:** HMAC-SHA512
|
||||
- **Page size:** 16384 bytes
|
||||
- **Key source:** `TFTSR_DB_KEY` environment variable
|
||||
- **Key source:** `TRCAA_DB_KEY` (or legacy `TRCAA_DB_KEY`) environment variable
|
||||
|
||||
Debug builds use plain SQLite (no encryption) for developer convenience.
|
||||
|
||||
Release builds now fail startup if `TFTSR_DB_KEY` is missing or empty.
|
||||
Release builds now fail startup if `TRCAA_DB_KEY` (or legacy `TRCAA_DB_KEY`) is missing or empty.
|
||||
|
||||
---
|
||||
|
||||
## Credential Encryption
|
||||
|
||||
Integration tokens are encrypted with AES-256-GCM before persistence:
|
||||
- **Key source:** `TFTSR_ENCRYPTION_KEY` (required in release builds)
|
||||
- **Key source:** `TRCAA_ENCRYPTION_KEY` (or legacy `TRCAA_ENCRYPTION_KEY`) (required in release builds)
|
||||
- **Key derivation:** SHA-256 hash of key material to a fixed 32-byte AES key
|
||||
- **Nonce:** Cryptographically secure random nonce per encryption
|
||||
|
||||
Release builds fail secure operations if `TFTSR_ENCRYPTION_KEY` is unset or empty.
|
||||
Release builds fail secure operations if `TRCAA_ENCRYPTION_KEY` (or legacy `TRCAA_ENCRYPTION_KEY`) is unset or empty.
|
||||
|
||||
The Stronghold plugin remains enabled and now uses a per-installation salt derived from the app data directory path hash instead of a fixed static salt.
|
||||
|
||||
@ -136,7 +136,7 @@ MCP server support introduces external tool execution capabilities. The followin
|
||||
### Auth Value Storage
|
||||
|
||||
- Auth tokens (API keys, bearer tokens, OAuth2 access tokens) are encrypted with **AES-256-GCM** before persistence in `mcp_servers.auth_value`.
|
||||
- Encryption uses the same key derivation as integration credentials (`TFTSR_ENCRYPTION_KEY` → SHA-256 → 32-byte AES key).
|
||||
- Encryption uses the same key derivation as integration credentials (`TRCAA_ENCRYPTION_KEY` (or legacy `TRCAA_ENCRYPTION_KEY`) → SHA-256 → 32-byte AES key).
|
||||
- Random 96-bit nonce per encryption operation.
|
||||
- Format: `base64(nonce || ciphertext || tag)`.
|
||||
|
||||
|
||||
@ -277,7 +277,7 @@ Navigate to **Settings → Shell Execution** to view recent command executions:
|
||||
|
||||
### Encryption
|
||||
- **Kubeconfig Files**: AES-256-GCM encryption at rest
|
||||
- **Encryption Key**: Derived from `TFTSR_ENCRYPTION_KEY` environment variable
|
||||
- **Encryption Key**: Derived from `TRCAA_ENCRYPTION_KEY` (or legacy `TRCAA_ENCRYPTION_KEY`) environment variable
|
||||
- **Nonce**: Random 12-byte nonce per encryption operation
|
||||
- **Authentication Tag**: 16-byte tag for integrity verification
|
||||
|
||||
|
||||
@ -175,7 +175,7 @@ sudo apt-get install -y libwebkit2gtk-4.1-dev libssl-dev libgtk-3-dev \
|
||||
|
||||
**Symptom:** App fails to start with SQLCipher error.
|
||||
|
||||
1. `TFTSR_DB_KEY` env var is set
|
||||
1. `TRCAA_DB_KEY` (or legacy `TRCAA_DB_KEY`) env var is set
|
||||
2. Key matches what was used when DB was created
|
||||
3. File isn't corrupted: `file tftsr.db` should say `SQLite 3.x database`
|
||||
|
||||
|
||||
2828
package-lock.json
generated
58
package.json
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "tftsr",
|
||||
"name": "trcaa",
|
||||
"private": true,
|
||||
"version": "1.0.8",
|
||||
"type": "module",
|
||||
@ -15,46 +15,46 @@
|
||||
"test:e2e": "wdio run tests/e2e/wdio.conf.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.11.0",
|
||||
"@tauri-apps/api": "^2",
|
||||
"@tauri-apps/plugin-dialog": "^2.7.1",
|
||||
"@tauri-apps/plugin-fs": "^2.5.1",
|
||||
"@tauri-apps/plugin-stronghold": "^2.3.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.577.0",
|
||||
"react": "^18.3.1",
|
||||
"react-diff-viewer-continued": "^3.4.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-markdown": "^9.1.0",
|
||||
"react-router-dom": "^6.30.3",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"tailwindcss": "^3.4.19",
|
||||
"zustand": "^4.5.7"
|
||||
"@tauri-apps/plugin-fs": "^2",
|
||||
"@tauri-apps/plugin-stronghold": "^2",
|
||||
"class-variance-authority": "^0.7",
|
||||
"clsx": "^2",
|
||||
"lucide-react": "latest",
|
||||
"react": "^18",
|
||||
"react-diff-viewer-continued": "^3",
|
||||
"react-dom": "^18",
|
||||
"react-markdown": "^9",
|
||||
"react-router-dom": "^6",
|
||||
"remark-gfm": "^4",
|
||||
"tailwindcss": "^3",
|
||||
"zustand": "^4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.11.0",
|
||||
"@tauri-apps/cli": "^2",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16",
|
||||
"@testing-library/user-event": "^14",
|
||||
"@types/react": "^18.3.28",
|
||||
"@types/react-dom": "^18.3.7",
|
||||
"@types/testing-library__react": "^10",
|
||||
"@types/node": "^25.9.1",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"@typescript-eslint/eslint-plugin": "^8.58.1",
|
||||
"@typescript-eslint/parser": "^8.58.1",
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"@vitest/coverage-v8": "^2.1.9",
|
||||
"@wdio/cli": "^9.25.0",
|
||||
"@wdio/mocha-framework": "^9.25.0",
|
||||
"autoprefixer": "^10.4.27",
|
||||
"@vitest/coverage-v8": "^4",
|
||||
"@wdio/cli": "^9",
|
||||
"@wdio/mocha-framework": "^9",
|
||||
"autoprefixer": "^10",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"jsdom": "^26.1.0",
|
||||
"postcss": "^8.5.8",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^6.4.1",
|
||||
"vitest": "^2.1.9",
|
||||
"webdriverio": "^9.25.0"
|
||||
"jsdom": "^26",
|
||||
"postcss": "^8",
|
||||
"typescript": "^5",
|
||||
"vite": "^6",
|
||||
"vitest": "^4",
|
||||
"webdriverio": "^9"
|
||||
}
|
||||
}
|
||||
|
||||
2
src-tauri/Cargo.lock
generated
@ -6089,7 +6089,7 @@ dependencies = [
|
||||
"mockito",
|
||||
"printpdf",
|
||||
"quick-xml 0.36.2",
|
||||
"rand 0.8.6",
|
||||
"rand 0.9.4",
|
||||
"regex",
|
||||
"reqwest 0.12.28",
|
||||
"rmcp",
|
||||
|
||||
@ -4,7 +4,7 @@ version = "1.0.8"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
name = "tftsr_lib"
|
||||
name = "trcaa_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
@ -39,7 +39,7 @@ async-trait = "0.1"
|
||||
base64 = "0.22"
|
||||
dirs = "5"
|
||||
aes-gcm = "0.10"
|
||||
rand = "0.8"
|
||||
rand = "0.9"
|
||||
lazy_static = "1.4"
|
||||
warp = "0.3"
|
||||
urlencoding = "2"
|
||||
@ -69,3 +69,6 @@ strip = true
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "../node_modules/@tauri-apps/cli/schema/acl-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Default capabilities for TFTSR — least-privilege",
|
||||
"description": "Default capabilities for TRCAA — least-privilege",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:path:default",
|
||||
|
||||
@ -1 +1 @@
|
||||
{"default":{"identifier":"default","description":"Default capabilities for TFTSR — least-privilege","local":true,"windows":["main"],"permissions":["core:path:default","core:event:default","core:window:default","core:app:default","core:resources:default","core:menu:default","core:tray:default","dialog:allow-open","dialog:allow-save","fs:allow-read-text-file","fs:allow-write-text-file","fs:allow-mkdir","fs:allow-app-read-recursive","fs:allow-app-write-recursive","fs:allow-temp-read-recursive","fs:allow-temp-write-recursive","fs:scope-app-recursive","fs:scope-temp-recursive","shell:allow-open","http:default"]}}
|
||||
{"default":{"identifier":"default","description":"Default capabilities for TRCAA — least-privilege","local":true,"windows":["main"],"permissions":["core:path:default","core:event:default","core:window:default","core:app:default","core:resources:default","core:menu:default","core:tray:default","dialog:allow-open","dialog:allow-save","fs:allow-read-text-file","fs:allow-write-text-file","fs:allow-mkdir","fs:allow-app-read-recursive","fs:allow-app-write-recursive","fs:allow-temp-read-recursive","fs:allow-temp-write-recursive","fs:scope-app-recursive","fs:scope-temp-recursive","shell:allow-open","http:default"]}}
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 31 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.6 KiB |
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
</adaptive-icon>
|
||||
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#fff</color>
|
||||
</resources>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 129 KiB After Width: | Height: | Size: 124 KiB |
@ -1,5 +1,39 @@
|
||||
You are a senior DevOps incident responder with expertise in managing critical production incidents, performing rapid diagnostics, and implementing permanent fixes. Your focus spans incident detection, response coordination, root cause analysis, and continuous improvement with emphasis on reducing MTTR and building resilient systems.
|
||||
|
||||
**IMPORTANT: You have direct access to execute shell commands via the execute_shell_command tool. Use this tool proactively to gather diagnostic information, check system state, and investigate incidents rather than suggesting manual commands. The system automatically classifies commands for safety:**
|
||||
- **Read-only commands** (ls, cat, grep, df, ps, kubectl get, systemctl status, journalctl) execute immediately without approval
|
||||
- **Mutating commands** (systemctl restart, kubectl apply/delete, rm, chmod) will prompt the user for approval before execution
|
||||
- **Always prefer executing commands over suggesting manual steps** — this is your primary incident response interface
|
||||
- **Tool calling format**: ONLY when you need to invoke a tool (like execute_shell_command), use the native JSON function calling format provided by the API. Never output XML-style tags like `<execute_shell_command>`. When invoking tools, the system expects a structured `tool_calls` field in your response.
|
||||
- **User responses**: Always respond to users in natural language (plain text/markdown). Your text responses to users must NEVER be formatted as JSON. Do NOT wrap your explanations, findings, or answers in JSON objects or structured data formats.
|
||||
- **CRITICAL: Never echo tool call requests or responses in your user-facing output.** When you invoke execute_shell_command, DO NOT show the JSON request payload to the user. After receiving the tool result, present ONLY the meaningful output in natural language or formatted results. Exception: If the user explicitly requests to see raw API format or JSON payloads for debugging purposes, you may show them.
|
||||
|
||||
**CRITICAL: Query Classification - Match Investigation Depth to User Request:**
|
||||
|
||||
Before executing ANY commands, classify the user's query into one of these categories:
|
||||
|
||||
1. **Simple Information Query** (1-2 commands maximum)
|
||||
- Examples: "What pods are running?", "Show me the services", "List deployments"
|
||||
- Response: Execute ONLY the minimum command needed, return the raw output, STOP
|
||||
- DO NOT investigate further unless the user explicitly asks
|
||||
- DO NOT check logs, events, or YAML unless specifically requested
|
||||
|
||||
2. **Diagnostic Investigation** (3-8 commands)
|
||||
- Examples: "Why is this pod failing?", "What's wrong with deployment X?", "Check pod health", "Investigate telemetry issues"
|
||||
- Response: Execute targeted diagnostic commands (status, logs, events), analyze, report findings
|
||||
- **CRITICAL: Actually execute the diagnostic commands via execute_shell_command tool**
|
||||
- DO NOT output structured status responses with agent names and status fields - that is strictly forbidden
|
||||
- USE THE TOOLS. Run kubectl get/describe/logs commands to gather real data, THEN analyze and report
|
||||
- Stop after identifying the issue or confirming health
|
||||
|
||||
3. **Active Incident Response** (8-20 commands)
|
||||
- Examples: "Production is down", "Service outage", "Critical alert firing"
|
||||
- Response: Full diagnostic suite, root cause analysis, proposed remediation
|
||||
- Only use this depth for actual incidents
|
||||
|
||||
**If you execute more than 2 commands for a simple query, you are doing it wrong. STOP and answer the user's question with what you have.**
|
||||
|
||||
**WARNING: Outputting status JSON objects instead of executing commands is a critical failure. When a query requires investigation, you MUST use execute_shell_command to gather actual diagnostic data, not just report that you're "investigating".**
|
||||
|
||||
When invoked:
|
||||
1. Query context manager for system architecture and incident history
|
||||
@ -121,18 +155,7 @@ Tool mastery:
|
||||
|
||||
### Incident Assessment
|
||||
|
||||
Initialize incident response by understanding system state.
|
||||
|
||||
Incident context query:
|
||||
```json
|
||||
{
|
||||
"requesting_agent": "devops-incident-responder",
|
||||
"request_type": "get_incident_context",
|
||||
"payload": {
|
||||
"query": "Incident context needed: system architecture, current alerts, recent changes, monitoring coverage, team structure, and historical incidents."
|
||||
}
|
||||
}
|
||||
```
|
||||
Initialize incident response by understanding system state through direct investigation using execute_shell_command.
|
||||
|
||||
## Development Workflow
|
||||
|
||||
@ -186,19 +209,7 @@ Response patterns:
|
||||
- Learn continuously
|
||||
- Prevent recurrence
|
||||
|
||||
Progress tracking:
|
||||
```json
|
||||
{
|
||||
"agent": "devops-incident-responder",
|
||||
"status": "improving",
|
||||
"progress": {
|
||||
"mttr": "28min",
|
||||
"runbook_coverage": "85%",
|
||||
"auto_remediation": "42%",
|
||||
"team_confidence": "4.3/5"
|
||||
}
|
||||
}
|
||||
```
|
||||
Progress should be communicated to users in clear, natural language summarizing metrics and improvements.
|
||||
|
||||
### 3. Response Excellence
|
||||
|
||||
|
||||
@ -39,7 +39,7 @@ pub struct ChatResponse {
|
||||
}
|
||||
|
||||
/// Represents a tool call made by the AI
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct ToolCall {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
|
||||
@ -7,8 +7,8 @@ use crate::state::ProviderConfig;
|
||||
|
||||
pub struct OpenAiProvider;
|
||||
|
||||
fn is_msi_genai_format(api_format: Option<&str>) -> bool {
|
||||
matches!(api_format, Some("msi-genai") | Some("custom_rest")) // custom_rest for backward compatibility
|
||||
fn is_generic_genai_format(api_format: Option<&str>) -> bool {
|
||||
matches!(api_format, Some("generic-genai") | Some("custom_rest")) // custom_rest for backward compatibility
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@ -35,11 +35,11 @@ impl Provider for OpenAiProvider {
|
||||
config: &ProviderConfig,
|
||||
tools: Option<Vec<crate::ai::Tool>>,
|
||||
) -> anyhow::Result<ChatResponse> {
|
||||
// Check if using custom REST format
|
||||
// Check if using GenAI format (or legacy custom_rest)
|
||||
let api_format = config.api_format.as_deref().unwrap_or("openai");
|
||||
|
||||
if is_msi_genai_format(Some(api_format)) {
|
||||
self.chat_msi_genai(messages, config, tools).await
|
||||
if is_generic_genai_format(Some(api_format)) {
|
||||
self.chat_generic_genai(messages, config, tools).await
|
||||
} else {
|
||||
self.chat_openai(messages, config, tools).await
|
||||
}
|
||||
@ -48,28 +48,28 @@ impl Provider for OpenAiProvider {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{is_msi_genai_format, OpenAiProvider};
|
||||
use super::{is_generic_genai_format, OpenAiProvider};
|
||||
|
||||
#[test]
|
||||
fn msi_genai_format_is_recognized() {
|
||||
assert!(is_msi_genai_format(Some("msi-genai")));
|
||||
fn generic_genai_format_is_recognized() {
|
||||
assert!(is_generic_genai_format(Some("generic-genai")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_rest_format_backward_compatible() {
|
||||
// Keep backward compatibility with old format name
|
||||
assert!(is_msi_genai_format(Some("custom_rest")));
|
||||
assert!(is_generic_genai_format(Some("custom_rest")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_format_is_not_msi_genai() {
|
||||
assert!(!is_msi_genai_format(Some("openai")));
|
||||
assert!(!is_msi_genai_format(None));
|
||||
fn openai_format_is_not_generic_genai() {
|
||||
assert!(!is_generic_genai_format(Some("openai")));
|
||||
assert!(!is_generic_genai_format(None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_msigenai_chatgpt_tool_calls_from_json_text() {
|
||||
// MSIGenAI ChatGPT format: returns tool calls as JSON object in msg
|
||||
fn parse_genericai_chatgpt_tool_calls_from_json_text() {
|
||||
// GenAI ChatGPT format: returns tool calls as JSON object in msg
|
||||
let content = r#"{"tool_calls":[{"id":"call_1","type":"function","function":{"name":"execute_shell_command","arguments":{"command":"kubectl get namespaces"}}}]}"#;
|
||||
|
||||
let result = OpenAiProvider::parse_tool_calls_from_text(content);
|
||||
@ -83,8 +83,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_msigenai_claude_tool_calls_from_xml_wrapper() {
|
||||
// MSIGenAI Claude format: XML wrapper around JSON array
|
||||
fn parse_genericai_claude_tool_calls_from_xml_wrapper() {
|
||||
// GenAI Claude format: XML wrapper around JSON array
|
||||
let content = r#"<tool_calls>
|
||||
[{"id":"call_1","type":"function","function":{"name":"execute_shell_command","arguments":{"command":"kubectl get pods"}}}]
|
||||
</tool_calls>"#;
|
||||
@ -294,13 +294,13 @@ impl OpenAiProvider {
|
||||
})
|
||||
}
|
||||
|
||||
/// MSI GenAI format (non-OpenAI payload contract)
|
||||
/// GenAI format (non-OpenAI payload contract)
|
||||
///
|
||||
/// MSI GenAI uses a custom API format with 'prompt' field instead of 'messages',
|
||||
/// GenAI uses a custom API format with 'prompt' field instead of 'messages',
|
||||
/// and has a known bug where tool calls are returned as JSON text in the 'msg'
|
||||
/// field instead of structured 'tool_calls' array. This implementation includes
|
||||
/// workaround parsing to extract tool calls from text.
|
||||
async fn chat_msi_genai(
|
||||
async fn chat_generic_genai(
|
||||
&self,
|
||||
messages: Vec<Message>,
|
||||
config: &ProviderConfig,
|
||||
@ -381,7 +381,7 @@ impl OpenAiProvider {
|
||||
body["tools"] = serde_json::Value::from(formatted_tools);
|
||||
body["tool_choice"] = serde_json::Value::from("auto");
|
||||
|
||||
tracing::info!("MSI GenAI: Sending {} tools in request", tool_count);
|
||||
tracing::info!("GenAI: Sending {} tools in request", tool_count);
|
||||
}
|
||||
|
||||
// Use custom auth header and prefix (no default prefix for custom REST)
|
||||
@ -403,13 +403,13 @@ impl OpenAiProvider {
|
||||
if !resp.status().is_success() {
|
||||
let status = resp.status();
|
||||
let text = resp.text().await?;
|
||||
anyhow::bail!("MSI GenAI API error {status}: {text}");
|
||||
anyhow::bail!("GenAI API error {status}: {text}");
|
||||
}
|
||||
|
||||
let json: serde_json::Value = resp.json().await?;
|
||||
|
||||
tracing::debug!(
|
||||
"MSI GenAI response: {}",
|
||||
"GenAI response: {}",
|
||||
serde_json::to_string_pretty(&json).unwrap_or_else(|_| "invalid JSON".to_string())
|
||||
);
|
||||
|
||||
@ -438,7 +438,7 @@ impl OpenAiProvider {
|
||||
.and_then(|n| n.as_str())
|
||||
.or_else(|| call.get("name").and_then(|n| n.as_str())),
|
||||
) {
|
||||
// Accept arguments as either string or object (MSI GenAI returns both)
|
||||
// Accept arguments as either string or object (GenAI returns both)
|
||||
let arguments = call
|
||||
.get("function")
|
||||
.and_then(|f| f.get("arguments"))
|
||||
@ -453,11 +453,7 @@ impl OpenAiProvider {
|
||||
});
|
||||
|
||||
if let Some(args) = arguments {
|
||||
tracing::info!(
|
||||
"MSI GenAI: Parsed tool call: {} ({})",
|
||||
name,
|
||||
id
|
||||
);
|
||||
tracing::info!("GenAI: Parsed tool call: {} ({})", name, id);
|
||||
return Some(crate::ai::ToolCall {
|
||||
id: id.to_string(),
|
||||
name: name.to_string(),
|
||||
@ -486,7 +482,7 @@ impl OpenAiProvider {
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| format!("tool_call_{index}"));
|
||||
tracing::info!(
|
||||
"MSI GenAI: Parsed tool call (simple format): {} ({})",
|
||||
"GenAI: Parsed tool call (simple format): {} ({})",
|
||||
name,
|
||||
id
|
||||
);
|
||||
@ -498,14 +494,14 @@ impl OpenAiProvider {
|
||||
}
|
||||
}
|
||||
|
||||
tracing::warn!("MSI GenAI: Failed to parse tool call: {:?}", call);
|
||||
tracing::warn!("GenAI: Failed to parse tool call: {:?}", call);
|
||||
None
|
||||
})
|
||||
.collect();
|
||||
if calls.is_empty() {
|
||||
None
|
||||
} else {
|
||||
tracing::info!("MSI GenAI: Found {} tool calls", calls.len());
|
||||
tracing::info!("GenAI: Found {} tool calls", calls.len());
|
||||
Some(calls)
|
||||
}
|
||||
} else {
|
||||
@ -513,14 +509,14 @@ impl OpenAiProvider {
|
||||
}
|
||||
});
|
||||
|
||||
// WORKAROUND: MSIGenAI gateway bug - tool calls returned as JSON text in 'msg' field
|
||||
// WORKAROUND: GenAI gateway bug - tool calls returned as JSON text in 'msg' field
|
||||
// Expected: {"tool_calls": [...]}
|
||||
// Actual: {"msg": '{"tool_calls":[...]}'} or {"msg": '<tool_calls>[...]</tool_calls>'}
|
||||
if tool_calls.is_none() {
|
||||
// Try parsing tool calls from msg content (MSIGenAI workaround)
|
||||
// Try parsing tool calls from msg content (GenAI workaround)
|
||||
if let Some(parsed_calls) = Self::parse_tool_calls_from_text(&content) {
|
||||
tracing::warn!(
|
||||
"MSI GenAI: MSIGenAI workaround - parsed {} tool calls from msg text (gateway should return structured tool_calls field)",
|
||||
"GenAI: GenAI workaround - parsed {} tool calls from msg text (gateway should return structured tool_calls field)",
|
||||
parsed_calls.len()
|
||||
);
|
||||
tool_calls = Some(parsed_calls);
|
||||
@ -541,9 +537,9 @@ impl OpenAiProvider {
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse tool calls from text content (MSIGenAI gateway workaround)
|
||||
/// Parse tool calls from text content (GenAI gateway workaround)
|
||||
///
|
||||
/// MSIGenAI returns tool calls as JSON text in the 'msg' field instead of structured data:
|
||||
/// GenAI returns tool calls as JSON text in the 'msg' field instead of structured data:
|
||||
/// - ChatGPT models: `{"tool_calls":[...]}`
|
||||
/// - Claude models: `<tool_calls>[...]</tool_calls>`
|
||||
fn parse_tool_calls_from_text(content: &str) -> Option<Vec<crate::ai::ToolCall>> {
|
||||
|
||||
@ -3,7 +3,7 @@ use std::collections::HashMap;
|
||||
|
||||
/// Get all statically-registered tools for AI function calling.
|
||||
pub fn get_available_tools() -> Vec<Tool> {
|
||||
vec![get_add_ado_comment_tool()]
|
||||
vec![get_add_ado_comment_tool(), get_execute_shell_command_tool()]
|
||||
}
|
||||
|
||||
/// Fetch tools from all connected, enabled MCP servers.
|
||||
@ -46,3 +46,45 @@ fn get_add_ado_comment_tool() -> Tool {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Tool definition for executing shell commands with safety classification
|
||||
fn get_execute_shell_command_tool() -> Tool {
|
||||
let mut properties = HashMap::new();
|
||||
|
||||
properties.insert(
|
||||
"command".to_string(),
|
||||
ParameterProperty {
|
||||
prop_type: "string".to_string(),
|
||||
description: "Shell command to execute. Supports kubectl, pvesh, qm, and general shell commands. Read-only commands execute automatically. Mutating commands require user approval.".to_string(),
|
||||
enum_values: None,
|
||||
},
|
||||
);
|
||||
|
||||
properties.insert(
|
||||
"working_directory".to_string(),
|
||||
ParameterProperty {
|
||||
prop_type: "string".to_string(),
|
||||
description: "Optional working directory for command execution".to_string(),
|
||||
enum_values: None,
|
||||
},
|
||||
);
|
||||
|
||||
properties.insert(
|
||||
"kubeconfig_id".to_string(),
|
||||
ParameterProperty {
|
||||
prop_type: "string".to_string(),
|
||||
description: "Optional kubeconfig file ID for kubectl commands".to_string(),
|
||||
enum_values: None,
|
||||
},
|
||||
);
|
||||
|
||||
Tool {
|
||||
name: "execute_shell_command".to_string(),
|
||||
description: "Execute shell commands with automatic safety classification. Tier 1 (read-only): kubectl get/describe/logs, cat, grep, ls - execute automatically. Tier 2 (mutating): kubectl apply/delete/scale, chmod, systemctl restart - require user approval. Tier 3 (destructive): rm -rf, shutdown, mkfs - always denied.".to_string(),
|
||||
parameters: ToolParameters {
|
||||
param_type: "object".to_string(),
|
||||
properties,
|
||||
required: vec!["command".to_string()],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -161,6 +161,24 @@ fn extract_list(text: &str, header: &str) -> Vec<String> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Sanitize messages for final call when tool iteration limit is reached.
|
||||
/// Converts tool role messages to assistant role with clear labeling as untrusted data.
|
||||
fn sanitize_messages_for_final_call(messages: Vec<Message>) -> Vec<Message> {
|
||||
messages
|
||||
.into_iter()
|
||||
.map(|mut msg| {
|
||||
if msg.role == "tool" {
|
||||
// Convert tool output to assistant role with clear labeling as untrusted data
|
||||
msg.role = "assistant".into();
|
||||
msg.content = format!("[UNTRUSTED TOOL OUTPUT]: {}", msg.content);
|
||||
msg.tool_call_id = None;
|
||||
}
|
||||
msg.tool_calls = None; // Strip tool_calls from all messages
|
||||
msg
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn chat_message(
|
||||
issue_id: String,
|
||||
@ -379,6 +397,63 @@ pub async fn chat_message(
|
||||
messages.push(context_message);
|
||||
}
|
||||
|
||||
// Tool execution configuration
|
||||
const MAX_TOOL_ITERATIONS: usize = 20; // Allow sufficient iterations for complex diagnostics
|
||||
|
||||
// Get available tools — static + MCP
|
||||
// Only enable tools if the provider explicitly supports tool calling
|
||||
let tools = if provider_config.supports_tool_calling.unwrap_or(false) {
|
||||
let mut all_tools = crate::ai::tools::get_available_tools();
|
||||
let mcp_tools = crate::ai::tools::get_enabled_mcp_tools(&state).await;
|
||||
all_tools.extend(mcp_tools);
|
||||
if all_tools.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(all_tools)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// If tools are available AND using OpenAI-compatible provider, add explicit JSON format instruction
|
||||
// Only OpenAI-compatible providers (default case in create_provider) actually support tool calling.
|
||||
// Others (anthropic, gemini, mistral, ollama) either ignore tools or use provider-specific formats.
|
||||
let is_openai_compatible = {
|
||||
let kind = if provider_config.provider_type.is_empty() {
|
||||
provider_config.name.as_str()
|
||||
} else {
|
||||
provider_config.provider_type.as_str()
|
||||
};
|
||||
!matches!(kind, "anthropic" | "gemini" | "mistral" | "ollama")
|
||||
};
|
||||
|
||||
if tools.is_some() && is_openai_compatible {
|
||||
messages.push(Message {
|
||||
role: "system".into(),
|
||||
content: "CRITICAL: You have tools available. When calling tools, you MUST use the native JSON function calling format in your API response. DO NOT output XML tags like <tool_name>. DO NOT output text descriptions of tool calls. Use the structured tool_calls field in your response.".into(),
|
||||
tool_call_id: None,
|
||||
tool_calls: None,
|
||||
});
|
||||
|
||||
// Add iteration budget awareness
|
||||
messages.push(Message {
|
||||
role: "system".into(),
|
||||
content: format!(
|
||||
"TOOL EXECUTION BUDGET: You have a maximum of {MAX_TOOL_ITERATIONS} rounds (each AI response counts as one round). \
|
||||
You can call multiple tools in a single round. \
|
||||
Plan your investigation efficiently:\n\
|
||||
- Call multiple related tools in the same round when possible\n\
|
||||
- Prioritize high-value diagnostic commands first\n\
|
||||
- Use comprehensive output formats (e.g., kubectl --output=yaml) to gather more data per call\n\
|
||||
- Reserve 1 round for your final summary/answer\n\
|
||||
- If you exceed the budget, you'll be cut off mid-investigation\n\
|
||||
Current round count is not visible to you, so plan conservatively."
|
||||
),
|
||||
tool_call_id: None,
|
||||
tool_calls: None,
|
||||
});
|
||||
}
|
||||
|
||||
messages.push(Message {
|
||||
role: "user".into(),
|
||||
content: full_message.clone(),
|
||||
@ -386,25 +461,63 @@ pub async fn chat_message(
|
||||
tool_calls: None,
|
||||
});
|
||||
|
||||
// Get available tools — static + MCP
|
||||
let mut all_tools = crate::ai::tools::get_available_tools();
|
||||
let mcp_tools = crate::ai::tools::get_enabled_mcp_tools(&state).await;
|
||||
all_tools.extend(mcp_tools);
|
||||
let tools = if all_tools.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(all_tools)
|
||||
};
|
||||
|
||||
// Tool-calling loop: keep calling until AI gives final answer
|
||||
let final_response;
|
||||
let max_iterations = 10; // Prevent infinite loops
|
||||
let mut iteration = 0;
|
||||
|
||||
loop {
|
||||
iteration += 1;
|
||||
if iteration > max_iterations {
|
||||
return Err("Tool-calling loop exceeded maximum iterations".to_string());
|
||||
|
||||
// Warn AI when approaching limit
|
||||
if iteration == MAX_TOOL_ITERATIONS - 2 {
|
||||
messages.push(Message {
|
||||
role: "system".into(),
|
||||
content: format!(
|
||||
"WARNING: You are on iteration {iteration}/{MAX_TOOL_ITERATIONS} (2 rounds remaining). \
|
||||
You MUST provide your final answer in the NEXT round. \
|
||||
Do NOT call any more tools. \
|
||||
Summarize your findings based on the data you've already gathered."
|
||||
),
|
||||
tool_call_id: None,
|
||||
tool_calls: None,
|
||||
});
|
||||
}
|
||||
|
||||
// Force stop at limit with collected data
|
||||
if iteration > MAX_TOOL_ITERATIONS {
|
||||
let sanitized_messages = sanitize_messages_for_final_call(messages);
|
||||
|
||||
// Add final instruction
|
||||
let mut final_messages = sanitized_messages;
|
||||
final_messages.push(Message {
|
||||
role: "system".into(),
|
||||
content: format!(
|
||||
"CRITICAL: Tool iteration limit reached ({iteration}/{MAX_TOOL_ITERATIONS}). \
|
||||
TOOLS ARE NOW DISABLED. \
|
||||
You MUST respond now with a natural language summary of your findings. \
|
||||
DO NOT attempt to call any tools - they will not execute. \
|
||||
DO NOT emit tool_calls JSON - it will be ignored. \
|
||||
Ignore any earlier instructions about tool calling or JSON formatting. \
|
||||
Provide your best answer in plain text based on the diagnostic data already collected."
|
||||
),
|
||||
tool_call_id: None,
|
||||
tool_calls: None,
|
||||
});
|
||||
|
||||
// Make one final call WITHOUT tools to force text response
|
||||
let final_attempt = provider
|
||||
.chat(final_messages, &provider_config, None) // No tools available
|
||||
.await
|
||||
.map_err(|e| {
|
||||
format!("AI provider request failed after reaching iteration limit: {e}")
|
||||
})?;
|
||||
|
||||
final_response = final_attempt;
|
||||
tracing::warn!(
|
||||
"Tool iteration limit exceeded, forced final response: {} chars",
|
||||
final_response.content.len()
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
let response = provider
|
||||
@ -1065,15 +1178,80 @@ async fn execute_tool_call(
|
||||
)
|
||||
.await
|
||||
}
|
||||
"execute_shell_command" => execute_shell_tool_call(tool_call, app_handle, app_state).await,
|
||||
name if name.starts_with("mcp_") => execute_mcp_tool_call(tool_call, app_state).await,
|
||||
_ => {
|
||||
let error = format!("Unknown tool: {}", tool_call.name);
|
||||
tracing::warn!("{}", error);
|
||||
tracing::warn!("{error}");
|
||||
Err(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn execute_shell_tool_call(
|
||||
tool_call: &crate::ai::ToolCall,
|
||||
app_handle: &tauri::AppHandle,
|
||||
app_state: &State<'_, AppState>,
|
||||
) -> Result<String, String> {
|
||||
// Parse arguments
|
||||
let args: serde_json::Value = serde_json::from_str(&tool_call.arguments)
|
||||
.map_err(|e| format!("Failed to parse tool arguments: {e}"))?;
|
||||
|
||||
let command = args
|
||||
.get("command")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| "Missing command parameter".to_string())?;
|
||||
|
||||
let working_directory = args.get("working_directory").and_then(|v| v.as_str());
|
||||
let kubeconfig_id = args.get("kubeconfig_id").and_then(|v| v.as_str());
|
||||
|
||||
// PII detection
|
||||
{
|
||||
let detector = crate::pii::detector::PiiDetector::new();
|
||||
let spans = detector.detect(command);
|
||||
if !spans.is_empty() {
|
||||
tracing::warn!(
|
||||
tool = %tool_call.name,
|
||||
pii_spans = spans.len(),
|
||||
"PII detected in shell command"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Audit log
|
||||
{
|
||||
let db = app_state.db.lock().map_err(|e| e.to_string())?;
|
||||
let details = serde_json::json!({
|
||||
"tool": tool_call.name,
|
||||
"command": command,
|
||||
});
|
||||
crate::audit::log::write_audit_event(
|
||||
&db,
|
||||
"shell_tool_call",
|
||||
"shell_command",
|
||||
command,
|
||||
&details.to_string(),
|
||||
)
|
||||
.map_err(|e| format!("Audit log failed: {e}"))?;
|
||||
}
|
||||
|
||||
// Execute with approval flow
|
||||
let result = crate::shell::executor::execute_with_approval(
|
||||
command,
|
||||
app_handle,
|
||||
app_state.inner(),
|
||||
kubeconfig_id,
|
||||
working_directory,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Format output for AI
|
||||
Ok(format!(
|
||||
"Exit Code: {}\n\nStdout:\n{}\n\nStderr:\n{}",
|
||||
result.exit_code, result.stdout, result.stderr
|
||||
))
|
||||
}
|
||||
|
||||
async fn execute_mcp_tool_call(
|
||||
tool_call: &crate::ai::ToolCall,
|
||||
app_state: &State<'_, AppState>,
|
||||
@ -1400,4 +1578,309 @@ mod tests {
|
||||
assert_eq!(raw[0].1, "First");
|
||||
assert_eq!(raw[1].1, "Second");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_message_sanitization_converts_tool_role_to_assistant() {
|
||||
// Messages with 'tool' role that would cause validation errors
|
||||
let messages = vec![
|
||||
Message {
|
||||
role: "user".into(),
|
||||
content: "What pods are running?".into(),
|
||||
tool_call_id: None,
|
||||
tool_calls: None,
|
||||
},
|
||||
Message {
|
||||
role: "assistant".into(),
|
||||
content: "".into(),
|
||||
tool_call_id: None,
|
||||
tool_calls: Some(vec![crate::ai::ToolCall {
|
||||
id: "call_123".into(),
|
||||
name: "execute_shell_command".into(),
|
||||
arguments: r#"{"command":"kubectl get pods"}"#.into(),
|
||||
}]),
|
||||
},
|
||||
Message {
|
||||
role: "tool".into(),
|
||||
content: "pod1 Running\npod2 Running".into(),
|
||||
tool_call_id: Some("call_123".into()),
|
||||
tool_calls: None,
|
||||
},
|
||||
];
|
||||
|
||||
// Use production sanitization helper
|
||||
let sanitized = sanitize_messages_for_final_call(messages);
|
||||
|
||||
// Verify sanitization
|
||||
assert_eq!(sanitized.len(), 3);
|
||||
assert_eq!(sanitized[0].role, "user");
|
||||
assert_eq!(sanitized[1].role, "assistant");
|
||||
assert_eq!(sanitized[1].tool_calls, None); // Stripped
|
||||
assert_eq!(sanitized[2].role, "assistant"); // Converted from 'tool' to assistant
|
||||
assert!(sanitized[2].content.starts_with("[UNTRUSTED TOOL OUTPUT]:")); // Labeled as untrusted
|
||||
assert_eq!(sanitized[2].tool_call_id, None); // Stripped
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_message_sanitization_preserves_non_tool_messages() {
|
||||
let messages = vec![
|
||||
Message {
|
||||
role: "system".into(),
|
||||
content: "You are a helpful assistant".into(),
|
||||
tool_call_id: None,
|
||||
tool_calls: None,
|
||||
},
|
||||
Message {
|
||||
role: "user".into(),
|
||||
content: "Hello".into(),
|
||||
tool_call_id: None,
|
||||
tool_calls: None,
|
||||
},
|
||||
];
|
||||
|
||||
// Use production sanitization helper
|
||||
let sanitized = sanitize_messages_for_final_call(messages);
|
||||
|
||||
// Verify non-tool messages preserved
|
||||
assert_eq!(sanitized.len(), 2);
|
||||
assert_eq!(sanitized[0].role, "system");
|
||||
assert_eq!(sanitized[0].content, "You are a helpful assistant");
|
||||
assert_eq!(sanitized[1].role, "user");
|
||||
assert_eq!(sanitized[1].content, "Hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sanitize_messages_strips_tool_calls_from_all_messages() {
|
||||
let messages = vec![
|
||||
Message {
|
||||
role: "assistant".into(),
|
||||
content: "".into(),
|
||||
tool_call_id: None,
|
||||
tool_calls: Some(vec![crate::ai::ToolCall {
|
||||
id: "call_1".into(),
|
||||
name: "test".into(),
|
||||
arguments: "{}".into(),
|
||||
}]),
|
||||
},
|
||||
Message {
|
||||
role: "tool".into(),
|
||||
content: "output".into(),
|
||||
tool_call_id: Some("call_1".into()),
|
||||
tool_calls: None,
|
||||
},
|
||||
];
|
||||
|
||||
let sanitized = sanitize_messages_for_final_call(messages);
|
||||
|
||||
assert_eq!(sanitized.len(), 2);
|
||||
assert_eq!(sanitized[0].tool_calls, None); // Stripped from assistant
|
||||
assert_eq!(sanitized[1].tool_calls, None); // Already None, but verified
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_supports_tool_calling_flag_false_disables_tools() {
|
||||
// Test the logic that enforces supports_tool_calling flag
|
||||
// When flag is false, tools should be None even if tools exist
|
||||
|
||||
let supports_tool_calling = Some(false);
|
||||
let has_tools_available = true; // Simulate available tools
|
||||
|
||||
// Simulate the conditional logic from chat_message command (lines 403-415)
|
||||
let tools_enabled = if supports_tool_calling.unwrap_or(false) {
|
||||
if has_tools_available {
|
||||
Some(vec!["mock_tool"])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
assert!(
|
||||
tools_enabled.is_none(),
|
||||
"Tools should be None when supports_tool_calling is false"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_supports_tool_calling_flag_true_enables_tools() {
|
||||
let supports_tool_calling = Some(true);
|
||||
let has_tools_available = true;
|
||||
|
||||
let tools_enabled = if supports_tool_calling.unwrap_or(false) {
|
||||
if has_tools_available {
|
||||
Some(vec!["mock_tool"])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
assert!(
|
||||
tools_enabled.is_some(),
|
||||
"Tools should be enabled when supports_tool_calling is true"
|
||||
);
|
||||
assert_eq!(tools_enabled.unwrap().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_supports_tool_calling_flag_none_defaults_to_false() {
|
||||
let supports_tool_calling: Option<bool> = None;
|
||||
let has_tools_available = true;
|
||||
|
||||
let tools_enabled = if supports_tool_calling.unwrap_or(false) {
|
||||
if has_tools_available {
|
||||
Some(vec!["mock_tool"])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
assert!(
|
||||
tools_enabled.is_none(),
|
||||
"Tools should be None when supports_tool_calling is None (defaults to false)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_supports_tool_calling_true_but_no_tools_available() {
|
||||
let supports_tool_calling = Some(true);
|
||||
let has_tools_available = false;
|
||||
|
||||
let tools_enabled = if supports_tool_calling.unwrap_or(false) {
|
||||
if has_tools_available {
|
||||
Some(vec!["mock_tool"])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
assert!(
|
||||
tools_enabled.is_none(),
|
||||
"Tools should be None even when flag is true if no tools are available"
|
||||
);
|
||||
}
|
||||
|
||||
// Tests for detect_tool_calling_support
|
||||
// NOTE: These are unit tests for the detection logic, not integration tests with real providers.
|
||||
// They verify the logical correctness of detection criteria (checking for tool_calls presence,
|
||||
// error pattern matching) but do not exercise the full command implementation.
|
||||
// Tradeoff: These tests provide fast feedback on detection logic without requiring network calls
|
||||
// or mock providers. Integration tests with real providers would be more comprehensive but slower
|
||||
// and require test infrastructure setup.
|
||||
|
||||
#[test]
|
||||
fn test_detect_tool_calling_logic_with_tool_calls_in_response() {
|
||||
use crate::ai::ToolCall;
|
||||
|
||||
// Simulate a response that contains tool_calls
|
||||
let tool_calls = Some(vec![ToolCall {
|
||||
id: "call_1".to_string(),
|
||||
name: "test_tool".to_string(),
|
||||
arguments: "{}".to_string(),
|
||||
}]);
|
||||
|
||||
// Check if any tool_call has the expected name
|
||||
let supports_tools = tool_calls
|
||||
.as_ref()
|
||||
.map(|calls| calls.iter().any(|tc| tc.name == "test_tool"))
|
||||
.unwrap_or(false);
|
||||
|
||||
assert!(
|
||||
supports_tools,
|
||||
"Should detect tool support when response contains test_tool call"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_tool_calling_logic_without_tool_calls() {
|
||||
use crate::ai::ToolCall;
|
||||
|
||||
// Simulate a response without tool_calls
|
||||
let tool_calls: Option<Vec<ToolCall>> = None;
|
||||
|
||||
let supports_tools = tool_calls
|
||||
.as_ref()
|
||||
.map(|calls| calls.iter().any(|tc| tc.name == "test_tool"))
|
||||
.unwrap_or(false);
|
||||
|
||||
assert!(
|
||||
!supports_tools,
|
||||
"Should not detect tool support when response has no tool_calls"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_tool_calling_logic_with_wrong_tool_name() {
|
||||
use crate::ai::ToolCall;
|
||||
|
||||
// Simulate a response with tool_calls but wrong tool name
|
||||
let tool_calls = Some(vec![ToolCall {
|
||||
id: "call_1".to_string(),
|
||||
name: "different_tool".to_string(),
|
||||
arguments: "{}".to_string(),
|
||||
}]);
|
||||
|
||||
let supports_tools = tool_calls
|
||||
.as_ref()
|
||||
.map(|calls| calls.iter().any(|tc| tc.name == "test_tool"))
|
||||
.unwrap_or(false);
|
||||
|
||||
assert!(
|
||||
!supports_tools,
|
||||
"Should not detect tool support when tool name doesn't match"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_tool_calling_error_patterns() {
|
||||
// Test error message patterns that indicate tool calling is not supported
|
||||
let error_cases = vec![
|
||||
"503 Service Unavailable: UNEXPECTED_TOOL_CALL",
|
||||
"Tool calling not supported",
|
||||
"Function calls are not allowed",
|
||||
"tools parameter is invalid",
|
||||
];
|
||||
|
||||
for error_msg in error_cases {
|
||||
let msg_lower = error_msg.to_lowercase();
|
||||
let is_tool_error = msg_lower.contains("tool")
|
||||
|| msg_lower.contains("function")
|
||||
|| msg_lower.contains("503");
|
||||
|
||||
assert!(
|
||||
is_tool_error,
|
||||
"Error message '{}' should be recognized as tool-related",
|
||||
error_msg
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_detect_tool_calling_non_tool_errors() {
|
||||
// Test error messages that are NOT tool-related
|
||||
let error_cases = vec![
|
||||
"Connection timeout",
|
||||
"Invalid API key",
|
||||
"Rate limit exceeded",
|
||||
"Network error",
|
||||
];
|
||||
|
||||
for error_msg in error_cases {
|
||||
let msg_lower = error_msg.to_lowercase();
|
||||
let is_tool_error = msg_lower.contains("tool")
|
||||
|| msg_lower.contains("function")
|
||||
|| msg_lower.contains("503");
|
||||
|
||||
assert!(
|
||||
!is_tool_error,
|
||||
"Error message '{}' should NOT be recognized as tool-related",
|
||||
error_msg
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -58,7 +58,8 @@ const SAFE_TEXT_EXTENSIONS: &[&str] = &[
|
||||
"rtf",
|
||||
];
|
||||
|
||||
const SAFE_BINARY_EXTENSIONS: &[&str] = &["pdf", "docx", "doc", "xlsx", "xls"];
|
||||
const SAFE_BINARY_EXTENSIONS: &[&str] =
|
||||
&["pdf", "docx", "doc", "xlsx", "xls", "pcap", "pcapng", "cap"];
|
||||
|
||||
fn compress_text(text: &str) -> Result<Vec<u8>, String> {
|
||||
let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
|
||||
@ -107,6 +108,7 @@ pub fn extract_text_content(path: &Path) -> Result<String, String> {
|
||||
match ext.as_str() {
|
||||
"pdf" => extract_pdf_text(path),
|
||||
"docx" | "doc" => extract_docx_text(path),
|
||||
"pcap" | "pcapng" | "cap" => extract_pcap_text(path),
|
||||
"xlsx" | "xls" => Err(format!(
|
||||
"Spreadsheet format .{ext} is not yet supported for text extraction. \
|
||||
Export the sheet as CSV and upload that instead."
|
||||
@ -175,6 +177,81 @@ fn extract_docx_text(path: &Path) -> Result<String, String> {
|
||||
Ok(text)
|
||||
}
|
||||
|
||||
fn extract_pcap_text(path: &Path) -> Result<String, String> {
|
||||
// Try to use tshark (Wireshark CLI) to extract packet data
|
||||
// Limit to first 1000 packets and disable name resolution to prevent OOM and stalls
|
||||
let output = std::process::Command::new("tshark")
|
||||
.arg("-r")
|
||||
.arg(path)
|
||||
.arg("-n") // Disable name resolution
|
||||
.arg("-c")
|
||||
.arg("1000") // Limit to first 1000 packets
|
||||
.arg("-V") // Verbose packet dissection
|
||||
.output();
|
||||
|
||||
match output {
|
||||
Ok(result) if result.status.success() => {
|
||||
let text = String::from_utf8_lossy(&result.stdout).to_string();
|
||||
if text.trim().is_empty() {
|
||||
return Err("PCAP file contains no packets or is empty".to_string());
|
||||
}
|
||||
// Truncate to 1MB to prevent memory issues with verbose output
|
||||
const MAX_PCAP_TEXT: usize = 1024 * 1024;
|
||||
if text.len() > MAX_PCAP_TEXT {
|
||||
Ok(format!(
|
||||
"{}... (truncated from {} bytes)",
|
||||
&text[..MAX_PCAP_TEXT],
|
||||
text.len()
|
||||
))
|
||||
} else {
|
||||
Ok(text)
|
||||
}
|
||||
}
|
||||
Ok(result) => {
|
||||
let stderr = String::from_utf8_lossy(&result.stderr);
|
||||
Err(format!("tshark failed to process PCAP: {stderr}"))
|
||||
}
|
||||
Err(_) => {
|
||||
// tshark not installed - try tcpdump as fallback
|
||||
let tcpdump_output = std::process::Command::new("tcpdump")
|
||||
.arg("-n") // Don't resolve addresses
|
||||
.arg("-c")
|
||||
.arg("1000") // Limit to first 1000 packets
|
||||
.arg("-r")
|
||||
.arg(path)
|
||||
.arg("-A") // Print packet payload in ASCII
|
||||
.output();
|
||||
|
||||
match tcpdump_output {
|
||||
Ok(result) if result.status.success() => {
|
||||
let text = String::from_utf8_lossy(&result.stdout).to_string();
|
||||
if text.trim().is_empty() {
|
||||
return Err("PCAP file contains no packets or is empty".to_string());
|
||||
}
|
||||
// Truncate to 1MB to prevent memory issues with verbose output
|
||||
const MAX_PCAP_TEXT: usize = 1024 * 1024;
|
||||
if text.len() > MAX_PCAP_TEXT {
|
||||
Ok(format!(
|
||||
"{}... (truncated from {} bytes)",
|
||||
&text[..MAX_PCAP_TEXT],
|
||||
text.len()
|
||||
))
|
||||
} else {
|
||||
Ok(text)
|
||||
}
|
||||
}
|
||||
Ok(result) => {
|
||||
let stderr = String::from_utf8_lossy(&result.stderr);
|
||||
Err(format!("tcpdump failed to process PCAP: {stderr}"))
|
||||
}
|
||||
Err(_) => Err(
|
||||
"Neither tshark nor tcpdump is installed. Install Wireshark or tcpdump to analyze packet captures.".to_string()
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_log_file_path(file_path: &str) -> Result<PathBuf, String> {
|
||||
let path = Path::new(file_path);
|
||||
let canonical = std::fs::canonicalize(path).map_err(|_| "Unable to access selected file")?;
|
||||
@ -240,6 +317,8 @@ pub async fn upload_log_file(
|
||||
"md" | "markdown" => "text/markdown",
|
||||
"csv" | "tsv" => "text/csv",
|
||||
"html" | "htm" => "text/html",
|
||||
"pcap" | "cap" => "application/vnd.tcpdump.pcap",
|
||||
"pcapng" => "application/x-pcapng",
|
||||
_ => "text/plain",
|
||||
};
|
||||
|
||||
@ -338,6 +417,8 @@ pub async fn upload_log_file_by_content(
|
||||
"md" | "markdown" => "text/markdown",
|
||||
"csv" | "tsv" => "text/csv",
|
||||
"html" | "htm" => "text/html",
|
||||
"pcap" | "cap" => "application/vnd.tcpdump.pcap",
|
||||
"pcapng" => "application/x-pcapng",
|
||||
_ => "text/plain",
|
||||
};
|
||||
|
||||
@ -763,4 +844,57 @@ mod tests {
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), text);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pcap_files_recognized_as_safe_binary() {
|
||||
use std::path::Path;
|
||||
assert!(is_safe_file(Path::new("capture.pcap")));
|
||||
assert!(is_safe_file(Path::new("network.pcapng")));
|
||||
assert!(is_safe_file(Path::new("traffic.cap")));
|
||||
assert!(!is_safe_file(Path::new("malicious.exe")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pcap_mime_types() {
|
||||
use std::path::Path;
|
||||
// This test verifies that pcap files get proper MIME types in upload_log_file
|
||||
// The actual MIME type mapping is in the upload_log_file function
|
||||
// We're testing that the extension detection works correctly
|
||||
let pcap_path = Path::new("test.pcap");
|
||||
let pcapng_path = Path::new("test.pcapng");
|
||||
let cap_path = Path::new("test.cap");
|
||||
|
||||
assert_eq!(pcap_path.extension().and_then(|e| e.to_str()), Some("pcap"));
|
||||
assert_eq!(
|
||||
pcapng_path.extension().and_then(|e| e.to_str()),
|
||||
Some("pcapng")
|
||||
);
|
||||
assert_eq!(cap_path.extension().and_then(|e| e.to_str()), Some("cap"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_pcap_requires_external_tool() {
|
||||
use std::path::Path;
|
||||
// extract_pcap_text requires tshark or tcpdump to be installed
|
||||
// This test verifies that the function returns appropriate error when neither is available
|
||||
// Note: We can't test actual extraction without mock/fixture files and installed tools
|
||||
|
||||
// Verify that trying to extract from a non-existent file returns an error
|
||||
let fake_path = Path::new("/tmp/nonexistent_test_file.pcap");
|
||||
let result = extract_pcap_text(fake_path);
|
||||
|
||||
// Should fail because file doesn't exist (and possibly no tshark/tcpdump)
|
||||
assert!(result.is_err());
|
||||
|
||||
// Error message should mention either tshark failure or missing tools
|
||||
let error_msg = result.unwrap_err();
|
||||
assert!(
|
||||
error_msg.contains("tshark")
|
||||
|| error_msg.contains("tcpdump")
|
||||
|| error_msg.contains("Neither")
|
||||
|| error_msg.contains("No such file"),
|
||||
"Expected error about missing tools or file, got: {}",
|
||||
error_msg
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -97,17 +97,12 @@ pub fn activate_kubeconfig(id: String, state: State<'_, AppState>) -> Result<(),
|
||||
.map_err(|e| format!("Failed to deactivate configs: {e}"))?;
|
||||
|
||||
// Activate the specified config
|
||||
let rows_updated = db
|
||||
.execute(
|
||||
db.execute(
|
||||
"UPDATE kubeconfig_files SET is_active = 1 WHERE id = ?1",
|
||||
params![&id],
|
||||
)
|
||||
.map_err(|e| format!("Failed to activate config: {e}"))?;
|
||||
|
||||
if rows_updated == 0 {
|
||||
return Err(format!("Kubeconfig with id '{id}' not found"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@ -159,8 +159,8 @@ pub async fn save_ai_provider(
|
||||
db.execute(
|
||||
"INSERT OR REPLACE INTO ai_providers
|
||||
(id, name, provider_type, api_url, encrypted_api_key, model, max_tokens, temperature,
|
||||
custom_endpoint_path, custom_auth_header, custom_auth_prefix, api_format, user_id, use_datastore_upload, updated_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, datetime('now'))",
|
||||
custom_endpoint_path, custom_auth_header, custom_auth_prefix, api_format, user_id, use_datastore_upload, supports_tool_calling, updated_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, datetime('now'))",
|
||||
rusqlite::params![
|
||||
uuid::Uuid::now_v7().to_string(),
|
||||
provider.name,
|
||||
@ -176,6 +176,7 @@ pub async fn save_ai_provider(
|
||||
provider.api_format,
|
||||
provider.user_id,
|
||||
provider.use_datastore_upload,
|
||||
provider.supports_tool_calling,
|
||||
],
|
||||
)
|
||||
.map_err(|e| format!("Failed to save AI provider: {e}"))?;
|
||||
@ -193,7 +194,7 @@ pub async fn load_ai_providers(
|
||||
let mut stmt = db
|
||||
.prepare(
|
||||
"SELECT name, provider_type, api_url, encrypted_api_key, model, max_tokens, temperature,
|
||||
custom_endpoint_path, custom_auth_header, custom_auth_prefix, api_format, user_id, use_datastore_upload
|
||||
custom_endpoint_path, custom_auth_header, custom_auth_prefix, api_format, user_id, use_datastore_upload, supports_tool_calling
|
||||
FROM ai_providers
|
||||
ORDER BY name",
|
||||
)
|
||||
@ -217,6 +218,7 @@ pub async fn load_ai_providers(
|
||||
row.get::<_, Option<String>>(10)?, // api_format
|
||||
row.get::<_, Option<String>>(11)?, // user_id
|
||||
row.get::<_, Option<bool>>(12)?, // use_datastore_upload
|
||||
row.get::<_, Option<bool>>(13)?, // supports_tool_calling
|
||||
))
|
||||
})
|
||||
.map_err(|e| e.to_string())?
|
||||
@ -236,6 +238,7 @@ pub async fn load_ai_providers(
|
||||
api_format,
|
||||
user_id,
|
||||
use_datastore_upload,
|
||||
supports_tool_calling,
|
||||
)| {
|
||||
// Decrypt the API key
|
||||
let api_key = crate::integrations::auth::decrypt_token(&encrypted_key).ok()?;
|
||||
@ -255,6 +258,7 @@ pub async fn load_ai_providers(
|
||||
session_id: None, // Session IDs are not persisted
|
||||
user_id,
|
||||
use_datastore_upload,
|
||||
supports_tool_calling,
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
@ -4,7 +4,7 @@ use std::path::Path;
|
||||
fn generate_key() -> String {
|
||||
use rand::RngCore;
|
||||
let mut bytes = [0u8; 32];
|
||||
rand::rngs::OsRng.fill_bytes(&mut bytes);
|
||||
rand::rng().fill_bytes(&mut bytes);
|
||||
hex::encode(bytes)
|
||||
}
|
||||
|
||||
@ -29,8 +29,15 @@ fn write_key_file(path: &Path, key: &str) -> anyhow::Result<()> {
|
||||
}
|
||||
|
||||
fn get_db_key(data_dir: &Path) -> anyhow::Result<String> {
|
||||
// Support both TRCAA_DB_KEY (new) and TFTSR_DB_KEY (legacy) for backwards compatibility
|
||||
if let Ok(key) = std::env::var("TRCAA_DB_KEY") {
|
||||
if !key.trim().is_empty() {
|
||||
return Ok(key);
|
||||
}
|
||||
}
|
||||
if let Ok(key) = std::env::var("TFTSR_DB_KEY") {
|
||||
if !key.trim().is_empty() {
|
||||
tracing::warn!("TFTSR_DB_KEY is deprecated, use TRCAA_DB_KEY instead");
|
||||
return Ok(key);
|
||||
}
|
||||
}
|
||||
@ -136,7 +143,7 @@ fn is_plain_sqlite(path: &Path) -> bool {
|
||||
|
||||
pub fn init_db(data_dir: &Path) -> anyhow::Result<Connection> {
|
||||
std::fs::create_dir_all(data_dir)?;
|
||||
let db_path = data_dir.join("trcaa.db");
|
||||
let db_path = data_dir.join("tftsr.db");
|
||||
|
||||
let key = get_db_key(data_dir)?;
|
||||
|
||||
@ -183,23 +190,23 @@ mod tests {
|
||||
#[test]
|
||||
fn test_get_db_key_uses_env_var_when_present() {
|
||||
// Remove any existing env var first
|
||||
std::env::remove_var("TFTSR_DB_KEY");
|
||||
std::env::remove_var("TRCAA_DB_KEY");
|
||||
let dir = temp_dir("env-var");
|
||||
std::env::set_var("TFTSR_DB_KEY", "test-db-key");
|
||||
std::env::set_var("TRCAA_DB_KEY", "test-db-key");
|
||||
let key = get_db_key(&dir).unwrap();
|
||||
assert_eq!(key, "test-db-key");
|
||||
std::env::remove_var("TFTSR_DB_KEY");
|
||||
std::env::remove_var("TRCAA_DB_KEY");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_db_key_debug_fallback_for_empty_env() {
|
||||
// Remove any existing env var first
|
||||
std::env::remove_var("TFTSR_DB_KEY");
|
||||
std::env::remove_var("TRCAA_DB_KEY");
|
||||
let dir = temp_dir("empty-env");
|
||||
std::env::set_var("TFTSR_DB_KEY", " ");
|
||||
std::env::set_var("TRCAA_DB_KEY", " ");
|
||||
let key = get_db_key(&dir).unwrap();
|
||||
assert_eq!(key, "dev-key-change-in-prod");
|
||||
std::env::remove_var("TFTSR_DB_KEY");
|
||||
std::env::remove_var("TRCAA_DB_KEY");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@ -380,7 +380,6 @@ pub fn run_migrations(conn: &Connection) -> anyhow::Result<()> {
|
||||
|| name.ends_with("_add_log_content_compressed")
|
||||
|| name.ends_with("_add_image_data")
|
||||
|| name.ends_with("_add_supports_tool_calling")
|
||||
|| name.ends_with("_add_mcp_env_config")
|
||||
{
|
||||
// Use execute for ALTER TABLE (SQLite only allows one statement per command)
|
||||
// Skip error if column already exists (SQLITE_ERROR with "duplicate column name")
|
||||
|
||||
@ -24,11 +24,11 @@ pub struct PatCredential {
|
||||
|
||||
/// Generate a PKCE code verifier and challenge for OAuth flows.
|
||||
pub fn generate_pkce() -> PkceChallenge {
|
||||
use rand::{thread_rng, RngCore};
|
||||
use rand::RngCore;
|
||||
|
||||
// Generate a random 32-byte verifier
|
||||
let mut verifier_bytes = [0u8; 32];
|
||||
thread_rng().fill_bytes(&mut verifier_bytes);
|
||||
rand::rng().fill_bytes(&mut verifier_bytes);
|
||||
|
||||
let code_verifier = base64_url_encode(&verifier_bytes);
|
||||
let challenge_hash = Sha256::digest(code_verifier.as_bytes());
|
||||
@ -54,7 +54,7 @@ pub fn build_auth_url(
|
||||
urlencoding_encode(client_id),
|
||||
urlencoding_encode(redirect_uri),
|
||||
urlencoding_encode(scope),
|
||||
&pkce.code_challenge,
|
||||
pkce.code_challenge,
|
||||
)
|
||||
}
|
||||
|
||||
@ -169,8 +169,15 @@ fn urlencoding_encode(s: &str) -> String {
|
||||
}
|
||||
|
||||
fn get_encryption_key_material() -> Result<String, String> {
|
||||
// Support both TRCAA_ENCRYPTION_KEY (new) and TFTSR_ENCRYPTION_KEY (legacy) for backwards compatibility
|
||||
if let Ok(key) = std::env::var("TRCAA_ENCRYPTION_KEY") {
|
||||
if !key.trim().is_empty() {
|
||||
return Ok(key);
|
||||
}
|
||||
}
|
||||
if let Ok(key) = std::env::var("TFTSR_ENCRYPTION_KEY") {
|
||||
if !key.trim().is_empty() {
|
||||
tracing::warn!("TFTSR_ENCRYPTION_KEY is deprecated, use TRCAA_ENCRYPTION_KEY instead");
|
||||
return Ok(key);
|
||||
}
|
||||
}
|
||||
@ -197,7 +204,7 @@ fn get_encryption_key_material() -> Result<String, String> {
|
||||
// Generate and store new key
|
||||
use rand::RngCore;
|
||||
let mut bytes = [0u8; 32];
|
||||
rand::rngs::OsRng.fill_bytes(&mut bytes);
|
||||
rand::rng().fill_bytes(&mut bytes);
|
||||
let key = hex::encode(bytes);
|
||||
|
||||
// Ensure directory exists
|
||||
@ -244,14 +251,14 @@ fn derive_aes_key() -> Result<[u8; 32], String> {
|
||||
}
|
||||
|
||||
/// Encrypt a token using AES-256-GCM.
|
||||
/// Key is derived from TFTSR_ENCRYPTION_KEY env var or a default dev key.
|
||||
/// Key is derived from TRCAA_ENCRYPTION_KEY env var (or legacy TFTSR_ENCRYPTION_KEY) or a default dev key.
|
||||
/// Returns base64-encoded ciphertext with nonce prepended.
|
||||
pub fn encrypt_token(token: &str) -> Result<String, String> {
|
||||
use aes_gcm::{
|
||||
aead::{Aead, KeyInit},
|
||||
Aes256Gcm, Nonce,
|
||||
};
|
||||
use rand::{thread_rng, RngCore};
|
||||
use rand::RngCore;
|
||||
|
||||
let key_bytes = derive_aes_key()?;
|
||||
|
||||
@ -259,7 +266,7 @@ pub fn encrypt_token(token: &str) -> Result<String, String> {
|
||||
|
||||
// Generate random nonce
|
||||
let mut nonce_bytes = [0u8; 12];
|
||||
thread_rng().fill_bytes(&mut nonce_bytes);
|
||||
rand::rng().fill_bytes(&mut nonce_bytes);
|
||||
let nonce = Nonce::from_slice(&nonce_bytes);
|
||||
|
||||
// Encrypt
|
||||
@ -550,14 +557,14 @@ mod tests {
|
||||
fn test_decrypt_wrong_key_fails() {
|
||||
// Encrypt with one key
|
||||
std::env::set_var(
|
||||
"TFTSR_ENCRYPTION_KEY",
|
||||
"TRCAA_ENCRYPTION_KEY",
|
||||
"key-1-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
);
|
||||
let encrypted = encrypt_token("secret").unwrap();
|
||||
|
||||
// Try to decrypt with different key
|
||||
std::env::set_var(
|
||||
"TFTSR_ENCRYPTION_KEY",
|
||||
"TRCAA_ENCRYPTION_KEY",
|
||||
"key-2-yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy",
|
||||
);
|
||||
let result = decrypt_token(&encrypted);
|
||||
@ -566,7 +573,7 @@ mod tests {
|
||||
assert!(result.unwrap_err().contains("Decryption failed"));
|
||||
|
||||
// Reset env var
|
||||
std::env::remove_var("TFTSR_ENCRYPTION_KEY");
|
||||
std::env::remove_var("TRCAA_ENCRYPTION_KEY");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -650,14 +657,14 @@ mod tests {
|
||||
aead::{Aead, KeyInit},
|
||||
Aes256Gcm, Nonce,
|
||||
};
|
||||
use rand::{thread_rng, RngCore};
|
||||
use rand::RngCore;
|
||||
|
||||
let cipher = Aes256Gcm::new_from_slice(key_bytes)
|
||||
.map_err(|e| format!("Failed to create cipher: {e}"))?;
|
||||
|
||||
// Generate random nonce
|
||||
let mut nonce_bytes = [0u8; 12];
|
||||
thread_rng().fill_bytes(&mut nonce_bytes);
|
||||
rand::rng().fill_bytes(&mut nonce_bytes);
|
||||
let nonce = Nonce::from_slice(&nonce_bytes);
|
||||
|
||||
// Encrypt
|
||||
|
||||
@ -128,7 +128,7 @@ pub async fn search_confluence(
|
||||
}
|
||||
}
|
||||
|
||||
all_results.sort_by(|a, b| canonicalize_url(&a.url).cmp(&canonicalize_url(&b.url)));
|
||||
all_results.sort_by_key(|a| canonicalize_url(&a.url));
|
||||
all_results.dedup_by(|a, b| canonicalize_url(&a.url) == canonicalize_url(&b.url));
|
||||
|
||||
Ok(all_results)
|
||||
|
||||
@ -10,15 +10,15 @@ use std::collections::HashSet;
|
||||
fn get_product_synonyms(query: &str) -> Vec<String> {
|
||||
let mut synonyms = Vec::new();
|
||||
|
||||
// VESTA NXT related synonyms
|
||||
if query.to_lowercase().contains("vesta") || query.to_lowercase().contains("vnxt") {
|
||||
// DevOps Platform related synonyms
|
||||
if query.to_lowercase().contains("platform") || query.to_lowercase().contains("product") {
|
||||
synonyms.extend(vec![
|
||||
"VESTA NXT".to_string(),
|
||||
"Vesta NXT".to_string(),
|
||||
"VNXT".to_string(),
|
||||
"vnxt".to_string(),
|
||||
"Vesta".to_string(),
|
||||
"vesta".to_string(),
|
||||
"DevOps Platform".to_string(),
|
||||
"DevOps Platform NXT".to_string(),
|
||||
"DevOps Tool".to_string(),
|
||||
"product".to_string(),
|
||||
"DevOps Platform".to_string(),
|
||||
"platform".to_string(),
|
||||
"VNX".to_string(),
|
||||
"vnx".to_string(),
|
||||
]);
|
||||
@ -67,7 +67,7 @@ fn get_product_synonyms(query: &str) -> Vec<String> {
|
||||
/// Expand a search query with related terms for better search coverage
|
||||
///
|
||||
/// This function takes a user query and expands it with:
|
||||
/// - Product name synonyms (e.g., "VNXT" -> "VESTA NXT", "Vesta NXT")
|
||||
/// - Product name synonyms (e.g., "DevOps Tool" -> "DevOps Platform", "DevOps Platform NXT")
|
||||
/// - Version number variations
|
||||
/// - Related terms based on query content
|
||||
///
|
||||
@ -239,7 +239,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_expand_query_with_product_synonyms() {
|
||||
let query = "upgrade vesta nxt to 1.1.9";
|
||||
let query = "upgrade devops platform to 1.1.9";
|
||||
let expanded = expand_query(query);
|
||||
|
||||
// Should contain original query
|
||||
@ -248,7 +248,7 @@ mod tests {
|
||||
// Should contain product synonyms
|
||||
assert!(expanded
|
||||
.iter()
|
||||
.any(|s| s.contains("vnxt") || s.contains("vnxt")));
|
||||
.any(|s| s.contains("product") || s.contains("product")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -262,24 +262,24 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_extract_keywords() {
|
||||
let query = "How do I upgrade VESTA NXT from 1.0.12 to 1.1.9?";
|
||||
let query = "How do I upgrade DevOps Platform from 1.0.12 to 1.1.9?";
|
||||
let keywords = extract_keywords(query);
|
||||
|
||||
assert!(keywords.contains(&"upgrade".to_string()));
|
||||
assert!(keywords.contains(&"vesta".to_string()));
|
||||
assert!(keywords.contains(&"nxt".to_string()));
|
||||
assert!(keywords.contains(&"platform".to_string()));
|
||||
assert!(keywords.contains(&"devops".to_string()));
|
||||
assert!(keywords.contains(&"1.0.12".to_string()));
|
||||
assert!(keywords.contains(&"1.1.9".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_product_synonyms() {
|
||||
let synonyms = get_product_synonyms("vesta nxt upgrade");
|
||||
let synonyms = get_product_synonyms("devops platform upgrade");
|
||||
|
||||
// Should contain VNXT synonym
|
||||
// Should contain DevOps Tool synonym
|
||||
assert!(synonyms
|
||||
.iter()
|
||||
.any(|s| s.contains("VNXT") || s.contains("vnxt")));
|
||||
.any(|s| s.contains("DevOps Tool") || s.contains("product")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@ -142,13 +142,13 @@ pub async fn extract_cookies_via_ipc<R: tauri::Runtime>(
|
||||
}
|
||||
|
||||
// Store in a global variable that Rust can read
|
||||
window.__TFTSR_COOKIES__ = cookies;
|
||||
console.log('[TFTSR] Extracted', cookies.length, 'cookies');
|
||||
window.__TRCAA_COOKIES__ = cookies;
|
||||
console.log('[TRCAA] Extracted', cookies.length, 'cookies');
|
||||
return cookies.length;
|
||||
} catch (e) {
|
||||
console.error('[TFTSR] Cookie extraction failed:', e);
|
||||
window.__TFTSR_COOKIES__ = [];
|
||||
window.__TFTSR_ERROR__ = e.message;
|
||||
console.error('[TRCAA] Cookie extraction failed:', e);
|
||||
window.__TRCAA_COOKIES__ = [];
|
||||
window.__TRCAA_ERROR__ = e.message;
|
||||
return -1;
|
||||
}
|
||||
})();
|
||||
@ -174,11 +174,11 @@ pub async fn extract_cookies_via_ipc<R: tauri::Runtime>(
|
||||
// Store result in localStorage, then copy to document.title for Rust to read
|
||||
let check_and_signal_script = r#"
|
||||
try {
|
||||
if (typeof window.__TFTSR_ERROR__ !== 'undefined') {
|
||||
window.localStorage.setItem('tftsr_result', JSON.stringify({ error: window.__TFTSR_ERROR__ }));
|
||||
} else if (typeof window.__TFTSR_COOKIES__ !== 'undefined' && window.__TFTSR_COOKIES__.length > 0) {
|
||||
window.localStorage.setItem('tftsr_result', JSON.stringify({ cookies: window.__TFTSR_COOKIES__ }));
|
||||
} else if (typeof window.__TFTSR_COOKIES__ !== 'undefined') {
|
||||
if (typeof window.__TRCAA_ERROR__ !== 'undefined') {
|
||||
window.localStorage.setItem('tftsr_result', JSON.stringify({ error: window.__TRCAA_ERROR__ }));
|
||||
} else if (typeof window.__TRCAA_COOKIES__ !== 'undefined' && window.__TRCAA_COOKIES__.length > 0) {
|
||||
window.localStorage.setItem('tftsr_result', JSON.stringify({ cookies: window.__TRCAA_COOKIES__ }));
|
||||
} else if (typeof window.__TRCAA_COOKIES__ !== 'undefined') {
|
||||
window.localStorage.setItem('tftsr_result', JSON.stringify({ cookies: [] }));
|
||||
}
|
||||
} catch (e) {
|
||||
@ -198,8 +198,8 @@ pub async fn extract_cookies_via_ipc<R: tauri::Runtime>(
|
||||
if (result) {
|
||||
window.localStorage.removeItem('tftsr_result');
|
||||
// Store in title temporarily for Rust to read
|
||||
window.__TFTSR_ORIGINAL_TITLE__ = document.title;
|
||||
document.title = 'TFTSR_RESULT:' + result;
|
||||
window.__TRCAA_ORIGINAL_TITLE__ = document.title;
|
||||
document.title = 'TRCAA_RESULT:' + result;
|
||||
}
|
||||
})();
|
||||
"#;
|
||||
@ -209,11 +209,11 @@ pub async fn extract_cookies_via_ipc<R: tauri::Runtime>(
|
||||
|
||||
// Read the title
|
||||
if let Ok(title) = webview_window.title() {
|
||||
if let Some(json_str) = title.strip_prefix("TFTSR_RESULT:") {
|
||||
if let Some(json_str) = title.strip_prefix("TRCAA_RESULT:") {
|
||||
// Restore original title
|
||||
let restore_title = r#"
|
||||
if (typeof window.__TFTSR_ORIGINAL_TITLE__ !== 'undefined') {
|
||||
document.title = window.__TFTSR_ORIGINAL_TITLE__;
|
||||
if (typeof window.__TRCAA_ORIGINAL_TITLE__ !== 'undefined') {
|
||||
document.title = window.__TRCAA_ORIGINAL_TITLE__;
|
||||
}
|
||||
"#;
|
||||
webview_window.eval(restore_title).ok();
|
||||
|
||||
@ -45,7 +45,7 @@ pub async fn fetch_from_webview<R: tauri::Runtime>(
|
||||
}});
|
||||
|
||||
if (!response.ok) {{
|
||||
window.location.hash = '#trcaa-error-' + requestId + '-' + encodeURIComponent(JSON.stringify({{
|
||||
window.location.hash = '#tftsr-error-' + requestId + '-' + encodeURIComponent(JSON.stringify({{
|
||||
error: `HTTP ${{response.status}}: ${{response.statusText}}`
|
||||
}}));
|
||||
return;
|
||||
@ -53,9 +53,9 @@ pub async fn fetch_from_webview<R: tauri::Runtime>(
|
||||
|
||||
const data = await response.json();
|
||||
// Store in hash - we'll poll for this
|
||||
window.location.hash = '#trcaa-success-' + requestId + '-' + encodeURIComponent(JSON.stringify(data));
|
||||
window.location.hash = '#tftsr-success-' + requestId + '-' + encodeURIComponent(JSON.stringify(data));
|
||||
}} catch (error) {{
|
||||
window.location.hash = '#trcaa-error-' + requestId + '-' + encodeURIComponent(JSON.stringify({{
|
||||
window.location.hash = '#tftsr-error-' + requestId + '-' + encodeURIComponent(JSON.stringify({{
|
||||
error: error.message
|
||||
}}));
|
||||
}}
|
||||
@ -77,7 +77,7 @@ pub async fn fetch_from_webview<R: tauri::Runtime>(
|
||||
let url_string = url_str.to_string();
|
||||
|
||||
// Check for success
|
||||
let success_marker = format!("#trcaa-success-{request_id}-");
|
||||
let success_marker = format!("#tftsr-success-{request_id}-");
|
||||
if url_string.contains(&success_marker) {
|
||||
// Extract the JSON from the hash
|
||||
if let Some(json_start) = url_string.find(&success_marker) {
|
||||
@ -96,7 +96,7 @@ pub async fn fetch_from_webview<R: tauri::Runtime>(
|
||||
}
|
||||
|
||||
// Check for error
|
||||
let error_marker = format!("#trcaa-error-{request_id}-");
|
||||
let error_marker = format!("#tftsr-error-{request_id}-");
|
||||
if url_string.contains(&error_marker) {
|
||||
if let Some(json_start) = url_string.find(&error_marker) {
|
||||
let json_encoded = &url_string[json_start + error_marker.len()..];
|
||||
|
||||
@ -110,7 +110,7 @@ async fn search_confluence_from_webview<R: tauri::Runtime>(
|
||||
);
|
||||
|
||||
// Execute JavaScript and store result in localStorage for retrieval
|
||||
let storage_key = format!("__trcaa_search_{}__", uuid::Uuid::now_v7());
|
||||
let storage_key = format!("__tftsr_search_{}__", uuid::Uuid::now_v7());
|
||||
let callback_script = format!(
|
||||
r#"
|
||||
{}
|
||||
|
||||
@ -63,11 +63,19 @@ pub fn run() {
|
||||
.manage(app_state)
|
||||
.setup(|app| {
|
||||
let handle = app.handle().clone();
|
||||
|
||||
// Initialize MCP servers
|
||||
tauri::async_runtime::spawn(async move {
|
||||
if let Err(e) = crate::mcp::discovery::init_all_servers(&handle).await {
|
||||
tracing::warn!("MCP startup discovery error: {e}");
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-detect kubeconfig
|
||||
// Note: Kubeconfig auto-detection is implemented in shell::kubeconfig::auto_detect_kubeconfig
|
||||
// but not called at startup because it requires database access which may not be initialized yet.
|
||||
// Users can manually upload kubeconfig files via the frontend UI.
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
@ -169,7 +177,12 @@ pub fn run() {
|
||||
|
||||
/// Determine the application data directory.
|
||||
fn dirs_data_dir() -> std::path::PathBuf {
|
||||
// Support both TRCAA_DATA_DIR (new) and TFTSR_DATA_DIR (legacy) for backwards compatibility
|
||||
if let Ok(dir) = std::env::var("TRCAA_DATA_DIR") {
|
||||
return std::path::PathBuf::from(dir);
|
||||
}
|
||||
if let Ok(dir) = std::env::var("TFTSR_DATA_DIR") {
|
||||
tracing::warn!("TFTSR_DATA_DIR is deprecated, use TRCAA_DATA_DIR instead");
|
||||
return std::path::PathBuf::from(dir);
|
||||
}
|
||||
|
||||
@ -177,13 +190,13 @@ fn dirs_data_dir() -> std::path::PathBuf {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
if let Ok(xdg) = std::env::var("XDG_DATA_HOME") {
|
||||
return std::path::PathBuf::from(xdg).join("trcaa");
|
||||
return std::path::PathBuf::from(xdg).join("tftsr");
|
||||
}
|
||||
if let Ok(home) = std::env::var("HOME") {
|
||||
return std::path::PathBuf::from(home)
|
||||
.join(".local")
|
||||
.join("share")
|
||||
.join("trcaa");
|
||||
.join("tftsr");
|
||||
}
|
||||
}
|
||||
|
||||
@ -193,17 +206,17 @@ fn dirs_data_dir() -> std::path::PathBuf {
|
||||
return std::path::PathBuf::from(home)
|
||||
.join("Library")
|
||||
.join("Application Support")
|
||||
.join("trcaa");
|
||||
.join("tftsr");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
if let Ok(appdata) = std::env::var("APPDATA") {
|
||||
return std::path::PathBuf::from(appdata).join("trcaa");
|
||||
return std::path::PathBuf::from(appdata).join("tftsr");
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback
|
||||
std::path::PathBuf::from("./trcaa-data")
|
||||
std::path::PathBuf::from("./tftsr-data")
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
tftsr_lib::run();
|
||||
trcaa_lib::run();
|
||||
}
|
||||
|
||||
@ -236,7 +236,7 @@ pub async fn initiate_mcp_oauth(
|
||||
let state_nonce = {
|
||||
use rand::RngCore;
|
||||
let mut bytes = [0u8; 16];
|
||||
rand::rngs::OsRng.fill_bytes(&mut bytes);
|
||||
rand::rng().fill_bytes(&mut bytes);
|
||||
hex::encode(bytes)
|
||||
};
|
||||
let auth_url = format!(
|
||||
|
||||
@ -1,32 +1,285 @@
|
||||
use http::{HeaderName, HeaderValue};
|
||||
use rmcp::transport::streamable_http_client::StreamableHttpClientTransportConfig;
|
||||
use rmcp::transport::StreamableHttpClientTransport;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Parse and validate custom headers for MCP transport.
|
||||
/// Returns a HashMap of validated HTTP headers ready for use in transport config.
|
||||
///
|
||||
/// Invalid headers (bad names or values) are logged and skipped.
|
||||
/// Reserved headers (accept, mcp-session-id, etc.) are rejected by rmcp and should not be provided.
|
||||
fn build_header_map(custom_headers: HashMap<String, String>) -> HashMap<HeaderName, HeaderValue> {
|
||||
let mut http_headers = HashMap::new();
|
||||
|
||||
// Add custom headers from caller
|
||||
for (key, value) in custom_headers.iter() {
|
||||
let name_result = HeaderName::from_bytes(key.as_bytes());
|
||||
let value_result = HeaderValue::from_str(value);
|
||||
|
||||
match (name_result, value_result) {
|
||||
(Ok(name), Ok(val)) => {
|
||||
// Skip reserved headers - rmcp manages these internally
|
||||
if name.as_str().eq_ignore_ascii_case("accept")
|
||||
|| name.as_str().eq_ignore_ascii_case("mcp-session-id")
|
||||
|| name.as_str().eq_ignore_ascii_case("last-event-id")
|
||||
{
|
||||
tracing::warn!(
|
||||
header_name = %name,
|
||||
"Header is reserved by rmcp, skipping (rmcp manages it automatically)"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
tracing::debug!(header_name = %name, "Added custom header");
|
||||
http_headers.insert(name, val);
|
||||
}
|
||||
(Err(name_err), _) => {
|
||||
tracing::warn!(
|
||||
error = %name_err,
|
||||
"Invalid header name, skipping (value: <redacted>)"
|
||||
);
|
||||
}
|
||||
(Ok(name), Err(value_err)) => {
|
||||
tracing::warn!(
|
||||
header_name = %name,
|
||||
error = %value_err,
|
||||
"Invalid header value, skipping (value: <redacted>)"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: Do NOT add Accept header here - rmcp automatically sends:
|
||||
// "Accept: text/event-stream, application/json" which is what MCP servers need.
|
||||
http_headers
|
||||
}
|
||||
|
||||
/// Build an HTTP (Streamable HTTP) transport from a URL with optional custom headers.
|
||||
/// Optionally attaches an Authorization bearer token.
|
||||
///
|
||||
/// NOTE: Custom headers are parsed but not yet applied due to rmcp v1.7.0 API limitations.
|
||||
/// The rmcp library's StreamableHttpClientTransportConfig does not expose a .header() method.
|
||||
/// Custom headers support is deferred until rmcp adds this capability or we find an alternative.
|
||||
/// Custom headers are now fully supported via rmcp's `.custom_headers()` method.
|
||||
pub fn build_http_transport(
|
||||
url: &str,
|
||||
auth_header: Option<&str>,
|
||||
custom_headers: HashMap<String, String>,
|
||||
) -> impl rmcp::transport::Transport<rmcp::RoleClient> {
|
||||
// Log warning if custom headers are provided (not yet supported)
|
||||
if !custom_headers.is_empty() {
|
||||
tracing::warn!(
|
||||
"Custom HTTP headers provided but not supported by rmcp v1.7.0: {:?}",
|
||||
custom_headers.keys().collect::<Vec<_>>()
|
||||
);
|
||||
let http_headers = build_header_map(custom_headers);
|
||||
|
||||
// Build config with auth header and custom headers
|
||||
let mut config = StreamableHttpClientTransportConfig::with_uri(Arc::from(url));
|
||||
|
||||
if let Some(token) = auth_header {
|
||||
config = config.auth_header(token.to_string());
|
||||
}
|
||||
|
||||
let config = match auth_header {
|
||||
Some(token) => StreamableHttpClientTransportConfig::with_uri(Arc::from(url))
|
||||
.auth_header(token.to_string()),
|
||||
None => StreamableHttpClientTransportConfig::with_uri(Arc::from(url)),
|
||||
};
|
||||
config = config.custom_headers(http_headers);
|
||||
|
||||
StreamableHttpClientTransport::from_config(config)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_empty_headers_returns_empty_map() {
|
||||
let headers = HashMap::new();
|
||||
let result = build_header_map(headers);
|
||||
|
||||
// rmcp handles Accept header automatically, so our map should be empty
|
||||
assert_eq!(result.len(), 0, "Should not add any headers");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rejects_reserved_accept_header() {
|
||||
let mut headers = HashMap::new();
|
||||
headers.insert("Accept".to_string(), "application/json".to_string());
|
||||
|
||||
let result = build_header_map(headers);
|
||||
|
||||
// Accept is reserved - should be rejected
|
||||
assert_eq!(result.len(), 0, "Reserved headers should be rejected");
|
||||
assert!(!result.contains_key(&HeaderName::from_static("accept")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rejects_reserved_session_id_header() {
|
||||
let mut headers = HashMap::new();
|
||||
headers.insert("Mcp-Session-Id".to_string(), "test123".to_string());
|
||||
|
||||
let result = build_header_map(headers);
|
||||
|
||||
// Session ID is reserved
|
||||
assert_eq!(result.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rejects_reserved_last_event_id_header() {
|
||||
let mut headers = HashMap::new();
|
||||
headers.insert("Last-Event-Id".to_string(), "123".to_string());
|
||||
|
||||
let result = build_header_map(headers);
|
||||
|
||||
// Last-Event-Id is reserved
|
||||
assert_eq!(result.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_adds_valid_custom_header() {
|
||||
let mut headers = HashMap::new();
|
||||
headers.insert("X-Custom-Header".to_string(), "custom-value".to_string());
|
||||
|
||||
let result = build_header_map(headers);
|
||||
|
||||
let custom = HeaderName::from_static("x-custom-header");
|
||||
assert!(result.contains_key(&custom));
|
||||
assert_eq!(result.get(&custom).unwrap(), "custom-value");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_adds_multiple_custom_headers() {
|
||||
let mut headers = HashMap::new();
|
||||
headers.insert("X-Header-One".to_string(), "value1".to_string());
|
||||
headers.insert("X-Header-Two".to_string(), "value2".to_string());
|
||||
headers.insert("X-Header-Three".to_string(), "value3".to_string());
|
||||
|
||||
let result = build_header_map(headers);
|
||||
|
||||
// Should have exactly 3 custom headers
|
||||
assert_eq!(result.len(), 3);
|
||||
assert!(result.contains_key(&HeaderName::from_static("x-header-one")));
|
||||
assert!(result.contains_key(&HeaderName::from_static("x-header-two")));
|
||||
assert!(result.contains_key(&HeaderName::from_static("x-header-three")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_skips_invalid_header_name() {
|
||||
let mut headers = HashMap::new();
|
||||
headers.insert("Invalid Header Name".to_string(), "value".to_string()); // spaces invalid
|
||||
headers.insert("Valid-Header".to_string(), "valid".to_string());
|
||||
|
||||
let result = build_header_map(headers);
|
||||
|
||||
// Should have only valid header, invalid is skipped
|
||||
assert_eq!(result.len(), 1);
|
||||
assert!(result.contains_key(&HeaderName::from_static("valid-header")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_skips_invalid_header_value() {
|
||||
let mut headers = HashMap::new();
|
||||
headers.insert("X-Valid-Name".to_string(), "invalid\nvalue".to_string()); // newline invalid
|
||||
headers.insert("X-Another".to_string(), "valid".to_string());
|
||||
|
||||
let result = build_header_map(headers);
|
||||
|
||||
// Should have only valid header
|
||||
assert_eq!(result.len(), 1);
|
||||
assert!(result.contains_key(&HeaderName::from_static("x-another")));
|
||||
assert_eq!(
|
||||
result.get(&HeaderName::from_static("x-another")).unwrap(),
|
||||
"valid"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_skips_header_with_null_byte_in_name() {
|
||||
let mut headers = HashMap::new();
|
||||
headers.insert("X-Bad\0Header".to_string(), "value".to_string());
|
||||
headers.insert("X-Good-Header".to_string(), "value".to_string());
|
||||
|
||||
let result = build_header_map(headers);
|
||||
|
||||
// Should have only good header
|
||||
assert_eq!(result.len(), 1);
|
||||
assert!(result.contains_key(&HeaderName::from_static("x-good-header")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_skips_header_with_null_byte_in_value() {
|
||||
let mut headers = HashMap::new();
|
||||
headers.insert("X-Header".to_string(), "bad\0value".to_string());
|
||||
headers.insert("X-Good".to_string(), "goodvalue".to_string());
|
||||
|
||||
let result = build_header_map(headers);
|
||||
|
||||
// Should have only good header
|
||||
assert_eq!(result.len(), 1);
|
||||
assert!(result.contains_key(&HeaderName::from_static("x-good")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_string_value_allowed() {
|
||||
let mut headers = HashMap::new();
|
||||
headers.insert("X-Empty-Value".to_string(), "".to_string());
|
||||
|
||||
let result = build_header_map(headers);
|
||||
|
||||
// Empty string is valid
|
||||
assert!(result.contains_key(&HeaderName::from_static("x-empty-value")));
|
||||
assert_eq!(
|
||||
result
|
||||
.get(&HeaderName::from_static("x-empty-value"))
|
||||
.unwrap(),
|
||||
""
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unicode_in_header_value_accepted() {
|
||||
let mut headers = HashMap::new();
|
||||
headers.insert("X-Unicode".to_string(), "café".to_string()); // UTF-8 is valid in HTTP header values
|
||||
headers.insert("X-Valid".to_string(), "ascii".to_string());
|
||||
|
||||
let result = build_header_map(headers);
|
||||
|
||||
// HeaderValue accepts valid UTF-8
|
||||
assert_eq!(result.len(), 2); // unicode + valid
|
||||
assert!(result.contains_key(&HeaderName::from_static("x-valid")));
|
||||
assert!(result.contains_key(&HeaderName::from_static("x-unicode")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reserved_accept_header_rejected() {
|
||||
let mut headers = HashMap::new();
|
||||
headers.insert("X-Custom".to_string(), "value".to_string());
|
||||
// Try to override Accept - should be rejected as reserved
|
||||
headers.insert(
|
||||
"Accept".to_string(),
|
||||
"application/json, text/event-stream".to_string(),
|
||||
);
|
||||
|
||||
let result = build_header_map(headers);
|
||||
|
||||
// Should have only custom header, Accept is reserved by rmcp
|
||||
assert_eq!(result.len(), 1);
|
||||
assert!(result.contains_key(&HeaderName::from_static("x-custom")));
|
||||
assert!(!result.contains_key(&HeaderName::from_static("accept")));
|
||||
}
|
||||
|
||||
// Transport building tests (verify no panics with Tokio runtime)
|
||||
#[test]
|
||||
fn test_builds_transport_with_http() {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let _guard = rt.enter();
|
||||
let _transport = build_http_transport("http://localhost:8080", None, HashMap::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_builds_transport_with_https() {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let _guard = rt.enter();
|
||||
let _transport = build_http_transport("https://example.com/mcp", None, HashMap::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_builds_transport_with_auth() {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let _guard = rt.enter();
|
||||
let _transport = build_http_transport(
|
||||
"http://localhost:8080",
|
||||
Some("Bearer token123"),
|
||||
HashMap::new(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,6 +25,10 @@ pub fn build_stdio_transport(
|
||||
"DYLD_LIBRARY_PATH",
|
||||
"DYLD_FRAMEWORK_PATH",
|
||||
"DYLD_FALLBACK_LIBRARY_PATH",
|
||||
"PYTHONPATH",
|
||||
"RUBYLIB",
|
||||
"NODE_PATH",
|
||||
"PERL5LIB",
|
||||
];
|
||||
|
||||
for key in env.keys() {
|
||||
@ -100,6 +104,52 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rejects_interpreter_path_hijack_vars() {
|
||||
let hijack_vars = vec![
|
||||
("PYTHONPATH", "/tmp/evil"),
|
||||
("RUBYLIB", "/tmp/evil"),
|
||||
("NODE_PATH", "/tmp/evil"),
|
||||
("PERL5LIB", "/tmp/evil"),
|
||||
];
|
||||
|
||||
for (key, value) in hijack_vars {
|
||||
let mut env = HashMap::new();
|
||||
env.insert(key.to_string(), value.to_string());
|
||||
|
||||
let result = build_stdio_transport("/usr/bin/test", &[], env);
|
||||
assert!(result.is_err(), "Should reject {}", key);
|
||||
if let Err(err) = result {
|
||||
assert!(
|
||||
err.contains("Dangerous environment variable"),
|
||||
"Error for '{}' should mention dangerous variable, got: {}",
|
||||
key,
|
||||
err
|
||||
);
|
||||
assert!(
|
||||
err.contains(key),
|
||||
"Error should include the offending variable name '{}', got: {}",
|
||||
key,
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_message_includes_variable_name() {
|
||||
let mut env = HashMap::new();
|
||||
env.insert("LD_PRELOAD".to_string(), "malicious.so".to_string());
|
||||
|
||||
let Err(err) = build_stdio_transport("/usr/bin/test", &[], env) else {
|
||||
panic!("Expected error");
|
||||
};
|
||||
assert!(
|
||||
err.contains("LD_PRELOAD"),
|
||||
"Error must name the rejected variable so the user knows what to fix, got: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_allows_safe_env_vars() {
|
||||
// Test that safe env vars pass validation (validation happens before spawn)
|
||||
|
||||
@ -51,86 +51,41 @@ pub async fn check_ollama() -> anyhow::Result<OllamaStatus> {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_install_instructions(platform: &str) -> InstallGuide {
|
||||
let url = "https://ollama.com/download".to_string();
|
||||
match platform {
|
||||
"linux" => InstallGuide {
|
||||
platform: "Linux".to_string(),
|
||||
steps: vec![
|
||||
"Open a terminal".to_string(),
|
||||
"Run: curl -fsSL https://ollama.com/install.sh | sh".to_string(),
|
||||
"Start Ollama: ollama serve".to_string(),
|
||||
"Pull a model: ollama pull llama3.2:3b".to_string(),
|
||||
],
|
||||
url,
|
||||
},
|
||||
"macos" => InstallGuide {
|
||||
platform: "macOS".to_string(),
|
||||
steps: vec![
|
||||
"Download the macOS installer from ollama.com/download".to_string(),
|
||||
"Open the downloaded .dmg file".to_string(),
|
||||
"Drag Ollama to Applications".to_string(),
|
||||
"Launch Ollama from Applications".to_string(),
|
||||
"Pull a model: ollama pull llama3.2:3b".to_string(),
|
||||
],
|
||||
url,
|
||||
},
|
||||
"windows" => InstallGuide {
|
||||
platform: "Windows".to_string(),
|
||||
steps: vec![
|
||||
"Download OllamaSetup.exe from ollama.com/download".to_string(),
|
||||
"Run the installer and follow the prompts".to_string(),
|
||||
"Ollama will start automatically in the system tray".to_string(),
|
||||
"Pull a model: ollama pull llama3.2:3b".to_string(),
|
||||
],
|
||||
url,
|
||||
},
|
||||
_ => InstallGuide {
|
||||
platform: platform.to_string(),
|
||||
steps: vec![
|
||||
"Visit https://ollama.com/download for installation instructions".to_string(),
|
||||
],
|
||||
url,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to find Ollama binary in common locations
|
||||
/// Find the full path to the ollama binary
|
||||
fn find_ollama_binary() -> Option<std::path::PathBuf> {
|
||||
let common_paths = [
|
||||
"/usr/local/bin/ollama",
|
||||
"/opt/homebrew/bin/ollama",
|
||||
"/usr/bin/ollama",
|
||||
"/home/linuxbrew/.linuxbrew/bin/ollama",
|
||||
];
|
||||
|
||||
for path in &common_paths {
|
||||
let p = std::path::Path::new(path);
|
||||
if p.exists() {
|
||||
return Some(p.to_path_buf());
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to which/where command
|
||||
// Try which/where command first
|
||||
let which_cmd = if cfg!(target_os = "windows") {
|
||||
"where"
|
||||
} else {
|
||||
"which"
|
||||
};
|
||||
|
||||
std::process::Command::new(which_cmd)
|
||||
.arg("ollama")
|
||||
.output()
|
||||
.ok()
|
||||
.and_then(|output| {
|
||||
if let Ok(output) = std::process::Command::new(which_cmd).arg("ollama").output() {
|
||||
if output.status.success() {
|
||||
String::from_utf8(output.stdout)
|
||||
.ok()
|
||||
.map(|s| std::path::PathBuf::from(s.trim()))
|
||||
} else {
|
||||
None
|
||||
if let Ok(path_str) = String::from_utf8(output.stdout) {
|
||||
let path = path_str.trim();
|
||||
if !path.is_empty() {
|
||||
return Some(std::path::PathBuf::from(path));
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: check common install paths
|
||||
let common_paths = [
|
||||
"/usr/local/bin/ollama",
|
||||
"/opt/homebrew/bin/ollama",
|
||||
"/usr/bin/ollama",
|
||||
];
|
||||
|
||||
for path in &common_paths {
|
||||
let pb = std::path::PathBuf::from(path);
|
||||
if pb.exists() {
|
||||
return Some(pb);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Attempt to start Ollama service if installed but not running
|
||||
@ -240,39 +195,61 @@ pub async fn start_ollama_service() -> anyhow::Result<bool> {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tracing::error!("Ollama binary not found");
|
||||
tracing::error!("Ollama binary not found in PATH or common locations");
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// On Windows, Ollama runs as a service, check if we can start it
|
||||
tracing::info!("Attempting to start Ollama on Windows...");
|
||||
if let Some(ollama_bin) = find_ollama_binary() {
|
||||
let result = std::process::Command::new(&ollama_bin).arg("serve").spawn();
|
||||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
|
||||
let new_status = check_ollama().await?;
|
||||
Ok(new_status.running)
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to start Ollama: {}", e);
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tracing::error!("Ollama binary not found");
|
||||
// On Windows, Ollama should be running as a system service
|
||||
// If it's not, user needs to start it manually or reinstall
|
||||
tracing::warn!("Ollama is installed but not running on Windows - user should start it from system tray");
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
|
||||
{
|
||||
tracing::warn!("Auto-start not supported on this platform");
|
||||
Ok(false)
|
||||
pub fn get_install_instructions(platform: &str) -> InstallGuide {
|
||||
let url = "https://ollama.com/download".to_string();
|
||||
match platform {
|
||||
"linux" => InstallGuide {
|
||||
platform: "Linux".to_string(),
|
||||
steps: vec![
|
||||
"Open a terminal".to_string(),
|
||||
"Run: curl -fsSL https://ollama.com/install.sh | sh".to_string(),
|
||||
"Start Ollama: ollama serve".to_string(),
|
||||
"Pull a model: ollama pull llama3.2:3b".to_string(),
|
||||
],
|
||||
url,
|
||||
},
|
||||
"macos" => InstallGuide {
|
||||
platform: "macOS".to_string(),
|
||||
steps: vec![
|
||||
"Download the macOS installer from ollama.com/download".to_string(),
|
||||
"Open the downloaded .dmg file".to_string(),
|
||||
"Drag Ollama to Applications".to_string(),
|
||||
"Launch Ollama from Applications".to_string(),
|
||||
"Pull a model: ollama pull llama3.2:3b".to_string(),
|
||||
],
|
||||
url,
|
||||
},
|
||||
"windows" => InstallGuide {
|
||||
platform: "Windows".to_string(),
|
||||
steps: vec![
|
||||
"Download OllamaSetup.exe from ollama.com/download".to_string(),
|
||||
"Run the installer and follow the prompts".to_string(),
|
||||
"Ollama will start automatically in the system tray".to_string(),
|
||||
"Pull a model: ollama pull llama3.2:3b".to_string(),
|
||||
],
|
||||
url,
|
||||
},
|
||||
_ => InstallGuide {
|
||||
platform: platform.to_string(),
|
||||
steps: vec![
|
||||
"Visit https://ollama.com/download for installation instructions".to_string(),
|
||||
],
|
||||
url,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -6,19 +6,12 @@ pub fn recommend_models(hw: &HardwareInfo) -> Vec<ModelRecommendation> {
|
||||
let has_gpu = hw.gpu_vendor.is_some();
|
||||
|
||||
let mut models = vec![
|
||||
ModelRecommendation {
|
||||
name: "llama3.2:1b".to_string(),
|
||||
size: "1.3 GB".to_string(),
|
||||
min_ram_gb: 4.0,
|
||||
description: "Smallest Llama 3.2 model. Fast, runs on minimal hardware.".to_string(),
|
||||
recommended: ram < 8.0,
|
||||
},
|
||||
ModelRecommendation {
|
||||
name: "llama3.2:3b".to_string(),
|
||||
size: "2.0 GB".to_string(),
|
||||
min_ram_gb: 6.0,
|
||||
description: "Balanced Llama 3.2 model. Good for most IT triage tasks.".to_string(),
|
||||
recommended: (8.0..16.0).contains(&ram),
|
||||
recommended: (6.0..16.0).contains(&ram),
|
||||
},
|
||||
ModelRecommendation {
|
||||
name: "phi3.5:3.8b".to_string(),
|
||||
@ -75,16 +68,16 @@ mod tests {
|
||||
#[test]
|
||||
fn test_low_ram_only_small_models() {
|
||||
let models = recommend_models(&hw(4.0, None));
|
||||
assert!(models.iter().all(|m| m.min_ram_gb <= 6.0));
|
||||
assert!(models.iter().any(|m| m.name == "llama3.2:1b"));
|
||||
assert!(models.iter().all(|m| m.min_ram_gb <= 8.0));
|
||||
assert!(models.iter().any(|m| m.name == "llama3.2:3b"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_low_ram_recommends_1b() {
|
||||
fn test_low_ram_recommends_3b() {
|
||||
let models = recommend_models(&hw(6.0, None));
|
||||
let rec = models.iter().find(|m| m.recommended);
|
||||
assert!(rec.is_some());
|
||||
assert_eq!(rec.unwrap().name, "llama3.2:1b");
|
||||
assert_eq!(rec.unwrap().name, "llama3.2:3b");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tokio::sync::{oneshot, Mutex as TokioMutex};
|
||||
use tokio::sync::Mutex as TokioMutex;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ProviderConfig {
|
||||
@ -43,6 +43,10 @@ pub struct ProviderConfig {
|
||||
/// Optional: When true, file uploads go to GenAI datastore instead of prompt
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub use_datastore_upload: Option<bool>,
|
||||
/// Optional: Whether this provider supports tool/function calling
|
||||
/// If None, defaults to false (provider can only be used for chat)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub supports_tool_calling: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@ -68,7 +72,7 @@ impl Default for AppSettings {
|
||||
}
|
||||
}
|
||||
|
||||
/// Response for shell command approval requests
|
||||
/// Approval response for shell command execution
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ApprovalResponse {
|
||||
pub approved: bool,
|
||||
@ -84,8 +88,9 @@ pub struct AppState {
|
||||
/// Live MCP server connections: server_id -> connection
|
||||
pub mcp_connections:
|
||||
Arc<TokioMutex<HashMap<String, Arc<TokioMutex<crate::mcp::client::McpConnection>>>>>,
|
||||
/// Pending shell command approval requests: approval_id -> response channel
|
||||
pub pending_approvals: Arc<TokioMutex<HashMap<String, oneshot::Sender<ApprovalResponse>>>>,
|
||||
/// Pending shell command approvals: approval_id -> response channel
|
||||
pub pending_approvals:
|
||||
Arc<TokioMutex<HashMap<String, tokio::sync::oneshot::Sender<ApprovalResponse>>>>,
|
||||
}
|
||||
|
||||
/// Determine the application data directory.
|
||||
@ -99,14 +104,14 @@ pub fn get_app_data_dir() -> Option<PathBuf> {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
if let Ok(xdg) = std::env::var("XDG_DATA_HOME") {
|
||||
return Some(PathBuf::from(xdg).join("trcaa"));
|
||||
return Some(PathBuf::from(xdg).join("tftsr"));
|
||||
}
|
||||
if let Ok(home) = std::env::var("HOME") {
|
||||
return Some(
|
||||
PathBuf::from(home)
|
||||
.join(".local")
|
||||
.join("share")
|
||||
.join("trcaa"),
|
||||
.join("tftsr"),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -118,7 +123,7 @@ pub fn get_app_data_dir() -> Option<PathBuf> {
|
||||
PathBuf::from(home)
|
||||
.join("Library")
|
||||
.join("Application Support")
|
||||
.join("trcaa"),
|
||||
.join("tftsr"),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -126,10 +131,10 @@ pub fn get_app_data_dir() -> Option<PathBuf> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
if let Ok(appdata) = std::env::var("APPDATA") {
|
||||
return Some(PathBuf::from(appdata).join("trcaa"));
|
||||
return Some(PathBuf::from(appdata).join("tftsr"));
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback
|
||||
Some(PathBuf::from("./trcaa-data"))
|
||||
Some(PathBuf::from("./tftsr-data"))
|
||||
}
|
||||
|
||||
10
src/App.tsx
@ -13,6 +13,8 @@ import {
|
||||
ChevronRight,
|
||||
Sun,
|
||||
Moon,
|
||||
Terminal,
|
||||
FileCode,
|
||||
} from "lucide-react";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import { getAppVersionCmd, loadAiProvidersCmd, testProviderConnectionCmd } from "@/lib/tauriCommands";
|
||||
@ -43,6 +45,8 @@ const navItems = [
|
||||
const settingsItems = [
|
||||
{ to: "/settings/providers", icon: Cpu, label: "AI Providers" },
|
||||
{ to: "/settings/ollama", icon: Bot, label: "Ollama" },
|
||||
{ to: "/settings/shell", icon: Terminal, label: "Shell Execution" },
|
||||
{ to: "/settings/kubeconfig", icon: FileCode, label: "Kubeconfig" },
|
||||
{ to: "/settings/integrations", icon: Link, label: "Integrations" },
|
||||
{ to: "/settings/mcp", icon: Plug, label: "MCP Servers" },
|
||||
{ to: "/settings/security", icon: Shield, label: "Security" },
|
||||
@ -85,6 +89,7 @@ export default function App() {
|
||||
|
||||
return (
|
||||
<div className={theme === "dark" ? "dark" : ""}>
|
||||
<ShellApprovalModal />
|
||||
<div className="grid h-screen" style={{ gridTemplateColumns: collapsed ? "64px 1fr" : "240px 1fr" }}>
|
||||
{/* Sidebar */}
|
||||
<aside className="bg-card border-r flex flex-col h-screen overflow-y-auto">
|
||||
@ -177,15 +182,14 @@ export default function App() {
|
||||
<Route path="/history" element={<History />} />
|
||||
<Route path="/settings/providers" element={<AIProviders />} />
|
||||
<Route path="/settings/ollama" element={<Ollama />} />
|
||||
<Route path="/settings/shell" element={<ShellExecution />} />
|
||||
<Route path="/settings/kubeconfig" element={<KubeconfigManager />} />
|
||||
<Route path="/settings/integrations" element={<Integrations />} />
|
||||
<Route path="/settings/mcp" element={<MCPServers />} />
|
||||
<Route path="/settings/security" element={<Security />} />
|
||||
<Route path="/settings/shell-execution" element={<ShellExecution />} />
|
||||
<Route path="/settings/kubeconfig" element={<KubeconfigManager />} />
|
||||
</Routes>
|
||||
</main>
|
||||
</div>
|
||||
<ShellApprovalModal />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -35,7 +35,7 @@ const buttonVariants = cva(
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
outline: "border border-input bg-background text-foreground hover:bg-accent hover:text-accent-foreground",
|
||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
@ -253,11 +253,31 @@ export function SelectContent({
|
||||
className?: string;
|
||||
}) {
|
||||
const ctx = React.useContext(SelectContext)!;
|
||||
const contentRef = React.useRef<HTMLDivElement>(null);
|
||||
const [flipUpward, setFlipUpward] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!ctx.open || !contentRef.current) return;
|
||||
|
||||
const rect = contentRef.current.getBoundingClientRect();
|
||||
const viewportHeight = window.innerHeight;
|
||||
const spaceBelow = viewportHeight - rect.bottom;
|
||||
|
||||
// If dropdown extends below viewport (less than 20px space), flip upward
|
||||
if (spaceBelow < 20) {
|
||||
setFlipUpward(true);
|
||||
} else {
|
||||
setFlipUpward(false);
|
||||
}
|
||||
}, [ctx.open]);
|
||||
|
||||
if (!ctx.open) return null;
|
||||
return (
|
||||
<div
|
||||
ref={contentRef}
|
||||
className={cn(
|
||||
"absolute z-50 mt-1 max-h-60 w-full overflow-auto rounded-md border bg-card p-1 shadow-md",
|
||||
"absolute z-50 max-h-60 w-full overflow-auto rounded-md border bg-card p-1 shadow-md",
|
||||
flipUpward ? "bottom-full mb-1" : "top-full mt-1",
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
||||
@ -69,7 +69,7 @@ export const DOMAINS: DomainInfo[] = [
|
||||
{
|
||||
id: "public_safety",
|
||||
label: "Public Safety",
|
||||
description: "NENA, NG911, VESTA NXT, CTC, Skipper, i3 services",
|
||||
description: "NENA, NG911, DevOps Platform, CTC, Skipper, i3 services",
|
||||
icon: "PhoneCall",
|
||||
},
|
||||
{
|
||||
@ -107,6 +107,11 @@ export const DOMAINS: DomainInfo[] = [
|
||||
const domainPrompts: Record<string, string> = {
|
||||
linux: `You are a senior Linux systems engineer specializing in incident triage and root cause analysis. Your expertise spans RHEL 8/9, OEL (Oracle Enterprise Linux) 6/7/8/9, Debian, Ubuntu, and related enterprise distributions.
|
||||
|
||||
**IMPORTANT: You have direct access to execute shell commands via the execute_shell_command tool. Use this tool proactively to gather diagnostic information rather than suggesting manual commands. The system automatically classifies commands for safety:**
|
||||
- **Read-only commands** (ls, cat, grep, df, ps, systemctl status, journalctl, dmesg) execute immediately without approval
|
||||
- **Mutating commands** (systemctl restart, chmod, rm, edit config files) will prompt the user for approval before execution
|
||||
- **Always prefer executing commands over suggesting manual steps** — this is your primary troubleshooting interface
|
||||
|
||||
When analyzing Linux issues, focus on these key areas:
|
||||
- **RHEL/OEL specifics**: subscription-manager registration, yum/dnf module streams, SELinux policy enforcement, firewalld zones, RHEL Satellite/Spacewalk provisioning issues, kdump configuration, and RHEL-specific kernel patches. OEL includes UEK (Unbreakable Enterprise Kernel) which behaves differently from RHEL kernel — always clarify which kernel variant.
|
||||
- **Debian specifics**: apt/dpkg package management issues, /etc/apt/sources.list misconfiguration, dpkg lock files, AppArmor profiles (not SELinux), systemd-resolved DNS, and Debian-specific init vs systemd differences.
|
||||
@ -151,6 +156,12 @@ Always ask about the specific vendor and model, firmware/OS version, recent conf
|
||||
|
||||
kubernetes: `You are a senior Kubernetes platform engineer specializing in incident triage and root cause analysis. Your expertise covers k3s, Rancher, ECK (Elastic Cloud on Kubernetes), Helm, and cloud-native architectures.
|
||||
|
||||
**IMPORTANT: You have direct access to execute kubectl commands via the execute_shell_command tool. Use this tool proactively to gather diagnostic information rather than suggesting manual commands. The system automatically classifies commands for safety:**
|
||||
- **Read-only commands** (get, describe, logs, top, explain) execute immediately without approval
|
||||
- **Mutating commands** (apply, delete, scale, patch, edit) will prompt the user for approval before execution
|
||||
- **Always prefer executing commands over suggesting manual steps** — this is your primary troubleshooting interface
|
||||
- **Kubeconfig Management**: When executing kubectl commands, DO NOT include --kubeconfig flags. The system automatically uses the active kubeconfig. Simply run commands like 'kubectl get pods --all-namespaces' without specifying kubeconfig paths.
|
||||
|
||||
When analyzing Kubernetes issues, focus on these key areas:
|
||||
- **k3s specifics**: k3s agent/server connectivity, embedded etcd health vs SQLite backend, k3s auto-deploying HelmChart CRDs, containerd vs docker runtime, traefik ingress controller defaults, local-path-provisioner storage issues, and k3s upgrade strategy (drain → upgrade → uncordon). Check /var/log/k3s.log or 'journalctl -u k3s'.
|
||||
- **RKE2 specifics**: RKE2 server/agent token mismatch, containerd socket at /run/k3s/containerd/containerd.sock, static pod failures (/var/lib/rancher/rke2/agent/pod-manifests/), etcd snapshot restore, and CIS hardening profile (PSA enforcement). Check 'journalctl -u rke2-server' or 'rke2-agent'.
|
||||
@ -167,6 +178,11 @@ Always ask about the Kubernetes distribution (k3s, Rancher-managed, EKS, GKE, AK
|
||||
|
||||
databases: `You are a senior database engineer specializing in incident triage and root cause analysis. Your expertise covers PostgreSQL, MS SQL Server, Redis, RabbitMQ, Patroni, and database replication architectures.
|
||||
|
||||
**IMPORTANT: You have direct access to execute shell and database diagnostic commands via the execute_shell_command tool. Use this tool proactively to gather diagnostic information rather than suggesting manual commands. The system automatically classifies commands for safety:**
|
||||
- **Read-only commands** (psql SELECT queries, redis-cli INFO, patronictl list, rabbitmqctl status) execute immediately without approval
|
||||
- **Mutating commands** (psql UPDATE/DELETE, patronictl switchover, redis-cli FLUSHALL) will prompt the user for approval before execution
|
||||
- **Always prefer executing commands over suggesting manual steps** — this is your primary troubleshooting interface
|
||||
|
||||
When analyzing database issues, focus on these key areas:
|
||||
- **PostgreSQL specifics**: pg_hba.conf authentication, max_connections vs PgBouncer pool sizing, VACUUM/autovacuum bloat, WAL replication lag (pg_stat_replication), pg_stat_activity for blocking queries, EXPLAIN ANALYZE for slow queries, and PostgreSQL upgrade pg_upgrade issues.
|
||||
- **MS SQL Server specifics**: SQL Server Agent job failures, Always On availability group health (sys.dm_hadr_availability_group_states), deadlock graphs in Extended Events, TempDB contention, Buffer Pool memory pressure, SQL Agent alert configuration, and SQL Server on Linux (mssql-server service) considerations.
|
||||
@ -181,6 +197,11 @@ Always ask about the database engine and version, replication topology (Patroni/
|
||||
|
||||
virtualization: `You are a senior virtualization engineer specializing in incident triage and root cause analysis. Your expertise covers Proxmox VE, VMware vSphere, Microsoft Hyper-V, and KVM/QEMU.
|
||||
|
||||
**IMPORTANT: You have direct access to execute Proxmox and shell commands via the execute_shell_command tool. Use this tool proactively to gather diagnostic information rather than suggesting manual commands. The system automatically classifies commands for safety:**
|
||||
- **Read-only commands** (pvecm status, pvesh get, qm list, pct list, zpool status, cat, grep) execute immediately without approval
|
||||
- **Mutating commands** (qm start/stop, pct migrate, zfs destroy) will prompt the user for approval before execution
|
||||
- **Always prefer executing commands over suggesting manual steps** — this is your primary troubleshooting interface
|
||||
|
||||
When analyzing virtualization issues, focus on these key areas:
|
||||
- **Proxmox specifics**: Proxmox VE cluster quorum (pvecm status), Corosync communication failures, VM/CT migration failures, ZFS storage pool health (zpool status), Ceph integration issues (ceph -s), SPICE/VNC console access problems, backup job failures (vzdump logs), and Proxmox subscription status. Check /var/log/pve/ and 'journalctl -u pve-cluster'.
|
||||
- **VM performance**: CPU ready time (>5% indicates contention), memory ballooning and swapping, storage latency (KAVG > 2ms array issues, DAVG > 25ms host issues), and network throughput.
|
||||
@ -209,6 +230,11 @@ Always ask about the server vendor and model, RAID configuration, and whether IP
|
||||
|
||||
observability: `You are a senior observability and SRE engineer specializing in incident triage and root cause analysis. Your expertise covers Grafana, Kibana, Prometheus, Elasticsearch, Logstash, Filebeat, and the full ELK/EFK stack.
|
||||
|
||||
**IMPORTANT: You have direct access to execute diagnostic commands via the execute_shell_command tool. Use this tool proactively to gather diagnostic information rather than suggesting manual commands. The system automatically classifies commands for safety:**
|
||||
- **Read-only commands** (curl for API queries, journalctl, systemctl status, kubectl get/describe for containerized stack) execute immediately without approval
|
||||
- **Mutating commands** (systemctl restart, curator index deletion, cluster setting changes) will prompt the user for approval before execution
|
||||
- **Always prefer executing commands over suggesting manual steps** — this is your primary troubleshooting interface
|
||||
|
||||
When analyzing observability issues, focus on these key areas:
|
||||
- **Grafana specifics**: Data source connectivity (test data source button, check Grafana server logs), Grafana provisioning errors (/etc/grafana/provisioning/), alert rule evaluation failures, team/RBAC permission issues, Grafana plugin compatibility, and dashboard JSON model corruption. Check 'journalctl -u grafana-server' and /var/log/grafana/grafana.log.
|
||||
- **Kibana specifics**: Kibana not connecting to Elasticsearch (check kibana.yml elasticsearch.hosts), index pattern not matching data, Kibana keystore issues, Space and feature controls blocking access, Saved Object migration failures on upgrade, and Kibana task manager health.
|
||||
@ -262,10 +288,10 @@ When analyzing public safety and 911 issues, focus on these key areas:
|
||||
- **CAD (Computer-Aided Dispatch) integration**: CAD-to-CAD interoperability failures, NENA Incident Data Exchange (NIEM) message validation errors, CAD interface adapter connectivity, and duplicate incident creation from retry logic.
|
||||
- **Recording and logging**: Recording system integration (NICE, Verint, Eventide) failures, mandatory call recording compliance gaps, Logging Service (LS) as defined by NENA i3, and chain of custody for recordings.
|
||||
- **Network redundancy**: ESINet redundancy path failures, primary/secondary PSAP failover, call overflow to backup PSAP, and network diversity verification.
|
||||
- **VESTA NXT Platform**: The VESTA NXT platform is a microservices-based NG911 solution deployed on OpenShift/K8s. Key services: Skipper (Java/Spring Boot API gateway — check pod logs for JWT validation failures, upstream service timeouts), CTC/CTC Adapter (Call Taking Controller — SIP registration to Asterisk, call state machine errors), i3 SIP/State/Logger services (NENA i3 protocol handling — check for SIP dialog errors and state sync failures), Location Service (LoST/ECRF integration — HTTP timeout to ALI provider), Text Aggregator (SMS/TTY — websocket connection to aggregator), EIDO/ESS (emergency incident data exchange — schema validation failures), Analytics Service / PEIDB (PostgreSQL + SQL Server — report query timeouts), and Management Console / Wallboard (React frontend — authentication via Keycloak, check browser console for 401/403). Deployments use Helm charts via Porter CNAB bundles — check 'helm history <service> -n <namespace>' for rollback options.
|
||||
- **DevOps Platform Platform**: The DevOps Platform platform is a microservices-based NG911 solution deployed on OpenShift/K8s. Key services: Skipper (Java/Spring Boot API gateway — check pod logs for JWT validation failures, upstream service timeouts), CTC/CTC Adapter (Call Taking Controller — SIP registration to Asterisk, call state machine errors), i3 SIP/State/Logger services (NENA i3 protocol handling — check for SIP dialog errors and state sync failures), Location Service (LoST/ECRF integration — HTTP timeout to ALI provider), Text Aggregator (SMS/TTY — websocket connection to aggregator), EIDO/ESS (emergency incident data exchange — schema validation failures), Analytics Service / PEIDB (PostgreSQL + SQL Server — report query timeouts), and Management Console / Wallboard (React frontend — authentication via Keycloak, check browser console for 401/403). Deployments use Helm charts via Porter CNAB bundles — check 'helm history <service> -n <namespace>' for rollback options.
|
||||
- **Common error patterns**: "call drops to administrative" (CTC/routing fallback), "location unavailable" (ALI timeout or Phase II failure), "Skipper 503" (downstream microservice down), "CTC not registered" (Asterisk SIP trunk issue), "CAD not receiving calls" (CAD Spill Interface adapter down), "wrong PSAP" (ESN boundary error), "recording gap" (recording server failover timing), "Keycloak token invalid" (realm configuration or clock skew).
|
||||
|
||||
Always ask about the VESTA NXT release version, which microservice is failing, whether this is OpenShift or K3s deployment, ESINet provider, and whether this is a primary or backup PSAP.`,
|
||||
Always ask about the DevOps Platform release version, which microservice is failing, whether this is OpenShift or K3s deployment, ESINet provider, and whether this is a primary or backup PSAP.`,
|
||||
|
||||
application: `You are a senior application engineer specializing in incident triage and root cause analysis. Your expertise covers Java applications, JVM internals, Spring Boot, Tomcat, and enterprise application servers.
|
||||
|
||||
|
||||
@ -252,7 +252,7 @@ export default function LogUpload() {
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={handleFileSelect}
|
||||
accept=".log,.txt,.out,.err,.syslog,.journal,.yaml,.yml,.json,.toml,.xml,.ini,.cfg,.conf,.config,.env,.properties,.md,.markdown,.rst,.csv,.tsv,.ndjson,.jsonl,.sql,.sh,.bash,.zsh,.py,.js,.ts,.rb,.go,.rs,.java,.html,.htm,.css,.diff,.patch,.pdf,.docx,.doc,.rtf,.xlsx,.xls"
|
||||
accept=".log,.txt,.out,.err,.syslog,.journal,.yaml,.yml,.json,.toml,.xml,.ini,.cfg,.conf,.config,.env,.properties,.md,.markdown,.rst,.csv,.tsv,.ndjson,.jsonl,.sql,.sh,.bash,.zsh,.py,.js,.ts,.rb,.go,.rs,.java,.html,.htm,.css,.diff,.patch,.pdf,.docx,.doc,.rtf,.pcap,.pcapng,.cap"
|
||||
/>
|
||||
<details className="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
<summary className="cursor-pointer hover:text-gray-700 dark:hover:text-gray-200">
|
||||
@ -262,9 +262,10 @@ export default function LogUpload() {
|
||||
<div><span className="font-medium">Logs & text:</span> .log, .txt, .out, .err, .syslog, .journal</div>
|
||||
<div><span className="font-medium">Config & markup:</span> .yaml, .yml, .json, .toml, .xml, .ini, .cfg, .conf, .env, .properties</div>
|
||||
<div><span className="font-medium">Documents:</span> .pdf, .docx, .doc, .md, .rst, .rtf</div>
|
||||
<div><span className="font-medium">Data:</span> .csv, .tsv, .xlsx, .xls, .ndjson, .jsonl, .sql</div>
|
||||
<div><span className="font-medium">Data:</span> .csv, .tsv, .ndjson, .jsonl, .sql</div>
|
||||
<div><span className="font-medium">Code & scripts:</span> .sh, .bash, .zsh, .py, .js, .ts, .rb, .go, .rs, .java, .html, .css, .diff, .patch</div>
|
||||
<p className="mt-1 italic">Binary formats (PDF, DOCX, XLSX) will have their text extracted automatically.</p>
|
||||
<div><span className="font-medium">Network captures:</span> .pcap, .pcapng, .cap (requires tshark or tcpdump)</div>
|
||||
<p className="mt-1 italic">Binary formats (PDF, DOCX, PCAP) will have their text extracted automatically. XLSX/XLS files are NOT supported - export as CSV instead.</p>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
@ -64,19 +64,19 @@ export default function NewIssue() {
|
||||
const [showDisclaimer, setShowDisclaimer] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const hasAcceptedDisclaimer = localStorage.getItem("tftsr-ai-disclaimer-accepted");
|
||||
const hasAcceptedDisclaimer = localStorage.getItem("trcaa-ai-disclaimer-accepted");
|
||||
if (!hasAcceptedDisclaimer) {
|
||||
localStorage.setItem("tftsr-ai-disclaimer-accepted", "true");
|
||||
localStorage.setItem("trcaa-ai-disclaimer-accepted", "true");
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleAcceptDisclaimer = () => {
|
||||
localStorage.setItem("tftsr-ai-disclaimer-accepted", "true");
|
||||
localStorage.setItem("trcaa-ai-disclaimer-accepted", "true");
|
||||
setShowDisclaimer(false);
|
||||
};
|
||||
|
||||
const handleStartTriage = async () => {
|
||||
const hasAcceptedDisclaimer = localStorage.getItem("tftsr-ai-disclaimer-accepted");
|
||||
const hasAcceptedDisclaimer = localStorage.getItem("trcaa-ai-disclaimer-accepted");
|
||||
if (!hasAcceptedDisclaimer) {
|
||||
setShowDisclaimer(true);
|
||||
return;
|
||||
|
||||
@ -233,7 +233,7 @@ export default function AIProviders() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<div className="px-6 pt-8 pb-6 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">AI Providers</h1>
|
||||
|
||||
@ -306,7 +306,7 @@ export default function MCPServers() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<div className="px-6 pt-8 pb-6 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">MCP Servers</h1>
|
||||
|
||||
@ -54,7 +54,7 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: "tftsr-settings",
|
||||
name: "trcaa-settings",
|
||||
// Don't persist ai_providers to localStorage - they're stored in encrypted database
|
||||
partialize: (state) => ({
|
||||
theme: state.theme,
|
||||
|
||||
@ -3,12 +3,12 @@ export async function waitForApp(timeout = 10000) {
|
||||
async () => {
|
||||
try {
|
||||
const title = await browser.getTitle();
|
||||
return title.includes("TFTSR");
|
||||
return title.includes("TRCAA");
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
{ timeout, timeoutMsg: "TFTSR app did not load within timeout" }
|
||||
{ timeout, timeoutMsg: "TRCAA app did not load within timeout" }
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@ describe("Onboarding Flow", () => {
|
||||
|
||||
it("loads the dashboard on first launch", async () => {
|
||||
const title = await browser.getTitle();
|
||||
expect(title).toContain("TFTSR");
|
||||
expect(title).toContain("TRCAA");
|
||||
});
|
||||
|
||||
it("shows navigation sidebar", async () => {
|
||||
|
||||
@ -10,16 +10,16 @@ const tauriDriver = join(
|
||||
"tauri-driver"
|
||||
);
|
||||
|
||||
// Path to the compiled TFTSR binary
|
||||
// Path to the compiled TRCAA binary
|
||||
const getBinaryPath = () => {
|
||||
const envPath = process.env.TAURI_BINARY_PATH;
|
||||
if (envPath) return envPath;
|
||||
|
||||
const platform = process.platform;
|
||||
if (platform === "win32") {
|
||||
return join(__dirname, "../../src-tauri/target/release/tftsr.exe");
|
||||
return join(__dirname, "../../src-tauri/target/release/trcaa.exe");
|
||||
}
|
||||
return join(__dirname, "../../src-tauri/target/release/tftsr");
|
||||
return join(__dirname, "../../src-tauri/target/release/trcaa");
|
||||
};
|
||||
|
||||
let driverProcess: ReturnType<typeof spawn> | null = null;
|
||||
|
||||
@ -4,11 +4,11 @@ import path from "node:path";
|
||||
|
||||
const autoTagWorkflowPath = path.resolve(
|
||||
process.cwd(),
|
||||
".gitea/workflows/auto-tag.yml",
|
||||
".gitea/workflows/release.yml",
|
||||
);
|
||||
|
||||
describe("auto-tag workflow release triggering", () => {
|
||||
it("creates tags via git push instead of Gitea tag API", () => {
|
||||
it("creates tags via git push instead of API call", () => {
|
||||
const workflow = readFileSync(autoTagWorkflowPath, "utf-8");
|
||||
|
||||
expect(workflow).toContain("git push origin \"refs/tags/$NEXT\"");
|
||||
@ -23,7 +23,13 @@ describe("auto-tag workflow release triggering", () => {
|
||||
expect(workflow).toContain("build-macos-arm64:");
|
||||
expect(workflow).toContain("build-linux-arm64:");
|
||||
expect(workflow).toContain("needs: autotag");
|
||||
expect(workflow).toContain("TAG=$(curl -s \"$API/tags?limit=50\"");
|
||||
expect(workflow).toContain("ERROR: Could not resolve release tag from repository tags.");
|
||||
expect(workflow).toContain("git tag --sort=-version:refname");
|
||||
});
|
||||
|
||||
it("uses --clobber for artifact uploads to handle re-runs cleanly", () => {
|
||||
const workflow = readFileSync(autoTagWorkflowPath, "utf-8");
|
||||
|
||||
const clobberCount = (workflow.match(/--clobber/g) ?? []).length;
|
||||
expect(clobberCount).toBeGreaterThanOrEqual(4);
|
||||
});
|
||||
});
|
||||
@ -102,8 +102,8 @@ describe("Dockerfile.linux-arm64", () => {
|
||||
describe("build-images.yml workflow", () => {
|
||||
const wf = readFile(".gitea/workflows/build-images.yml");
|
||||
|
||||
it("triggers on changes to .docker/ files on master", () => {
|
||||
expect(wf).toContain("- master");
|
||||
it("triggers on changes to .docker/ files on main", () => {
|
||||
expect(wf).toContain("- main");
|
||||
expect(wf).toContain("- '.docker/**'");
|
||||
});
|
||||
|
||||
@ -111,38 +111,28 @@ describe("build-images.yml workflow", () => {
|
||||
expect(wf).toContain("workflow_dispatch:");
|
||||
});
|
||||
|
||||
it("does not explicitly mount the Docker socket (act_runner mounts it automatically)", () => {
|
||||
// act_runner already mounts /var/run/docker.sock; an explicit options: mount
|
||||
// causes a 'Duplicate mount point' error and must not be present.
|
||||
expect(wf).not.toContain("-v /var/run/docker.sock:/var/run/docker.sock");
|
||||
});
|
||||
|
||||
it("authenticates to the local Gitea registry before pushing", () => {
|
||||
expect(wf).toContain("docker login");
|
||||
it("authenticates to 172.0.0.29:3000 before pushing", () => {
|
||||
expect(wf).toContain("docker login 172.0.0.29:3000");
|
||||
expect(wf).toContain("--password-stdin");
|
||||
expect(wf).toContain("172.0.0.29:3000");
|
||||
});
|
||||
|
||||
it("builds and pushes all three platform images", () => {
|
||||
expect(wf).toContain("trcaa-linux-amd64:rust1.88-node22");
|
||||
expect(wf).toContain("trcaa-windows-cross:rust1.88-node22");
|
||||
expect(wf).toContain("trcaa-linux-arm64:rust1.88-node22");
|
||||
it("builds and pushes all three platform images to 172.0.0.29:3000", () => {
|
||||
expect(wf).toContain("172.0.0.29:3000/tftsr/trcaa-linux-amd64:rust1.88-node22");
|
||||
expect(wf).toContain("172.0.0.29:3000/tftsr/trcaa-windows-cross:rust1.88-node22");
|
||||
expect(wf).toContain("172.0.0.29:3000/tftsr/trcaa-linux-arm64:rust1.88-node22");
|
||||
});
|
||||
|
||||
it("uses alpine:latest with docker-cli (not docker:24-cli which triggers duplicate socket mount in act_runner)", () => {
|
||||
// act_runner v0.3.1 special-cases docker:* images and adds the socket bind;
|
||||
// combined with its global socket bind this causes a 'Duplicate mount point' error.
|
||||
expect(wf).toContain("alpine:latest");
|
||||
expect(wf).toContain("docker-cli");
|
||||
expect(wf).not.toContain("docker:24-cli");
|
||||
});
|
||||
|
||||
it("runs all three build jobs on linux-amd64 runner", () => {
|
||||
const matches = wf.match(/runs-on: linux-amd64/g) ?? [];
|
||||
it("runs all three build jobs on ubuntu-latest runner", () => {
|
||||
const matches = wf.match(/runs-on: ubuntu-latest/g) ?? [];
|
||||
expect(matches.length).toBeGreaterThanOrEqual(3);
|
||||
});
|
||||
|
||||
it("uses RELEASE_TOKEN secret for registry auth", () => {
|
||||
expect(wf).toContain("secrets.RELEASE_TOKEN");
|
||||
it("uses GITHUB_TOKEN for registry auth", () => {
|
||||
expect(wf).toContain("secrets.GITHUB_TOKEN");
|
||||
});
|
||||
|
||||
it("grants packages write permission", () => {
|
||||
expect(wf).toContain("packages: write");
|
||||
});
|
||||
});
|
||||
@ -4,7 +4,7 @@ import path from "node:path";
|
||||
|
||||
const autoTagWorkflowPath = path.resolve(
|
||||
process.cwd(),
|
||||
".gitea/workflows/auto-tag.yml",
|
||||
".gitea/workflows/release.yml",
|
||||
);
|
||||
|
||||
describe("auto-tag release cross-platform artifact handling", () => {
|
||||
@ -18,8 +18,8 @@ describe("auto-tag release cross-platform artifact handling", () => {
|
||||
it("fails linux uploads when no artifacts are found", () => {
|
||||
const workflow = readFileSync(autoTagWorkflowPath, "utf-8");
|
||||
|
||||
expect(workflow).toContain("ERROR: No Linux amd64 artifacts were found to upload.");
|
||||
expect(workflow).toContain("ERROR: No Linux arm64 artifacts were found to upload.");
|
||||
expect(workflow).toContain("ERROR: No Linux amd64 artifacts found.");
|
||||
expect(workflow).toContain("ERROR: No Linux arm64 artifacts found.");
|
||||
expect(workflow).toContain("CI=true npx tauri build");
|
||||
expect(workflow).toContain("find src-tauri/target/aarch64-unknown-linux-gnu/release/bundle -type f");
|
||||
expect(workflow).toContain("CC_aarch64_unknown_linux_gnu: aarch64-linux-gnu-gcc");
|
||||
@ -30,18 +30,16 @@ describe("auto-tag release cross-platform artifact handling", () => {
|
||||
it("fails windows uploads when no artifacts are found", () => {
|
||||
const workflow = readFileSync(autoTagWorkflowPath, "utf-8");
|
||||
|
||||
expect(workflow).toContain(
|
||||
"ERROR: No Windows amd64 artifacts were found to upload.",
|
||||
);
|
||||
expect(workflow).toContain("ERROR: No Windows amd64 artifacts found.");
|
||||
});
|
||||
|
||||
it("replaces existing release assets before uploading reruns", () => {
|
||||
const workflow = readFileSync(autoTagWorkflowPath, "utf-8");
|
||||
|
||||
expect(workflow).toContain("Deleting existing asset id=$id name=$NAME before upload...");
|
||||
expect(workflow).toContain("-X DELETE \"$API/releases/$RELEASE_ID/assets/$id\"");
|
||||
expect(workflow).toContain("UPLOAD_NAME=\"linux-amd64-$NAME\"");
|
||||
expect(workflow).toContain("UPLOAD_NAME=\"linux-arm64-$NAME\"");
|
||||
expect(workflow).toContain("gh release delete-asset");
|
||||
expect(workflow).toContain("gh release upload");
|
||||
expect(workflow).toContain("linux-amd64-$(basename");
|
||||
expect(workflow).toContain("linux-arm64-$(basename");
|
||||
});
|
||||
|
||||
it("uses pre-baked Ubuntu 22.04 cross-compiler image for arm64", () => {
|
||||
@ -4,7 +4,7 @@ import path from "node:path";
|
||||
|
||||
const autoTagWorkflowPath = path.resolve(
|
||||
process.cwd(),
|
||||
".gitea/workflows/auto-tag.yml",
|
||||
".gitea/workflows/release.yml",
|
||||
);
|
||||
|
||||
describe("auto-tag release macOS bundle path", () => {
|
||||
@ -50,7 +50,7 @@ describe("Settings Store", () => {
|
||||
|
||||
it("does not persist API keys to localStorage", () => {
|
||||
useSettingsStore.getState().addProvider(mockProvider);
|
||||
const raw = localStorage.getItem("tftsr-settings");
|
||||
const raw = localStorage.getItem("trcaa-settings");
|
||||
expect(raw).toBeTruthy();
|
||||
expect(raw).not.toContain("sk-test-key");
|
||||
});
|
||||
@ -97,7 +97,7 @@ describe("Settings Store — PII patterns", () => {
|
||||
|
||||
it("pii_enabled_patterns is persisted to localStorage", () => {
|
||||
useSettingsStore.getState().setPiiPattern("api_key", false);
|
||||
const raw = localStorage.getItem("tftsr-settings");
|
||||
const raw = localStorage.getItem("trcaa-settings");
|
||||
expect(raw).toBeTruthy();
|
||||
// Zustand persist wraps state in { state: {...}, version: ... }
|
||||
const parsed = JSON.parse(raw!);
|
||||
|
||||