Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ae3e790787 |
@@ -5,6 +5,6 @@ runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install syft
|
||||
uses: anchore/sbom-action/download-syft@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
|
||||
uses: anchore/sbom-action/download-syft@f325610c9f50a54015d37c8d16cb3b0e2c8f4de0 # v0.18.0
|
||||
with:
|
||||
syft-version: "v1.26.1"
|
||||
syft-version: "v1.20.0"
|
||||
|
||||
@@ -4,7 +4,7 @@ description: |
|
||||
inputs:
|
||||
version:
|
||||
description: "The Go version to use."
|
||||
default: "1.25.8"
|
||||
default: "1.25.7"
|
||||
use-cache:
|
||||
description: "Whether to use the cache."
|
||||
default: "true"
|
||||
|
||||
+98
-26
@@ -181,7 +181,7 @@ jobs:
|
||||
echo "LINT_CACHE_DIR=$dir" >> "$GITHUB_ENV"
|
||||
|
||||
- name: golangci-lint cache
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: |
|
||||
${{ env.LINT_CACHE_DIR }}
|
||||
@@ -1316,50 +1316,122 @@ jobs:
|
||||
"${IMAGE}"
|
||||
done
|
||||
|
||||
- name: Resolve Docker image digests for attestation
|
||||
id: docker_digests
|
||||
if: github.ref == 'refs/heads/main'
|
||||
continue-on-error: true
|
||||
env:
|
||||
IMAGE_BASE: ghcr.io/coder/coder-preview
|
||||
BUILD_TAG: ${{ steps.build-docker.outputs.tag }}
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
main_digest=$(docker buildx imagetools inspect --raw "${IMAGE_BASE}:main" | sha256sum | awk '{print "sha256:"$1}')
|
||||
echo "main_digest=${main_digest}" >> "$GITHUB_OUTPUT"
|
||||
latest_digest=$(docker buildx imagetools inspect --raw "${IMAGE_BASE}:latest" | sha256sum | awk '{print "sha256:"$1}')
|
||||
echo "latest_digest=${latest_digest}" >> "$GITHUB_OUTPUT"
|
||||
version_digest=$(docker buildx imagetools inspect --raw "${IMAGE_BASE}:${BUILD_TAG}" | sha256sum | awk '{print "sha256:"$1}')
|
||||
echo "version_digest=${version_digest}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# GitHub attestation provides SLSA provenance for the Docker images, establishing a verifiable
|
||||
# record that these images were built in GitHub Actions with specific inputs and environment.
|
||||
# This complements our existing cosign attestations which focus on SBOMs.
|
||||
#
|
||||
# We attest each tag separately to ensure all tags have proper provenance records.
|
||||
# TODO: Consider refactoring these steps to use a matrix strategy or composite action to reduce duplication
|
||||
# while maintaining the required functionality for each tag.
|
||||
- name: GitHub Attestation for Docker image
|
||||
id: attest_main
|
||||
if: github.ref == 'refs/heads/main' && steps.docker_digests.outputs.main_digest != ''
|
||||
if: github.ref == 'refs/heads/main'
|
||||
continue-on-error: true
|
||||
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
|
||||
with:
|
||||
subject-name: ghcr.io/coder/coder-preview
|
||||
subject-digest: ${{ steps.docker_digests.outputs.main_digest }}
|
||||
subject-name: "ghcr.io/coder/coder-preview:main"
|
||||
predicate-type: "https://slsa.dev/provenance/v1"
|
||||
predicate: |
|
||||
{
|
||||
"buildType": "https://github.com/actions/runner-images/",
|
||||
"builder": {
|
||||
"id": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
},
|
||||
"invocation": {
|
||||
"configSource": {
|
||||
"uri": "git+https://github.com/${{ github.repository }}@${{ github.ref }}",
|
||||
"digest": {
|
||||
"sha1": "${{ github.sha }}"
|
||||
},
|
||||
"entryPoint": ".github/workflows/ci.yaml"
|
||||
},
|
||||
"environment": {
|
||||
"github_workflow": "${{ github.workflow }}",
|
||||
"github_run_id": "${{ github.run_id }}"
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"buildInvocationID": "${{ github.run_id }}",
|
||||
"completeness": {
|
||||
"environment": true,
|
||||
"materials": true
|
||||
}
|
||||
}
|
||||
}
|
||||
push-to-registry: true
|
||||
|
||||
- name: GitHub Attestation for Docker image (latest tag)
|
||||
id: attest_latest
|
||||
if: github.ref == 'refs/heads/main' && steps.docker_digests.outputs.latest_digest != ''
|
||||
if: github.ref == 'refs/heads/main'
|
||||
continue-on-error: true
|
||||
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
|
||||
with:
|
||||
subject-name: ghcr.io/coder/coder-preview
|
||||
subject-digest: ${{ steps.docker_digests.outputs.latest_digest }}
|
||||
subject-name: "ghcr.io/coder/coder-preview:latest"
|
||||
predicate-type: "https://slsa.dev/provenance/v1"
|
||||
predicate: |
|
||||
{
|
||||
"buildType": "https://github.com/actions/runner-images/",
|
||||
"builder": {
|
||||
"id": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
},
|
||||
"invocation": {
|
||||
"configSource": {
|
||||
"uri": "git+https://github.com/${{ github.repository }}@${{ github.ref }}",
|
||||
"digest": {
|
||||
"sha1": "${{ github.sha }}"
|
||||
},
|
||||
"entryPoint": ".github/workflows/ci.yaml"
|
||||
},
|
||||
"environment": {
|
||||
"github_workflow": "${{ github.workflow }}",
|
||||
"github_run_id": "${{ github.run_id }}"
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"buildInvocationID": "${{ github.run_id }}",
|
||||
"completeness": {
|
||||
"environment": true,
|
||||
"materials": true
|
||||
}
|
||||
}
|
||||
}
|
||||
push-to-registry: true
|
||||
|
||||
- name: GitHub Attestation for version-specific Docker image
|
||||
id: attest_version
|
||||
if: github.ref == 'refs/heads/main' && steps.docker_digests.outputs.version_digest != ''
|
||||
if: github.ref == 'refs/heads/main'
|
||||
continue-on-error: true
|
||||
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
|
||||
with:
|
||||
subject-name: ghcr.io/coder/coder-preview
|
||||
subject-digest: ${{ steps.docker_digests.outputs.version_digest }}
|
||||
subject-name: "ghcr.io/coder/coder-preview:${{ steps.build-docker.outputs.tag }}"
|
||||
predicate-type: "https://slsa.dev/provenance/v1"
|
||||
predicate: |
|
||||
{
|
||||
"buildType": "https://github.com/actions/runner-images/",
|
||||
"builder": {
|
||||
"id": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
},
|
||||
"invocation": {
|
||||
"configSource": {
|
||||
"uri": "git+https://github.com/${{ github.repository }}@${{ github.ref }}",
|
||||
"digest": {
|
||||
"sha1": "${{ github.sha }}"
|
||||
},
|
||||
"entryPoint": ".github/workflows/ci.yaml"
|
||||
},
|
||||
"environment": {
|
||||
"github_workflow": "${{ github.workflow }}",
|
||||
"github_run_id": "${{ github.run_id }}"
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"buildInvocationID": "${{ github.run_id }}",
|
||||
"completeness": {
|
||||
"environment": true,
|
||||
"materials": true
|
||||
}
|
||||
}
|
||||
}
|
||||
push-to-registry: true
|
||||
|
||||
# Report attestation failures but don't fail the workflow
|
||||
|
||||
@@ -95,7 +95,7 @@ jobs:
|
||||
AWS_DOGFOOD_DEPLOY_REGION: ${{ vars.AWS_DOGFOOD_DEPLOY_REGION }}
|
||||
|
||||
- name: Set up Flux CLI
|
||||
uses: fluxcd/flux2/action@871be9b40d53627786d3a3835a3ddba1e3234bd2 # v2.8.3
|
||||
uses: fluxcd/flux2/action@8454b02a32e48d775b9f563cb51fdcb1787b5b93 # v2.7.5
|
||||
with:
|
||||
# Keep this and the github action up to date with the version of flux installed in dogfood cluster
|
||||
version: "2.8.2"
|
||||
|
||||
@@ -240,7 +240,6 @@ jobs:
|
||||
- name: Create Coder Task for Documentation Check
|
||||
if: steps.check-secrets.outputs.skip != 'true'
|
||||
id: create_task
|
||||
continue-on-error: true
|
||||
uses: ./.github/actions/create-task-action
|
||||
with:
|
||||
coder-url: ${{ secrets.DOC_CHECK_CODER_URL }}
|
||||
@@ -255,21 +254,8 @@ jobs:
|
||||
github-issue-url: ${{ steps.determine-context.outputs.pr_url }}
|
||||
comment-on-issue: false
|
||||
|
||||
- name: Handle Task Creation Failure
|
||||
if: steps.check-secrets.outputs.skip != 'true' && steps.create_task.outcome != 'success'
|
||||
run: |
|
||||
{
|
||||
echo "## Documentation Check Task"
|
||||
echo ""
|
||||
echo "⚠️ The external Coder task service was unavailable, so this"
|
||||
echo "advisory documentation check did not run."
|
||||
echo ""
|
||||
echo "Maintainers can rerun the workflow or trigger it manually"
|
||||
echo "after the service recovers."
|
||||
} >> "${GITHUB_STEP_SUMMARY}"
|
||||
|
||||
- name: Write Task Info
|
||||
if: steps.check-secrets.outputs.skip != 'true' && steps.create_task.outcome == 'success'
|
||||
if: steps.check-secrets.outputs.skip != 'true'
|
||||
env:
|
||||
TASK_CREATED: ${{ steps.create_task.outputs.task-created }}
|
||||
TASK_NAME: ${{ steps.create_task.outputs.task-name }}
|
||||
@@ -287,7 +273,7 @@ jobs:
|
||||
} >> "${GITHUB_STEP_SUMMARY}"
|
||||
|
||||
- name: Wait for Task Completion
|
||||
if: steps.check-secrets.outputs.skip != 'true' && steps.create_task.outcome == 'success'
|
||||
if: steps.check-secrets.outputs.skip != 'true'
|
||||
id: wait_task
|
||||
env:
|
||||
TASK_NAME: ${{ steps.create_task.outputs.task-name }}
|
||||
@@ -377,7 +363,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Fetch Task Logs
|
||||
if: always() && steps.check-secrets.outputs.skip != 'true' && steps.create_task.outcome == 'success'
|
||||
if: always() && steps.check-secrets.outputs.skip != 'true'
|
||||
env:
|
||||
TASK_NAME: ${{ steps.create_task.outputs.task-name }}
|
||||
run: |
|
||||
@@ -390,7 +376,7 @@ jobs:
|
||||
echo "::endgroup::"
|
||||
|
||||
- name: Cleanup Task
|
||||
if: always() && steps.check-secrets.outputs.skip != 'true' && steps.create_task.outcome == 'success'
|
||||
if: always() && steps.check-secrets.outputs.skip != 'true'
|
||||
env:
|
||||
TASK_NAME: ${{ steps.create_task.outputs.task-name }}
|
||||
run: |
|
||||
@@ -404,7 +390,6 @@ jobs:
|
||||
- name: Write Final Summary
|
||||
if: always() && steps.check-secrets.outputs.skip != 'true'
|
||||
env:
|
||||
CREATE_TASK_OUTCOME: ${{ steps.create_task.outcome }}
|
||||
TASK_NAME: ${{ steps.create_task.outputs.task-name }}
|
||||
TASK_MESSAGE: ${{ steps.wait_task.outputs.task_message }}
|
||||
RESULT_URI: ${{ steps.wait_task.outputs.result_uri }}
|
||||
@@ -415,15 +400,10 @@ jobs:
|
||||
echo "---"
|
||||
echo "### Result"
|
||||
echo ""
|
||||
if [[ "${CREATE_TASK_OUTCOME}" == "success" ]]; then
|
||||
echo "**Status:** ${TASK_MESSAGE:-Task completed}"
|
||||
if [[ -n "${RESULT_URI}" ]]; then
|
||||
echo "**Comment:** ${RESULT_URI}"
|
||||
fi
|
||||
echo ""
|
||||
echo "Task \`${TASK_NAME}\` has been cleaned up."
|
||||
else
|
||||
echo "**Status:** Skipped because the external Coder task"
|
||||
echo "service was unavailable."
|
||||
echo "**Status:** ${TASK_MESSAGE:-Task completed}"
|
||||
if [[ -n "${RESULT_URI}" ]]; then
|
||||
echo "**Comment:** ${RESULT_URI}"
|
||||
fi
|
||||
echo ""
|
||||
echo "Task \`${TASK_NAME}\` has been cleaned up."
|
||||
} >> "${GITHUB_STEP_SUMMARY}"
|
||||
|
||||
@@ -4,7 +4,9 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- "release/2.[0-9]+"
|
||||
# This event reads the workflow from the default branch (main), not the
|
||||
# release branch. No cherry-pick needed.
|
||||
# https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#release
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
@@ -13,13 +15,12 @@ permissions:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
# Queue rather than cancel so back-to-back pushes to main don't cancel the first sync.
|
||||
cancel-in-progress: false
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
sync-main:
|
||||
name: Sync issues to next Linear release
|
||||
if: github.event_name == 'push' && github.ref_name == 'main'
|
||||
sync:
|
||||
name: Sync issues to Linear release
|
||||
if: github.event_name == 'push'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
@@ -27,84 +28,18 @@ jobs:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Detect next release version
|
||||
id: version
|
||||
# Find the highest release/2.X branch (exact pattern, no suffixes like
|
||||
# release/2.31_hotfix) and derive the next minor version for the release
|
||||
# currently in development on main.
|
||||
run: |
|
||||
LATEST_MINOR=$(git branch -r | grep -E '^\s*origin/release/2\.[0-9]+$' | \
|
||||
sed 's/.*release\/2\.//' | sort -n | tail -1)
|
||||
if [ -z "$LATEST_MINOR" ]; then
|
||||
echo "No release branch found, skipping sync."
|
||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
echo "version=2.$((LATEST_MINOR + 1))" >> "$GITHUB_OUTPUT"
|
||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Sync issues
|
||||
id: sync
|
||||
if: steps.version.outputs.skip != 'true'
|
||||
uses: linear/linear-release-action@755d50b5adb7dd42b976ee9334952745d62ceb2d # v0.6.0
|
||||
uses: linear/linear-release-action@5cbaabc187ceb63eee9d446e62e68e5c29a03ae8 # v0.5.0
|
||||
with:
|
||||
access_key: ${{ secrets.LINEAR_ACCESS_KEY }}
|
||||
command: sync
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
timeout: 300
|
||||
|
||||
sync-release-branch:
|
||||
name: Sync backports to Linear release
|
||||
if: github.event_name == 'push' && startsWith(github.ref_name, 'release/')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Extract release version
|
||||
id: version
|
||||
# The trigger only allows exact release/2.X branch names.
|
||||
run: |
|
||||
echo "version=${GITHUB_REF_NAME#release/}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Sync issues
|
||||
id: sync
|
||||
uses: linear/linear-release-action@755d50b5adb7dd42b976ee9334952745d62ceb2d # v0.6.0
|
||||
with:
|
||||
access_key: ${{ secrets.LINEAR_ACCESS_KEY }}
|
||||
command: sync
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
timeout: 300
|
||||
|
||||
code-freeze:
|
||||
name: Move Linear release to Code Freeze
|
||||
needs: sync-release-branch
|
||||
if: >
|
||||
github.event_name == 'push' &&
|
||||
startsWith(github.ref_name, 'release/') &&
|
||||
github.event.created == true
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Extract release version
|
||||
id: version
|
||||
run: |
|
||||
echo "version=${GITHUB_REF_NAME#release/}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Move to Code Freeze
|
||||
id: update
|
||||
uses: linear/linear-release-action@755d50b5adb7dd42b976ee9334952745d62ceb2d # v0.6.0
|
||||
with:
|
||||
access_key: ${{ secrets.LINEAR_ACCESS_KEY }}
|
||||
command: update
|
||||
stage: Code Freeze
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
timeout: 300
|
||||
- name: Print release URL
|
||||
if: steps.sync.outputs.release-url
|
||||
run: echo "Synced to $RELEASE_URL"
|
||||
env:
|
||||
RELEASE_URL: ${{ steps.sync.outputs.release-url }}
|
||||
|
||||
complete:
|
||||
name: Complete Linear release
|
||||
@@ -115,29 +50,16 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Extract release version
|
||||
id: version
|
||||
# Strip "v" prefix and patch: "v2.31.0" -> "2.31". Also detect whether
|
||||
# this is a minor release (v*.*.0) — patch releases (v2.31.1, v2.31.2,
|
||||
# ...) are grouped into the same Linear release and must not re-complete
|
||||
# it after it has already shipped.
|
||||
run: |
|
||||
VERSION=$(echo "$TAG" | sed 's/^v//' | cut -d. -f1,2)
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
if [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.0$ ]]; then
|
||||
echo "is_minor=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "is_minor=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
env:
|
||||
TAG: ${{ github.event.release.tag_name }}
|
||||
|
||||
- name: Complete release
|
||||
id: complete
|
||||
if: steps.version.outputs.is_minor == 'true'
|
||||
uses: linear/linear-release-action@755d50b5adb7dd42b976ee9334952745d62ceb2d # v0.6.0
|
||||
uses: linear/linear-release-action@5cbaabc187ceb63eee9d446e62e68e5c29a03ae8 # v0
|
||||
with:
|
||||
access_key: ${{ secrets.LINEAR_ACCESS_KEY }}
|
||||
command: complete
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
timeout: 300
|
||||
version: ${{ github.event.release.tag_name }}
|
||||
|
||||
- name: Print release URL
|
||||
if: steps.complete.outputs.release-url
|
||||
run: echo "Completed $RELEASE_URL"
|
||||
env:
|
||||
RELEASE_URL: ${{ steps.complete.outputs.release-url }}
|
||||
|
||||
+107
-31
@@ -302,7 +302,6 @@ jobs:
|
||||
|
||||
# This uses OIDC authentication, so no auth variables are required.
|
||||
- name: Build base Docker image via depot.dev
|
||||
id: build_base_image
|
||||
if: steps.image-base-tag.outputs.tag != ''
|
||||
uses: depot/build-push-action@5f3b3c2e5a00f0093de47f657aeaefcedff27d18 # v1.17.0
|
||||
with:
|
||||
@@ -350,14 +349,48 @@ jobs:
|
||||
env:
|
||||
IMAGE_TAG: ${{ steps.image-base-tag.outputs.tag }}
|
||||
|
||||
# GitHub attestation provides SLSA provenance for Docker images, establishing a verifiable
|
||||
# record that these images were built in GitHub Actions with specific inputs and environment.
|
||||
# This complements our existing cosign attestations (which focus on SBOMs) by adding
|
||||
# GitHub-specific build provenance to enhance our supply chain security.
|
||||
#
|
||||
# TODO: Consider refactoring these attestation steps to use a matrix strategy or composite action
|
||||
# to reduce duplication while maintaining the required functionality for each distinct image tag.
|
||||
- name: GitHub Attestation for Base Docker image
|
||||
id: attest_base
|
||||
if: ${{ !inputs.dry_run && steps.build_base_image.outputs.digest != '' }}
|
||||
if: ${{ !inputs.dry_run && steps.image-base-tag.outputs.tag != '' }}
|
||||
continue-on-error: true
|
||||
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
|
||||
with:
|
||||
subject-name: ghcr.io/coder/coder-base
|
||||
subject-digest: ${{ steps.build_base_image.outputs.digest }}
|
||||
subject-name: ${{ steps.image-base-tag.outputs.tag }}
|
||||
predicate-type: "https://slsa.dev/provenance/v1"
|
||||
predicate: |
|
||||
{
|
||||
"buildType": "https://github.com/actions/runner-images/",
|
||||
"builder": {
|
||||
"id": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
},
|
||||
"invocation": {
|
||||
"configSource": {
|
||||
"uri": "git+https://github.com/${{ github.repository }}@${{ github.ref }}",
|
||||
"digest": {
|
||||
"sha1": "${{ github.sha }}"
|
||||
},
|
||||
"entryPoint": ".github/workflows/release.yaml"
|
||||
},
|
||||
"environment": {
|
||||
"github_workflow": "${{ github.workflow }}",
|
||||
"github_run_id": "${{ github.run_id }}"
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"buildInvocationID": "${{ github.run_id }}",
|
||||
"completeness": {
|
||||
"environment": true,
|
||||
"materials": true
|
||||
}
|
||||
}
|
||||
}
|
||||
push-to-registry: true
|
||||
|
||||
- name: Build Linux Docker images
|
||||
@@ -380,6 +413,7 @@ jobs:
|
||||
# being pushed so will automatically push them.
|
||||
make push/build/coder_"$version"_linux.tag
|
||||
|
||||
# Save multiarch image tag for attestation
|
||||
multiarch_image="$(./scripts/image_tag.sh)"
|
||||
echo "multiarch_image=${multiarch_image}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
@@ -390,14 +424,12 @@ jobs:
|
||||
# version in the repo, also create a multi-arch image as ":latest" and
|
||||
# push it
|
||||
if [[ "$(git tag | grep '^v' | grep -vE '(rc|dev|-|\+|\/)' | sort -r --version-sort | head -n1)" == "v$(./scripts/version.sh)" ]]; then
|
||||
latest_target="$(./scripts/image_tag.sh --version latest)"
|
||||
# shellcheck disable=SC2046
|
||||
./scripts/build_docker_multiarch.sh \
|
||||
--push \
|
||||
--target "${latest_target}" \
|
||||
--target "$(./scripts/image_tag.sh --version latest)" \
|
||||
$(cat build/coder_"$version"_linux_{amd64,arm64,armv7}.tag)
|
||||
echo "created_latest_tag=true" >> "$GITHUB_OUTPUT"
|
||||
echo "latest_target=${latest_target}" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "created_latest_tag=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
@@ -418,6 +450,7 @@ jobs:
|
||||
echo "Generating SBOM for multi-arch image: ${MULTIARCH_IMAGE}"
|
||||
syft "${MULTIARCH_IMAGE}" -o spdx-json > "coder_${VERSION}_sbom.spdx.json"
|
||||
|
||||
# Attest SBOM to multi-arch image
|
||||
echo "Attesting SBOM to multi-arch image: ${MULTIARCH_IMAGE}"
|
||||
cosign clean --force=true "${MULTIARCH_IMAGE}"
|
||||
cosign attest --type spdxjson \
|
||||
@@ -439,42 +472,85 @@ jobs:
|
||||
"${latest_tag}"
|
||||
fi
|
||||
|
||||
- name: Resolve Docker image digests for attestation
|
||||
id: docker_digests
|
||||
if: ${{ !inputs.dry_run }}
|
||||
continue-on-error: true
|
||||
env:
|
||||
MULTIARCH_IMAGE: ${{ steps.build_docker.outputs.multiarch_image }}
|
||||
LATEST_TARGET: ${{ steps.build_docker.outputs.latest_target }}
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
if [[ -n "${MULTIARCH_IMAGE}" ]]; then
|
||||
multiarch_digest=$(docker buildx imagetools inspect --raw "${MULTIARCH_IMAGE}" | sha256sum | awk '{print "sha256:"$1}')
|
||||
echo "multiarch_digest=${multiarch_digest}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
if [[ -n "${LATEST_TARGET}" ]]; then
|
||||
latest_digest=$(docker buildx imagetools inspect --raw "${LATEST_TARGET}" | sha256sum | awk '{print "sha256:"$1}')
|
||||
echo "latest_digest=${latest_digest}" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: GitHub Attestation for Docker image
|
||||
id: attest_main
|
||||
if: ${{ !inputs.dry_run && steps.docker_digests.outputs.multiarch_digest != '' }}
|
||||
if: ${{ !inputs.dry_run }}
|
||||
continue-on-error: true
|
||||
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
|
||||
with:
|
||||
subject-name: ghcr.io/coder/coder
|
||||
subject-digest: ${{ steps.docker_digests.outputs.multiarch_digest }}
|
||||
subject-name: ${{ steps.build_docker.outputs.multiarch_image }}
|
||||
predicate-type: "https://slsa.dev/provenance/v1"
|
||||
predicate: |
|
||||
{
|
||||
"buildType": "https://github.com/actions/runner-images/",
|
||||
"builder": {
|
||||
"id": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
},
|
||||
"invocation": {
|
||||
"configSource": {
|
||||
"uri": "git+https://github.com/${{ github.repository }}@${{ github.ref }}",
|
||||
"digest": {
|
||||
"sha1": "${{ github.sha }}"
|
||||
},
|
||||
"entryPoint": ".github/workflows/release.yaml"
|
||||
},
|
||||
"environment": {
|
||||
"github_workflow": "${{ github.workflow }}",
|
||||
"github_run_id": "${{ github.run_id }}"
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"buildInvocationID": "${{ github.run_id }}",
|
||||
"completeness": {
|
||||
"environment": true,
|
||||
"materials": true
|
||||
}
|
||||
}
|
||||
}
|
||||
push-to-registry: true
|
||||
|
||||
# Get the latest tag name for attestation
|
||||
- name: Get latest tag name
|
||||
id: latest_tag
|
||||
if: ${{ !inputs.dry_run && steps.build_docker.outputs.created_latest_tag == 'true' }}
|
||||
run: echo "tag=$(./scripts/image_tag.sh --version latest)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# If this is the highest version according to semver, also attest the "latest" tag
|
||||
- name: GitHub Attestation for "latest" Docker image
|
||||
id: attest_latest
|
||||
if: ${{ !inputs.dry_run && steps.docker_digests.outputs.latest_digest != '' }}
|
||||
if: ${{ !inputs.dry_run && steps.build_docker.outputs.created_latest_tag == 'true' }}
|
||||
continue-on-error: true
|
||||
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
|
||||
with:
|
||||
subject-name: ghcr.io/coder/coder
|
||||
subject-digest: ${{ steps.docker_digests.outputs.latest_digest }}
|
||||
subject-name: ${{ steps.latest_tag.outputs.tag }}
|
||||
predicate-type: "https://slsa.dev/provenance/v1"
|
||||
predicate: |
|
||||
{
|
||||
"buildType": "https://github.com/actions/runner-images/",
|
||||
"builder": {
|
||||
"id": "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
},
|
||||
"invocation": {
|
||||
"configSource": {
|
||||
"uri": "git+https://github.com/${{ github.repository }}@${{ github.ref }}",
|
||||
"digest": {
|
||||
"sha1": "${{ github.sha }}"
|
||||
},
|
||||
"entryPoint": ".github/workflows/release.yaml"
|
||||
},
|
||||
"environment": {
|
||||
"github_workflow": "${{ github.workflow }}",
|
||||
"github_run_id": "${{ github.run_id }}"
|
||||
}
|
||||
},
|
||||
"metadata": {
|
||||
"buildInvocationID": "${{ github.run_id }}",
|
||||
"completeness": {
|
||||
"environment": true,
|
||||
"materials": true
|
||||
}
|
||||
}
|
||||
}
|
||||
push-to-registry: true
|
||||
|
||||
# Report attestation failures but don't fail the workflow
|
||||
|
||||
@@ -125,7 +125,7 @@ jobs:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Delete PR Cleanup workflow runs
|
||||
uses: Mattraks/delete-workflow-runs@b3018382ca039b53d238908238bd35d1fb14f8ee # v2.1.0
|
||||
uses: Mattraks/delete-workflow-runs@5bf9a1dac5c4d041c029f0a8370ddf0c5cb5aeb7 # v2.1.0
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
repository: ${{ github.repository }}
|
||||
@@ -134,7 +134,7 @@ jobs:
|
||||
delete_workflow_pattern: pr-cleanup.yaml
|
||||
|
||||
- name: Delete PR Deploy workflow skipped runs
|
||||
uses: Mattraks/delete-workflow-runs@b3018382ca039b53d238908238bd35d1fb14f8ee # v2.1.0
|
||||
uses: Mattraks/delete-workflow-runs@5bf9a1dac5c4d041c029f0a8370ddf0c5cb5aeb7 # v2.1.0
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
repository: ${{ github.repository }}
|
||||
|
||||
@@ -46,14 +46,8 @@ jobs:
|
||||
echo " replacement: \"https://github.com/coder/coder/tree/${HEAD_SHA}/\""
|
||||
} >> .github/.linkspector.yml
|
||||
|
||||
# TODO: Remove this workaround once action-linkspector sets
|
||||
# package-manager-cache: false in its internal setup-node step.
|
||||
# See: https://github.com/UmbrellaDocs/action-linkspector/issues/54
|
||||
- name: Enable corepack
|
||||
run: corepack enable pnpm
|
||||
|
||||
- name: Check Markdown links
|
||||
uses: umbrelladocs/action-linkspector@37c85bcde51b30bf929936502bac6bfb7e8f0a4d # v1.4.1
|
||||
uses: umbrelladocs/action-linkspector@652f85bc57bb1e7d4327260decc10aa68f7694c3 # v1.4.0
|
||||
id: markdown-link-check
|
||||
# checks all markdown files from /docs including all subfolders
|
||||
with:
|
||||
|
||||
@@ -54,7 +54,6 @@ site/stats/
|
||||
*.tfstate.backup
|
||||
*.tfplan
|
||||
*.lock.hcl
|
||||
!provisioner/terraform/testdata/resources/.terraform.lock.hcl
|
||||
.terraform/
|
||||
!coderd/testdata/parameters/modules/.terraform/
|
||||
!provisioner/terraform/testdata/modules-source-caching/.terraform/
|
||||
|
||||
@@ -1260,21 +1260,11 @@ provisioner/terraform/testdata/.gen-golden: $(wildcard provisioner/terraform/tes
|
||||
touch "$@"
|
||||
|
||||
provisioner/terraform/testdata/version:
|
||||
@tf_match=true; \
|
||||
if [[ "$$(cat provisioner/terraform/testdata/version.txt)" != \
|
||||
"$$(terraform version -json | jq -r '.terraform_version')" ]]; then \
|
||||
tf_match=false; \
|
||||
fi; \
|
||||
if ! $$tf_match || \
|
||||
! ./provisioner/terraform/testdata/generate.sh --check; then \
|
||||
./provisioner/terraform/testdata/generate.sh; \
|
||||
if [[ "$(shell cat provisioner/terraform/testdata/version.txt)" != "$(shell terraform version -json | jq -r '.terraform_version')" ]]; then
|
||||
./provisioner/terraform/testdata/generate.sh
|
||||
fi
|
||||
.PHONY: provisioner/terraform/testdata/version
|
||||
|
||||
update-terraform-testdata:
|
||||
./provisioner/terraform/testdata/generate.sh --upgrade
|
||||
.PHONY: update-terraform-testdata
|
||||
|
||||
# Set the retry flags if TEST_RETRIES is set
|
||||
ifdef TEST_RETRIES
|
||||
GOTESTSUM_RETRY_FLAGS := --rerun-fails=$(TEST_RETRIES)
|
||||
|
||||
@@ -50,7 +50,6 @@ import (
|
||||
"github.com/coder/coder/v2/agent/proto/resourcesmonitor"
|
||||
"github.com/coder/coder/v2/agent/reconnectingpty"
|
||||
"github.com/coder/coder/v2/agent/x/agentdesktop"
|
||||
"github.com/coder/coder/v2/agent/x/agentmcp"
|
||||
"github.com/coder/coder/v2/buildinfo"
|
||||
"github.com/coder/coder/v2/cli/gitauth"
|
||||
"github.com/coder/coder/v2/coderd/database/dbtime"
|
||||
@@ -312,8 +311,6 @@ type agent struct {
|
||||
gitAPI *agentgit.API
|
||||
processAPI *agentproc.API
|
||||
desktopAPI *agentdesktop.API
|
||||
mcpManager *agentmcp.Manager
|
||||
mcpAPI *agentmcp.API
|
||||
|
||||
socketServerEnabled bool
|
||||
socketPath string
|
||||
@@ -399,8 +396,6 @@ func (a *agent) init() {
|
||||
a.logger.Named("desktop"), a.execer, a.scriptRunner.ScriptBinDir(),
|
||||
)
|
||||
a.desktopAPI = agentdesktop.NewAPI(a.logger.Named("desktop"), desktop, a.clock)
|
||||
a.mcpManager = agentmcp.NewManager(a.logger.Named("mcp"))
|
||||
a.mcpAPI = agentmcp.NewAPI(a.logger.Named("mcp"), a.mcpManager)
|
||||
a.reconnectingPTYServer = reconnectingpty.NewServer(
|
||||
a.logger.Named("reconnecting-pty"),
|
||||
a.sshServer,
|
||||
@@ -1353,14 +1348,6 @@ func (a *agent) handleManifest(manifestOK *checkpoint) func(ctx context.Context,
|
||||
}
|
||||
a.metrics.startupScriptSeconds.WithLabelValues(label).Set(dur)
|
||||
a.scriptRunner.StartCron()
|
||||
|
||||
// Connect to workspace MCP servers after the
|
||||
// lifecycle transition to avoid delaying Ready.
|
||||
// This runs inside the tracked goroutine so it
|
||||
// is properly awaited on shutdown.
|
||||
if mcpErr := a.mcpManager.Connect(a.gracefulCtx, manifest.Directory); mcpErr != nil {
|
||||
a.logger.Warn(ctx, "failed to connect to workspace MCP servers", slog.Error(mcpErr))
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("track conn goroutine: %w", err)
|
||||
@@ -2083,10 +2070,6 @@ func (a *agent) Close() error {
|
||||
a.logger.Error(a.hardCtx, "desktop API close", slog.Error(err))
|
||||
}
|
||||
|
||||
if err := a.mcpManager.Close(); err != nil {
|
||||
a.logger.Error(a.hardCtx, "mcp manager close", slog.Error(err))
|
||||
}
|
||||
|
||||
if a.boundaryLogProxy != nil {
|
||||
err = a.boundaryLogProxy.Close()
|
||||
if err != nil {
|
||||
|
||||
@@ -159,6 +159,7 @@ func TestConvertDockerVolume(t *testing.T) {
|
||||
func TestConvertDockerInspect(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
//nolint:paralleltest // variable recapture no longer required
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
expect []codersdk.WorkspaceAgentContainer
|
||||
@@ -387,6 +388,7 @@ func TestConvertDockerInspect(t *testing.T) {
|
||||
},
|
||||
},
|
||||
} {
|
||||
// nolint:paralleltest // variable recapture no longer required
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
bs, err := os.ReadFile(filepath.Join("testdata", tt.name, "docker_inspect.json"))
|
||||
|
||||
@@ -166,6 +166,7 @@ func TestDockerEnvInfoer(t *testing.T) {
|
||||
|
||||
pool, err := dockertest.NewPool("")
|
||||
require.NoError(t, err, "Could not connect to docker")
|
||||
// nolint:paralleltest // variable recapture no longer required
|
||||
for idx, tt := range []struct {
|
||||
image string
|
||||
labels map[string]string
|
||||
@@ -222,6 +223,7 @@ func TestDockerEnvInfoer(t *testing.T) {
|
||||
expectedUserShell: "/bin/bash",
|
||||
},
|
||||
} {
|
||||
//nolint:paralleltest // variable recapture no longer required
|
||||
t.Run(fmt.Sprintf("#%d", idx), func(t *testing.T) {
|
||||
// Start a container with the given image
|
||||
// and environment variables
|
||||
|
||||
@@ -211,7 +211,7 @@ func TestServer_X11_EvictionLRU(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
stderr, err := sess.StderrPipe()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, sess.Start("sh"))
|
||||
require.NoError(t, sess.Shell())
|
||||
|
||||
// The SSH server lazily starts the session. We need to write a command
|
||||
// and read back to ensure the X11 forwarding is started.
|
||||
|
||||
@@ -31,7 +31,6 @@ func (a *agent) apiHandler() http.Handler {
|
||||
r.Mount("/api/v0/git", a.gitAPI.Routes())
|
||||
r.Mount("/api/v0/processes", a.processAPI.Routes())
|
||||
r.Mount("/api/v0/desktop", a.desktopAPI.Routes())
|
||||
r.Mount("/api/v0/mcp", a.mcpAPI.Routes())
|
||||
|
||||
if a.devcontainers {
|
||||
r.Mount("/api/v0/containers", a.containerAPI.Routes())
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
package agentmcp
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"cdr.dev/slog/v3"
|
||||
"github.com/coder/coder/v2/coderd/httpapi"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/codersdk/workspacesdk"
|
||||
)
|
||||
|
||||
// API exposes MCP tool discovery and call proxying through the
|
||||
// agent.
|
||||
type API struct {
|
||||
logger slog.Logger
|
||||
manager *Manager
|
||||
}
|
||||
|
||||
// NewAPI creates a new MCP API handler backed by the given
|
||||
// manager.
|
||||
func NewAPI(logger slog.Logger, manager *Manager) *API {
|
||||
return &API{
|
||||
logger: logger,
|
||||
manager: manager,
|
||||
}
|
||||
}
|
||||
|
||||
// Routes returns the HTTP handler for MCP-related routes.
|
||||
func (api *API) Routes() http.Handler {
|
||||
r := chi.NewRouter()
|
||||
r.Get("/tools", api.handleListTools)
|
||||
r.Post("/call-tool", api.handleCallTool)
|
||||
return r
|
||||
}
|
||||
|
||||
// handleListTools returns the cached MCP tool definitions,
|
||||
// optionally refreshing them first if ?refresh=true is set.
|
||||
func (api *API) handleListTools(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// Allow callers to force a tool re-scan before listing.
|
||||
if r.URL.Query().Get("refresh") == "true" {
|
||||
if err := api.manager.RefreshTools(ctx); err != nil {
|
||||
api.logger.Warn(ctx, "failed to refresh MCP tools", slog.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
tools := api.manager.Tools()
|
||||
// Ensure non-nil so JSON serialization returns [] not null.
|
||||
if tools == nil {
|
||||
tools = []workspacesdk.MCPToolInfo{}
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, workspacesdk.ListMCPToolsResponse{
|
||||
Tools: tools,
|
||||
})
|
||||
}
|
||||
|
||||
// handleCallTool proxies a tool invocation to the appropriate
|
||||
// MCP server based on the tool name prefix.
|
||||
func (api *API) handleCallTool(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
var req workspacesdk.CallMCPToolRequest
|
||||
if !httpapi.Read(ctx, rw, r, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := api.manager.CallTool(ctx, req)
|
||||
if err != nil {
|
||||
status := http.StatusBadGateway
|
||||
if errors.Is(err, ErrInvalidToolName) {
|
||||
status = http.StatusBadRequest
|
||||
} else if errors.Is(err, ErrUnknownServer) {
|
||||
status = http.StatusNotFound
|
||||
}
|
||||
httpapi.Write(ctx, rw, status, codersdk.Response{
|
||||
Message: "MCP tool call failed.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, resp)
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
package agentmcp
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
// ServerConfig describes a single MCP server parsed from a .mcp.json file.
|
||||
type ServerConfig struct {
|
||||
Name string `json:"name"`
|
||||
Transport string `json:"type"`
|
||||
Command string `json:"command"`
|
||||
Args []string `json:"args"`
|
||||
Env map[string]string `json:"env"`
|
||||
URL string `json:"url"`
|
||||
Headers map[string]string `json:"headers"`
|
||||
}
|
||||
|
||||
// mcpConfigFile mirrors the on-disk .mcp.json schema.
|
||||
type mcpConfigFile struct {
|
||||
MCPServers map[string]json.RawMessage `json:"mcpServers"`
|
||||
}
|
||||
|
||||
// mcpServerEntry is a single server block inside mcpServers.
|
||||
type mcpServerEntry struct {
|
||||
Command string `json:"command"`
|
||||
Args []string `json:"args"`
|
||||
Env map[string]string `json:"env"`
|
||||
Type string `json:"type"`
|
||||
URL string `json:"url"`
|
||||
Headers map[string]string `json:"headers"`
|
||||
}
|
||||
|
||||
// ParseConfig reads a .mcp.json file at path and returns the declared
|
||||
// MCP servers sorted by name. It returns an empty slice when the
|
||||
// mcpServers key is missing or empty.
|
||||
func ParseConfig(path string) ([]ServerConfig, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("read mcp config %q: %w", path, err)
|
||||
}
|
||||
|
||||
var cfg mcpConfigFile
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
return nil, xerrors.Errorf("parse mcp config %q: %w", path, err)
|
||||
}
|
||||
|
||||
if len(cfg.MCPServers) == 0 {
|
||||
return []ServerConfig{}, nil
|
||||
}
|
||||
|
||||
servers := make([]ServerConfig, 0, len(cfg.MCPServers))
|
||||
for name, raw := range cfg.MCPServers {
|
||||
var entry mcpServerEntry
|
||||
if err := json.Unmarshal(raw, &entry); err != nil {
|
||||
return nil, xerrors.Errorf("parse server %q in %q: %w", name, path, err)
|
||||
}
|
||||
|
||||
if strings.Contains(name, ToolNameSep) || strings.HasPrefix(name, "_") || strings.HasSuffix(name, "_") {
|
||||
return nil, xerrors.Errorf("server name %q in %q contains reserved separator %q or leading/trailing underscore", name, path, ToolNameSep)
|
||||
}
|
||||
|
||||
transport := inferTransport(entry)
|
||||
|
||||
if transport == "" {
|
||||
return nil, xerrors.Errorf("server %q in %q has no command or url", name, path)
|
||||
}
|
||||
|
||||
resolveEnvVars(entry.Env)
|
||||
|
||||
servers = append(servers, ServerConfig{
|
||||
Name: name,
|
||||
Transport: transport,
|
||||
Command: entry.Command,
|
||||
Args: entry.Args,
|
||||
Env: entry.Env,
|
||||
URL: entry.URL,
|
||||
Headers: entry.Headers,
|
||||
})
|
||||
}
|
||||
|
||||
slices.SortFunc(servers, func(a, b ServerConfig) int {
|
||||
return strings.Compare(a.Name, b.Name)
|
||||
})
|
||||
|
||||
return servers, nil
|
||||
}
|
||||
|
||||
// inferTransport determines the transport type for a server entry.
|
||||
// An explicit "type" field takes priority; otherwise the presence
|
||||
// of "command" implies stdio and "url" implies http.
|
||||
func inferTransport(e mcpServerEntry) string {
|
||||
if e.Type != "" {
|
||||
return e.Type
|
||||
}
|
||||
if e.Command != "" {
|
||||
return "stdio"
|
||||
}
|
||||
if e.URL != "" {
|
||||
return "http"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// resolveEnvVars expands ${VAR} references in env map values
|
||||
// using the current process environment.
|
||||
func resolveEnvVars(env map[string]string) {
|
||||
for k, v := range env {
|
||||
env[k] = os.Expand(v, os.Getenv)
|
||||
}
|
||||
}
|
||||
@@ -1,254 +0,0 @@
|
||||
package agentmcp_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/agent/x/agentmcp"
|
||||
)
|
||||
|
||||
func TestParseConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
content string
|
||||
expected []agentmcp.ServerConfig
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "StdioServer",
|
||||
content: mustJSON(t, map[string]any{
|
||||
"mcpServers": map[string]any{
|
||||
"my-server": map[string]any{
|
||||
"command": "npx",
|
||||
"args": []string{"-y", "@example/mcp-server"},
|
||||
"env": map[string]string{"FOO": "bar"},
|
||||
},
|
||||
},
|
||||
}),
|
||||
expected: []agentmcp.ServerConfig{
|
||||
{
|
||||
Name: "my-server",
|
||||
Transport: "stdio",
|
||||
Command: "npx",
|
||||
Args: []string{"-y", "@example/mcp-server"},
|
||||
Env: map[string]string{"FOO": "bar"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "HTTPServer",
|
||||
content: mustJSON(t, map[string]any{
|
||||
"mcpServers": map[string]any{
|
||||
"remote": map[string]any{
|
||||
"url": "https://example.com/mcp",
|
||||
"headers": map[string]string{"Authorization": "Bearer tok"},
|
||||
},
|
||||
},
|
||||
}),
|
||||
expected: []agentmcp.ServerConfig{
|
||||
{
|
||||
Name: "remote",
|
||||
Transport: "http",
|
||||
URL: "https://example.com/mcp",
|
||||
Headers: map[string]string{"Authorization": "Bearer tok"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SSEServer",
|
||||
content: mustJSON(t, map[string]any{
|
||||
"mcpServers": map[string]any{
|
||||
"events": map[string]any{
|
||||
"type": "sse",
|
||||
"url": "https://example.com/sse",
|
||||
},
|
||||
},
|
||||
}),
|
||||
expected: []agentmcp.ServerConfig{
|
||||
{
|
||||
Name: "events",
|
||||
Transport: "sse",
|
||||
URL: "https://example.com/sse",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ExplicitTypeOverridesInference",
|
||||
content: mustJSON(t, map[string]any{
|
||||
"mcpServers": map[string]any{
|
||||
"hybrid": map[string]any{
|
||||
"command": "some-binary",
|
||||
"type": "http",
|
||||
},
|
||||
},
|
||||
}),
|
||||
expected: []agentmcp.ServerConfig{
|
||||
{
|
||||
Name: "hybrid",
|
||||
Transport: "http",
|
||||
Command: "some-binary",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "EnvVarPassthrough",
|
||||
content: mustJSON(t, map[string]any{
|
||||
"mcpServers": map[string]any{
|
||||
"srv": map[string]any{
|
||||
"command": "run",
|
||||
"env": map[string]string{"PLAIN": "literal-value"},
|
||||
},
|
||||
},
|
||||
}),
|
||||
expected: []agentmcp.ServerConfig{
|
||||
{
|
||||
Name: "srv",
|
||||
Transport: "stdio",
|
||||
Command: "run",
|
||||
Env: map[string]string{"PLAIN": "literal-value"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "EmptyMCPServers",
|
||||
content: mustJSON(t, map[string]any{
|
||||
"mcpServers": map[string]any{},
|
||||
}),
|
||||
expected: []agentmcp.ServerConfig{},
|
||||
},
|
||||
{
|
||||
name: "MalformedJSON",
|
||||
content: `{not valid json`,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "ServerNameContainsSeparator",
|
||||
content: mustJSON(t, map[string]any{
|
||||
"mcpServers": map[string]any{
|
||||
"bad__name": map[string]any{"command": "run"},
|
||||
},
|
||||
}),
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "ServerNameTrailingUnderscore",
|
||||
content: mustJSON(t, map[string]any{
|
||||
"mcpServers": map[string]any{
|
||||
"server_": map[string]any{"command": "run"},
|
||||
},
|
||||
}),
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "ServerNameLeadingUnderscore",
|
||||
content: mustJSON(t, map[string]any{
|
||||
"mcpServers": map[string]any{
|
||||
"_server": map[string]any{"command": "run"},
|
||||
},
|
||||
}),
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "EmptyTransport", content: mustJSON(t, map[string]any{
|
||||
"mcpServers": map[string]any{
|
||||
"empty": map[string]any{},
|
||||
},
|
||||
}),
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "MissingMCPServersKey",
|
||||
content: mustJSON(t, map[string]any{
|
||||
"servers": map[string]any{},
|
||||
}),
|
||||
expected: []agentmcp.ServerConfig{},
|
||||
},
|
||||
{
|
||||
name: "MultipleServersSortedByName",
|
||||
content: mustJSON(t, map[string]any{
|
||||
"mcpServers": map[string]any{
|
||||
"zeta": map[string]any{"command": "z"},
|
||||
"alpha": map[string]any{"command": "a"},
|
||||
"mu": map[string]any{"command": "m"},
|
||||
},
|
||||
}),
|
||||
expected: []agentmcp.ServerConfig{
|
||||
{Name: "alpha", Transport: "stdio", Command: "a"},
|
||||
{Name: "mu", Transport: "stdio", Command: "m"},
|
||||
{Name: "zeta", Transport: "stdio", Command: "z"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, ".mcp.json")
|
||||
err := os.WriteFile(path, []byte(tt.content), 0o600)
|
||||
require.NoError(t, err)
|
||||
|
||||
got, err := agentmcp.ParseConfig(path)
|
||||
if tt.expectError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.expected, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseConfig_EnvVarInterpolation verifies that ${VAR} references
|
||||
// in env values are resolved from the process environment. This test
|
||||
// cannot be parallel because t.Setenv is incompatible with t.Parallel.
|
||||
func TestParseConfig_EnvVarInterpolation(t *testing.T) {
|
||||
t.Setenv("TEST_MCP_TOKEN", "secret123")
|
||||
|
||||
content := mustJSON(t, map[string]any{
|
||||
"mcpServers": map[string]any{
|
||||
"srv": map[string]any{
|
||||
"command": "run",
|
||||
"env": map[string]string{"TOKEN": "${TEST_MCP_TOKEN}"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, ".mcp.json")
|
||||
err := os.WriteFile(path, []byte(content), 0o600)
|
||||
require.NoError(t, err)
|
||||
|
||||
got, err := agentmcp.ParseConfig(path)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []agentmcp.ServerConfig{
|
||||
{
|
||||
Name: "srv",
|
||||
Transport: "stdio",
|
||||
Command: "run",
|
||||
Env: map[string]string{"TOKEN": "secret123"},
|
||||
},
|
||||
}, got)
|
||||
}
|
||||
|
||||
func TestParseConfig_FileNotFound(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, err := agentmcp.ParseConfig(filepath.Join(t.TempDir(), "nonexistent.json"))
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
// mustJSON marshals v to a JSON string, failing the test on error.
|
||||
func mustJSON(t *testing.T, v any) string {
|
||||
t.Helper()
|
||||
data, err := json.Marshal(v)
|
||||
require.NoError(t, err)
|
||||
return string(data)
|
||||
}
|
||||
@@ -1,447 +0,0 @@
|
||||
package agentmcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/mark3labs/mcp-go/client"
|
||||
"github.com/mark3labs/mcp-go/client/transport"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"cdr.dev/slog/v3"
|
||||
"github.com/coder/coder/v2/buildinfo"
|
||||
"github.com/coder/coder/v2/codersdk/workspacesdk"
|
||||
)
|
||||
|
||||
// ToolNameSep separates the server name from the original tool name
|
||||
// in prefixed tool names. Double underscore avoids collisions with
|
||||
// tool names that may contain single underscores.
|
||||
const ToolNameSep = "__"
|
||||
|
||||
// connectTimeout bounds how long we wait for a single MCP server
|
||||
// to start its transport and complete initialization.
|
||||
const connectTimeout = 30 * time.Second
|
||||
|
||||
// toolCallTimeout bounds how long a single tool invocation may
|
||||
// take before being canceled.
|
||||
const toolCallTimeout = 60 * time.Second
|
||||
|
||||
var (
|
||||
// ErrInvalidToolName is returned when the tool name format
|
||||
// is not "server__tool".
|
||||
ErrInvalidToolName = xerrors.New("invalid tool name format")
|
||||
// ErrUnknownServer is returned when no MCP server matches
|
||||
// the prefix in the tool name.
|
||||
ErrUnknownServer = xerrors.New("unknown MCP server")
|
||||
)
|
||||
|
||||
// Manager manages connections to MCP servers discovered from a
|
||||
// workspace's .mcp.json file. It caches the aggregated tool list
|
||||
// and proxies tool calls to the appropriate server.
|
||||
type Manager struct {
|
||||
mu sync.RWMutex
|
||||
logger slog.Logger
|
||||
closed bool
|
||||
servers map[string]*serverEntry // keyed by server name
|
||||
tools []workspacesdk.MCPToolInfo
|
||||
}
|
||||
|
||||
// serverEntry pairs a server config with its connected client.
|
||||
type serverEntry struct {
|
||||
config ServerConfig
|
||||
client *client.Client
|
||||
}
|
||||
|
||||
// NewManager creates a new MCP client manager.
|
||||
func NewManager(logger slog.Logger) *Manager {
|
||||
return &Manager{
|
||||
logger: logger,
|
||||
servers: make(map[string]*serverEntry),
|
||||
}
|
||||
}
|
||||
|
||||
// Connect discovers .mcp.json in dir and connects to all
|
||||
// configured servers. Failed servers are logged and skipped.
|
||||
func (m *Manager) Connect(ctx context.Context, dir string) error {
|
||||
path := filepath.Join(dir, ".mcp.json")
|
||||
configs, err := ParseConfig(path)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
return xerrors.Errorf("parse mcp config: %w", err)
|
||||
}
|
||||
|
||||
// Connect to servers in parallel without holding the
|
||||
// lock, since each connectServer call may block on
|
||||
// network I/O for up to connectTimeout.
|
||||
type connectedServer struct {
|
||||
name string
|
||||
config ServerConfig
|
||||
client *client.Client
|
||||
}
|
||||
var (
|
||||
mu sync.Mutex
|
||||
connected []connectedServer
|
||||
)
|
||||
var eg errgroup.Group
|
||||
for _, cfg := range configs {
|
||||
eg.Go(func() error {
|
||||
c, err := m.connectServer(ctx, cfg)
|
||||
if err != nil {
|
||||
m.logger.Warn(ctx, "skipping MCP server",
|
||||
slog.F("server", cfg.Name),
|
||||
slog.F("transport", cfg.Transport),
|
||||
slog.Error(err),
|
||||
)
|
||||
return nil // Don't fail the group.
|
||||
}
|
||||
mu.Lock()
|
||||
connected = append(connected, connectedServer{
|
||||
name: cfg.Name, config: cfg, client: c,
|
||||
})
|
||||
mu.Unlock()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
_ = eg.Wait()
|
||||
|
||||
m.mu.Lock()
|
||||
if m.closed {
|
||||
m.mu.Unlock()
|
||||
// Close the freshly-connected clients since we're
|
||||
// shutting down.
|
||||
for _, cs := range connected {
|
||||
_ = cs.client.Close()
|
||||
}
|
||||
return xerrors.New("manager closed")
|
||||
}
|
||||
|
||||
// Close previous connections to avoid leaking child
|
||||
// processes on agent reconnect.
|
||||
for _, entry := range m.servers {
|
||||
_ = entry.client.Close()
|
||||
}
|
||||
m.servers = make(map[string]*serverEntry, len(connected))
|
||||
|
||||
for _, cs := range connected {
|
||||
m.servers[cs.name] = &serverEntry{
|
||||
config: cs.config,
|
||||
client: cs.client,
|
||||
}
|
||||
}
|
||||
m.mu.Unlock()
|
||||
|
||||
// Refresh tools outside the lock to avoid blocking
|
||||
// concurrent reads during network I/O.
|
||||
if err := m.RefreshTools(ctx); err != nil {
|
||||
m.logger.Warn(ctx, "failed to refresh MCP tools after connect", slog.Error(err))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// connectServer establishes a connection to a single MCP server
|
||||
// and returns the connected client. It does not modify any Manager
|
||||
// state.
|
||||
func (*Manager) connectServer(ctx context.Context, cfg ServerConfig) (*client.Client, error) {
|
||||
tr, err := createTransport(cfg)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("create transport for %q: %w", cfg.Name, err)
|
||||
}
|
||||
|
||||
c := client.NewClient(tr)
|
||||
|
||||
connectCtx, cancel := context.WithTimeout(ctx, connectTimeout)
|
||||
defer cancel()
|
||||
|
||||
if err := c.Start(connectCtx); err != nil {
|
||||
_ = c.Close()
|
||||
return nil, xerrors.Errorf("start %q: %w", cfg.Name, err)
|
||||
}
|
||||
|
||||
_, err = c.Initialize(connectCtx, mcp.InitializeRequest{
|
||||
Params: mcp.InitializeParams{
|
||||
ProtocolVersion: mcp.LATEST_PROTOCOL_VERSION,
|
||||
ClientInfo: mcp.Implementation{
|
||||
Name: "coder-agent",
|
||||
Version: buildinfo.Version(),
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
_ = c.Close()
|
||||
return nil, xerrors.Errorf("initialize %q: %w", cfg.Name, err)
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// createTransport builds the mcp-go transport for a server config.
|
||||
func createTransport(cfg ServerConfig) (transport.Interface, error) {
|
||||
switch cfg.Transport {
|
||||
case "stdio":
|
||||
return transport.NewStdio(
|
||||
cfg.Command,
|
||||
buildEnv(cfg.Env),
|
||||
cfg.Args...,
|
||||
), nil
|
||||
case "http", "":
|
||||
return transport.NewStreamableHTTP(
|
||||
cfg.URL,
|
||||
transport.WithHTTPHeaders(cfg.Headers),
|
||||
)
|
||||
case "sse":
|
||||
return transport.NewSSE(
|
||||
cfg.URL,
|
||||
transport.WithHeaders(cfg.Headers),
|
||||
)
|
||||
default:
|
||||
return nil, xerrors.Errorf("unsupported transport %q", cfg.Transport)
|
||||
}
|
||||
}
|
||||
|
||||
// buildEnv merges the current process environment with explicit
|
||||
// overrides, returning the result as KEY=VALUE strings suitable
|
||||
// for the stdio transport.
|
||||
func buildEnv(explicit map[string]string) []string {
|
||||
env := os.Environ()
|
||||
if len(explicit) == 0 {
|
||||
return env
|
||||
}
|
||||
|
||||
// Index existing env so explicit keys can override in-place.
|
||||
existing := make(map[string]int, len(env))
|
||||
for i, kv := range env {
|
||||
if k, _, ok := strings.Cut(kv, "="); ok {
|
||||
existing[k] = i
|
||||
}
|
||||
}
|
||||
|
||||
for k, v := range explicit {
|
||||
entry := k + "=" + v
|
||||
if idx, ok := existing[k]; ok {
|
||||
env[idx] = entry
|
||||
} else {
|
||||
env = append(env, entry)
|
||||
}
|
||||
}
|
||||
return env
|
||||
}
|
||||
|
||||
// Tools returns the cached tool list. Thread-safe.
|
||||
func (m *Manager) Tools() []workspacesdk.MCPToolInfo {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
return slices.Clone(m.tools)
|
||||
}
|
||||
|
||||
// CallTool proxies a tool call to the appropriate MCP server.
|
||||
func (m *Manager) CallTool(ctx context.Context, req workspacesdk.CallMCPToolRequest) (workspacesdk.CallMCPToolResponse, error) {
|
||||
serverName, originalName, err := splitToolName(req.ToolName)
|
||||
if err != nil {
|
||||
return workspacesdk.CallMCPToolResponse{}, err
|
||||
}
|
||||
|
||||
m.mu.RLock()
|
||||
entry, ok := m.servers[serverName]
|
||||
m.mu.RUnlock()
|
||||
|
||||
if !ok {
|
||||
return workspacesdk.CallMCPToolResponse{}, xerrors.Errorf("%w: %q", ErrUnknownServer, serverName)
|
||||
}
|
||||
|
||||
callCtx, cancel := context.WithTimeout(ctx, toolCallTimeout)
|
||||
defer cancel()
|
||||
|
||||
result, err := entry.client.CallTool(callCtx, mcp.CallToolRequest{
|
||||
Params: mcp.CallToolParams{
|
||||
Name: originalName,
|
||||
Arguments: req.Arguments,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return workspacesdk.CallMCPToolResponse{}, xerrors.Errorf("call tool %q on %q: %w", originalName, serverName, err)
|
||||
}
|
||||
|
||||
return convertResult(result), nil
|
||||
}
|
||||
|
||||
// splitToolName extracts the server name and original tool name
|
||||
// from a prefixed tool name like "server__tool".
|
||||
func splitToolName(prefixed string) (serverName, toolName string, err error) {
|
||||
server, tool, ok := strings.Cut(prefixed, ToolNameSep)
|
||||
if !ok || server == "" || tool == "" {
|
||||
return "", "", xerrors.Errorf("%w: expected format \"server%stool\", got %q", ErrInvalidToolName, ToolNameSep, prefixed)
|
||||
}
|
||||
return server, tool, nil
|
||||
}
|
||||
|
||||
// convertResult translates an MCP CallToolResult into a
|
||||
// workspacesdk.CallMCPToolResponse. It iterates over content
|
||||
// items and maps each recognized type.
|
||||
func convertResult(result *mcp.CallToolResult) workspacesdk.CallMCPToolResponse {
|
||||
if result == nil {
|
||||
return workspacesdk.CallMCPToolResponse{}
|
||||
}
|
||||
|
||||
var content []workspacesdk.MCPToolContent
|
||||
for _, item := range result.Content {
|
||||
switch c := item.(type) {
|
||||
case mcp.TextContent:
|
||||
content = append(content, workspacesdk.MCPToolContent{
|
||||
Type: "text",
|
||||
Text: c.Text,
|
||||
})
|
||||
case mcp.ImageContent:
|
||||
content = append(content, workspacesdk.MCPToolContent{
|
||||
Type: "image",
|
||||
Data: c.Data,
|
||||
MediaType: c.MIMEType,
|
||||
})
|
||||
case mcp.AudioContent:
|
||||
content = append(content, workspacesdk.MCPToolContent{
|
||||
Type: "audio",
|
||||
Data: c.Data,
|
||||
MediaType: c.MIMEType,
|
||||
})
|
||||
case mcp.EmbeddedResource:
|
||||
content = append(content, workspacesdk.MCPToolContent{
|
||||
Type: "resource",
|
||||
Text: fmt.Sprintf("[embedded resource: %T]", c.Resource),
|
||||
})
|
||||
case mcp.ResourceLink:
|
||||
content = append(content, workspacesdk.MCPToolContent{
|
||||
Type: "resource",
|
||||
Text: fmt.Sprintf("[resource link: %s]", c.URI),
|
||||
})
|
||||
default:
|
||||
content = append(content, workspacesdk.MCPToolContent{
|
||||
Type: "text",
|
||||
Text: fmt.Sprintf("[unsupported content type: %T]", item),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return workspacesdk.CallMCPToolResponse{
|
||||
Content: content,
|
||||
IsError: result.IsError,
|
||||
}
|
||||
}
|
||||
|
||||
// RefreshTools re-fetches tool lists from all connected servers
|
||||
// in parallel and rebuilds the cache. On partial failure, tools
|
||||
// from servers that responded successfully are merged with the
|
||||
// existing cached tools for servers that failed, so a single
|
||||
// dead server doesn't block updates from healthy ones.
|
||||
func (m *Manager) RefreshTools(ctx context.Context) error {
|
||||
// Snapshot servers under read lock.
|
||||
m.mu.RLock()
|
||||
servers := make(map[string]*serverEntry, len(m.servers))
|
||||
for k, v := range m.servers {
|
||||
servers[k] = v
|
||||
}
|
||||
m.mu.RUnlock()
|
||||
|
||||
// Fetch tool lists in parallel without holding any lock.
|
||||
type serverTools struct {
|
||||
name string
|
||||
tools []workspacesdk.MCPToolInfo
|
||||
}
|
||||
var (
|
||||
mu sync.Mutex
|
||||
results []serverTools
|
||||
failed []string
|
||||
errs []error
|
||||
)
|
||||
var eg errgroup.Group
|
||||
for name, entry := range servers {
|
||||
eg.Go(func() error {
|
||||
listCtx, cancel := context.WithTimeout(ctx, connectTimeout)
|
||||
result, err := entry.client.ListTools(listCtx, mcp.ListToolsRequest{})
|
||||
cancel()
|
||||
if err != nil {
|
||||
m.logger.Warn(ctx, "failed to list tools from MCP server",
|
||||
slog.F("server", name),
|
||||
slog.Error(err),
|
||||
)
|
||||
mu.Lock()
|
||||
errs = append(errs, xerrors.Errorf("list tools from %q: %w", name, err))
|
||||
failed = append(failed, name)
|
||||
mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
var tools []workspacesdk.MCPToolInfo
|
||||
for _, tool := range result.Tools {
|
||||
tools = append(tools, workspacesdk.MCPToolInfo{
|
||||
ServerName: name,
|
||||
Name: name + ToolNameSep + tool.Name,
|
||||
Description: tool.Description,
|
||||
Schema: tool.InputSchema.Properties,
|
||||
Required: tool.InputSchema.Required,
|
||||
})
|
||||
}
|
||||
mu.Lock()
|
||||
results = append(results, serverTools{name: name, tools: tools})
|
||||
mu.Unlock()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
_ = eg.Wait()
|
||||
|
||||
// Build the new tool list. For servers that failed, preserve
|
||||
// their tools from the existing cache so a single dead server
|
||||
// doesn't remove healthy tools.
|
||||
var merged []workspacesdk.MCPToolInfo
|
||||
for _, st := range results {
|
||||
merged = append(merged, st.tools...)
|
||||
}
|
||||
if len(failed) > 0 {
|
||||
failedSet := make(map[string]struct{}, len(failed))
|
||||
for _, f := range failed {
|
||||
failedSet[f] = struct{}{}
|
||||
}
|
||||
m.mu.RLock()
|
||||
for _, t := range m.tools {
|
||||
if _, ok := failedSet[t.ServerName]; ok {
|
||||
merged = append(merged, t)
|
||||
}
|
||||
}
|
||||
m.mu.RUnlock()
|
||||
}
|
||||
slices.SortFunc(merged, func(a, b workspacesdk.MCPToolInfo) int {
|
||||
return strings.Compare(a.Name, b.Name)
|
||||
})
|
||||
|
||||
m.mu.Lock()
|
||||
m.tools = merged
|
||||
m.mu.Unlock()
|
||||
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
// Close terminates all MCP server connections and child
|
||||
// processes.
|
||||
func (m *Manager) Close() error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
m.closed = true
|
||||
var errs []error
|
||||
for _, entry := range m.servers {
|
||||
errs = append(errs, entry.client.Close())
|
||||
}
|
||||
m.servers = make(map[string]*serverEntry)
|
||||
m.tools = nil
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
@@ -1,195 +0,0 @@
|
||||
package agentmcp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/codersdk/workspacesdk"
|
||||
)
|
||||
|
||||
func TestSplitToolName(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantServer string
|
||||
wantTool string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Valid",
|
||||
input: "server__tool",
|
||||
wantServer: "server",
|
||||
wantTool: "tool",
|
||||
},
|
||||
{
|
||||
name: "ValidWithUnderscoresInTool",
|
||||
input: "server__my_tool",
|
||||
wantServer: "server",
|
||||
wantTool: "my_tool",
|
||||
},
|
||||
{
|
||||
name: "MissingSeparator",
|
||||
input: "servertool",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "EmptyServer",
|
||||
input: "__tool",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "EmptyTool",
|
||||
input: "server__",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "JustSeparator",
|
||||
input: "__",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
server, tool, err := splitToolName(tt.input)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
assert.ErrorIs(t, err, ErrInvalidToolName)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.wantServer, server)
|
||||
assert.Equal(t, tt.wantTool, tool)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertResult(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
// input is a pointer so we can test nil.
|
||||
input *mcp.CallToolResult
|
||||
want workspacesdk.CallMCPToolResponse
|
||||
}{
|
||||
{
|
||||
name: "NilInput",
|
||||
input: nil,
|
||||
want: workspacesdk.CallMCPToolResponse{},
|
||||
},
|
||||
{
|
||||
name: "TextContent",
|
||||
input: &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{Type: "text", Text: "hello"},
|
||||
},
|
||||
},
|
||||
want: workspacesdk.CallMCPToolResponse{
|
||||
Content: []workspacesdk.MCPToolContent{
|
||||
{Type: "text", Text: "hello"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ImageContent",
|
||||
input: &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.ImageContent{
|
||||
Type: "image",
|
||||
Data: "base64data",
|
||||
MIMEType: "image/png",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: workspacesdk.CallMCPToolResponse{
|
||||
Content: []workspacesdk.MCPToolContent{
|
||||
{Type: "image", Data: "base64data", MediaType: "image/png"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "AudioContent",
|
||||
input: &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.AudioContent{
|
||||
Type: "audio",
|
||||
Data: "base64audio",
|
||||
MIMEType: "audio/mp3",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: workspacesdk.CallMCPToolResponse{
|
||||
Content: []workspacesdk.MCPToolContent{
|
||||
{Type: "audio", Data: "base64audio", MediaType: "audio/mp3"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "IsErrorPropagation",
|
||||
input: &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{Type: "text", Text: "fail"},
|
||||
},
|
||||
IsError: true,
|
||||
},
|
||||
want: workspacesdk.CallMCPToolResponse{
|
||||
Content: []workspacesdk.MCPToolContent{
|
||||
{Type: "text", Text: "fail"},
|
||||
},
|
||||
IsError: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "MultipleContentItems",
|
||||
input: &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.TextContent{Type: "text", Text: "caption"},
|
||||
mcp.ImageContent{
|
||||
Type: "image",
|
||||
Data: "imgdata",
|
||||
MIMEType: "image/jpeg",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: workspacesdk.CallMCPToolResponse{
|
||||
Content: []workspacesdk.MCPToolContent{
|
||||
{Type: "text", Text: "caption"},
|
||||
{Type: "image", Data: "imgdata", MediaType: "image/jpeg"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ResourceLink",
|
||||
input: &mcp.CallToolResult{
|
||||
Content: []mcp.Content{
|
||||
mcp.ResourceLink{
|
||||
Type: "resource_link",
|
||||
URI: "file:///tmp/test.txt",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: workspacesdk.CallMCPToolResponse{
|
||||
Content: []workspacesdk.MCPToolContent{
|
||||
{Type: "resource", Text: "[resource link: file:///tmp/test.txt]"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got := convertResult(tt.input)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -173,10 +173,7 @@ func Start(t *testing.T, inv *serpent.Invocation) {
|
||||
StartWithAssert(t, inv, nil)
|
||||
}
|
||||
|
||||
// StartWithAssert starts the given invocation and calls assertCallback
|
||||
// with the resulting error when the invocation completes. If assertCallback
|
||||
// is nil, expected shutdown errors are silently tolerated.
|
||||
func StartWithAssert(t *testing.T, inv *serpent.Invocation, assertCallback func(t *testing.T, err error)) {
|
||||
func StartWithAssert(t *testing.T, inv *serpent.Invocation, assertCallback func(t *testing.T, err error)) { //nolint:revive
|
||||
t.Helper()
|
||||
|
||||
closeCh := make(chan struct{})
|
||||
|
||||
@@ -173,6 +173,7 @@ func (selectModel) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
//nolint:revive // The linter complains about modifying 'm' but this is typical practice for bubbletea
|
||||
func (m selectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmd tea.Cmd
|
||||
|
||||
@@ -462,6 +463,7 @@ func (multiSelectModel) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
//nolint:revive // For same reason as previous Update definition
|
||||
func (m multiSelectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var cmd tea.Cmd
|
||||
|
||||
|
||||
@@ -1414,6 +1414,7 @@ func tailLineStyle() pretty.Style {
|
||||
return pretty.Style{pretty.Nop}
|
||||
}
|
||||
|
||||
//nolint:unused
|
||||
func SlimUnsupported(w io.Writer, cmd string) {
|
||||
_, _ = fmt.Fprintf(w, "You are using a 'slim' build of Coder, which does not support the %s subcommand.\n", pretty.Sprint(cliui.DefaultStyles.Code, cmd))
|
||||
_, _ = fmt.Fprintln(w, "")
|
||||
|
||||
+2
-13
@@ -352,6 +352,8 @@ func TestScheduleOverride(t *testing.T) {
|
||||
require.NoError(t, err, "invalid schedule")
|
||||
ownerClient, _, _, ws := setupTestSchedule(t, sched)
|
||||
now := time.Now()
|
||||
// To avoid the likelihood of time-related flakes, only matching up to the hour.
|
||||
expectedDeadline := now.In(loc).Add(10 * time.Hour).Format("2006-01-02T15:")
|
||||
|
||||
// When: we override the stop schedule
|
||||
inv, root := clitest.New(t,
|
||||
@@ -362,19 +364,6 @@ func TestScheduleOverride(t *testing.T) {
|
||||
pty := ptytest.New(t).Attach(inv)
|
||||
require.NoError(t, inv.Run())
|
||||
|
||||
// Fetch the workspace to get the actual deadline set by the
|
||||
// server. Computing our own expected deadline from a separately
|
||||
// captured time.Now() is racy: the CLI command calls time.Now()
|
||||
// internally, and with the Asia/Kolkata +05:30 offset the hour
|
||||
// boundary falls at :30 UTC minutes. A small delay between our
|
||||
// time.Now() and the command's is enough to land in different
|
||||
// hours.
|
||||
updated, err := ownerClient.Workspace(context.Background(), ws[0].ID)
|
||||
require.NoError(t, err)
|
||||
require.False(t, updated.LatestBuild.Deadline.IsZero(), "deadline should be set after extend")
|
||||
require.WithinDuration(t, now.Add(10*time.Hour), updated.LatestBuild.Deadline.Time, 5*time.Minute)
|
||||
expectedDeadline := updated.LatestBuild.Deadline.Time.In(loc).Format(time.RFC3339)
|
||||
|
||||
// Then: the updated schedule should be shown
|
||||
pty.ExpectMatch(ws[0].OwnerName + "/" + ws[0].Name)
|
||||
pty.ExpectMatch(sched.Humanize())
|
||||
|
||||
+5
-2
@@ -305,6 +305,7 @@ func enablePrometheus(
|
||||
}
|
||||
options.ProvisionerdServerMetrics = provisionerdserverMetrics
|
||||
|
||||
//nolint:revive
|
||||
return ServeHandler(
|
||||
ctx, logger, promhttp.InstrumentMetricHandler(
|
||||
options.PrometheusRegistry, promhttp.HandlerFor(options.PrometheusRegistry, promhttp.HandlerOpts{}),
|
||||
@@ -1636,6 +1637,8 @@ var defaultCipherSuites = func() []uint16 {
|
||||
// configureServerTLS returns the TLS config used for the Coderd server
|
||||
// connections to clients. A logger is passed in to allow printing warning
|
||||
// messages that do not block startup.
|
||||
//
|
||||
//nolint:revive
|
||||
func configureServerTLS(ctx context.Context, logger slog.Logger, tlsMinVersion, tlsClientAuth string, tlsCertFiles, tlsKeyFiles []string, tlsClientCAFile string, ciphers []string, allowInsecureCiphers bool) (*tls.Config, error) {
|
||||
tlsConfig := &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
@@ -2052,6 +2055,7 @@ func getGithubOAuth2ConfigParams(ctx context.Context, db database.Store, vals *c
|
||||
return ¶ms, nil
|
||||
}
|
||||
|
||||
//nolint:revive // Ignore flag-parameter: parameter 'allowEveryone' seems to be a control flag, avoid control coupling (revive)
|
||||
func configureGithubOAuth2(instrument *promoauth.Factory, params *githubOAuth2ConfigParams) (*coderd.GithubOAuth2Config, error) {
|
||||
redirectURL, err := params.accessURL.Parse("/api/v2/users/oauth2/github/callback")
|
||||
if err != nil {
|
||||
@@ -2327,8 +2331,7 @@ func ConfigureHTTPClient(ctx context.Context, clientCertFile, clientKeyFile stri
|
||||
return ctx, nil, err
|
||||
}
|
||||
|
||||
tlsClientConfig := &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
tlsClientConfig := &tls.Config{ //nolint:gosec
|
||||
Certificates: certificates,
|
||||
NextProtos: []string{"h2", "http/1.1"},
|
||||
}
|
||||
|
||||
@@ -2123,6 +2123,7 @@ func TestServer_TelemetryDisable(t *testing.T) {
|
||||
// Set the default telemetry to true (normally disabled in tests).
|
||||
t.Setenv("CODER_TEST_TELEMETRY_DEFAULT_ENABLE", "true")
|
||||
|
||||
//nolint:paralleltest // No need to reinitialise the variable tt (Go version).
|
||||
for _, tt := range []struct {
|
||||
key string
|
||||
val string
|
||||
|
||||
@@ -828,7 +828,7 @@ func TestTemplateEdit(t *testing.T) {
|
||||
"--require-active-version",
|
||||
}
|
||||
inv, root := clitest.New(t, cmdArgs...)
|
||||
//nolint:gocritic // Using owner client is required for template editing.
|
||||
//nolint
|
||||
clitest.SetupConfig(t, client, root)
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
@@ -858,7 +858,7 @@ func TestTemplateEdit(t *testing.T) {
|
||||
"--name", "something-new",
|
||||
}
|
||||
inv, root := clitest.New(t, cmdArgs...)
|
||||
//nolint:gocritic // Using owner client is required for template editing.
|
||||
//nolint
|
||||
clitest.SetupConfig(t, client, root)
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitLong)
|
||||
|
||||
+2
-4
@@ -17,8 +17,7 @@
|
||||
"name": "owner",
|
||||
"display_name": "Owner"
|
||||
}
|
||||
],
|
||||
"has_ai_seat": false
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "==========[second user ID]==========",
|
||||
@@ -32,7 +31,6 @@
|
||||
"organization_ids": [
|
||||
"===========[first org ID]==========="
|
||||
],
|
||||
"roles": [],
|
||||
"has_ai_seat": false
|
||||
"roles": []
|
||||
}
|
||||
]
|
||||
|
||||
+2
-7
@@ -857,18 +857,13 @@ aibridgeproxy:
|
||||
# Comma-separated list of AI provider domains for which HTTPS traffic will be
|
||||
# decrypted and routed through AI Bridge. Requests to other domains will be
|
||||
# tunneled directly without decryption. Supported domains: api.anthropic.com,
|
||||
# api.openai.com, api.individual.githubcopilot.com,
|
||||
# api.business.githubcopilot.com, api.enterprise.githubcopilot.com, chatgpt.com.
|
||||
# (default:
|
||||
# api.anthropic.com,api.openai.com,api.individual.githubcopilot.com,api.business.githubcopilot.com,api.enterprise.githubcopilot.com,chatgpt.com,
|
||||
# api.openai.com, api.individual.githubcopilot.com.
|
||||
# (default: api.anthropic.com,api.openai.com,api.individual.githubcopilot.com,
|
||||
# type: string-array)
|
||||
domain_allowlist:
|
||||
- api.anthropic.com
|
||||
- api.openai.com
|
||||
- api.individual.githubcopilot.com
|
||||
- api.business.githubcopilot.com
|
||||
- api.enterprise.githubcopilot.com
|
||||
- chatgpt.com
|
||||
# URL of an upstream HTTP proxy to chain tunneled (non-allowlisted) requests
|
||||
# through. Format: http://[user:pass@]host:port or https://[user:pass@]host:port.
|
||||
# (default: <unset>, type: string)
|
||||
|
||||
@@ -101,6 +101,7 @@ func TestConnectionLog(t *testing.T) {
|
||||
reason: "because error says so",
|
||||
},
|
||||
}
|
||||
//nolint:paralleltest // No longer necessary to reinitialise the variable tt.
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -3,7 +3,6 @@ package agentapi
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -61,8 +60,6 @@ func (a *MetadataAPI) BatchUpdateMetadata(ctx context.Context, req *agentproto.B
|
||||
}
|
||||
)
|
||||
for _, md := range req.Metadata {
|
||||
md.Result.Value = strings.TrimSpace(md.Result.Value)
|
||||
md.Result.Error = strings.TrimSpace(md.Result.Error)
|
||||
metadataError := md.Result.Error
|
||||
|
||||
allKeysLen += len(md.Key)
|
||||
|
||||
@@ -57,44 +57,16 @@ func TestBatchUpdateMetadata(t *testing.T) {
|
||||
CollectedAt: timestamppb.New(now.Add(-3 * time.Second)),
|
||||
Age: 3,
|
||||
Value: "",
|
||||
Error: "\t uncool error ",
|
||||
Error: "uncool value",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
batchSize := len(req.Metadata)
|
||||
// This test sends 2 metadata entries (one clean, one with
|
||||
// whitespace padding). With batch size 2 we expect exactly
|
||||
// 1 capacity flush. The matcher verifies that stored values
|
||||
// are trimmed while clean values pass through unchanged.
|
||||
expectedValues := map[string]string{
|
||||
"awesome key": "awesome value",
|
||||
"uncool key": "",
|
||||
}
|
||||
expectedErrors := map[string]string{
|
||||
"awesome key": "",
|
||||
"uncool key": "uncool error",
|
||||
}
|
||||
// This test sends 2 metadata entries. With batch size 2, we expect
|
||||
// exactly 1 capacity flush.
|
||||
store.EXPECT().
|
||||
BatchUpdateWorkspaceAgentMetadata(
|
||||
gomock.Any(),
|
||||
gomock.Cond(func(arg database.BatchUpdateWorkspaceAgentMetadataParams) bool {
|
||||
if len(arg.Key) != len(expectedValues) {
|
||||
return false
|
||||
}
|
||||
for i, key := range arg.Key {
|
||||
expVal, ok := expectedValues[key]
|
||||
if !ok || arg.Value[i] != expVal {
|
||||
return false
|
||||
}
|
||||
expErr, ok := expectedErrors[key]
|
||||
if !ok || arg.Error[i] != expErr {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}),
|
||||
).
|
||||
BatchUpdateWorkspaceAgentMetadata(gomock.Any(), gomock.Any()).
|
||||
Return(nil).
|
||||
Times(1)
|
||||
|
||||
|
||||
@@ -16,25 +16,6 @@ import (
|
||||
// that use per-user LLM credentials but cannot set custom headers.
|
||||
const HeaderCoderToken = "X-Coder-AI-Governance-Token" //nolint:gosec // This is a header name, not a credential.
|
||||
|
||||
// HeaderCoderRequestID is a header set by aibridgeproxyd on each
|
||||
// request forwarded to aibridged for cross-service log correlation.
|
||||
const HeaderCoderRequestID = "X-Coder-AI-Governance-Request-Id"
|
||||
|
||||
// Copilot provider.
|
||||
const (
|
||||
ProviderCopilotBusiness = "copilot-business"
|
||||
HostCopilotBusiness = "api.business.githubcopilot.com"
|
||||
ProviderCopilotEnterprise = "copilot-enterprise"
|
||||
HostCopilotEnterprise = "api.enterprise.githubcopilot.com"
|
||||
)
|
||||
|
||||
// ChatGPT provider.
|
||||
const (
|
||||
ProviderChatGPT = "chatgpt"
|
||||
HostChatGPT = "chatgpt.com"
|
||||
BaseURLChatGPT = "https://" + HostChatGPT + "/backend-api/codex"
|
||||
)
|
||||
|
||||
// IsBYOK reports whether the request is using BYOK mode, determined
|
||||
// by the presence of the X-Coder-AI-Governance-Token header.
|
||||
func IsBYOK(header http.Header) bool {
|
||||
|
||||
Generated
-276
@@ -84,34 +84,6 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"/aibridge/clients": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"AI Bridge"
|
||||
],
|
||||
"summary": "List AI Bridge clients",
|
||||
"operationId": "list-ai-bridge-clients",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/aibridge/interceptions": {
|
||||
"get": {
|
||||
"produces": [
|
||||
@@ -242,58 +214,6 @@ const docTemplate = `{
|
||||
]
|
||||
}
|
||||
},
|
||||
"/aibridge/sessions/{session_id}": {
|
||||
"get": {
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"AI Bridge"
|
||||
],
|
||||
"summary": "Get AI Bridge session threads",
|
||||
"operationId": "get-ai-bridge-session-threads",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Session ID (client_session_id or interception UUID)",
|
||||
"name": "session_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Thread pagination cursor (forward/older)",
|
||||
"name": "after_id",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Thread pagination cursor (backward/newer)",
|
||||
"name": "before_id",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Number of threads per page (default 50)",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.AIBridgeSessionThreadsResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/appearance": {
|
||||
"get": {
|
||||
"produces": [
|
||||
@@ -12755,29 +12675,6 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AIBridgeAgenticAction": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"thinking": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.AIBridgeModelThought"
|
||||
}
|
||||
},
|
||||
"token_usage": {
|
||||
"$ref": "#/definitions/codersdk.AIBridgeSessionThreadsTokenUsage"
|
||||
},
|
||||
"tool_calls": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.AIBridgeToolCall"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AIBridgeAnthropicConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -12946,14 +12843,6 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AIBridgeModelThought": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AIBridgeOpenAIConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -13053,76 +12942,6 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AIBridgeSessionThreadsResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"client": {
|
||||
"type": "string"
|
||||
},
|
||||
"ended_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"initiator": {
|
||||
"$ref": "#/definitions/codersdk.MinimalUser"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"additionalProperties": {}
|
||||
},
|
||||
"models": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"page_ended_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"page_started_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"providers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"started_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"threads": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.AIBridgeThread"
|
||||
}
|
||||
},
|
||||
"token_usage_summary": {
|
||||
"$ref": "#/definitions/codersdk.AIBridgeSessionThreadsTokenUsage"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AIBridgeSessionThreadsTokenUsage": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"input_tokens": {
|
||||
"type": "integer"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"additionalProperties": {}
|
||||
},
|
||||
"output_tokens": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AIBridgeSessionTokenUsageSummary": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -13134,41 +12953,6 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AIBridgeThread": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"agentic_actions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.AIBridgeAgenticAction"
|
||||
}
|
||||
},
|
||||
"ended_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"prompt": {
|
||||
"type": "string"
|
||||
},
|
||||
"provider": {
|
||||
"type": "string"
|
||||
},
|
||||
"started_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"token_usage": {
|
||||
"$ref": "#/definitions/codersdk.AIBridgeSessionThreadsTokenUsage"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AIBridgeTokenUsage": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -13199,42 +12983,6 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AIBridgeToolCall": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"injected": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"input": {
|
||||
"type": "string"
|
||||
},
|
||||
"interception_id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"additionalProperties": {}
|
||||
},
|
||||
"provider_response_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"server_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"tool": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AIBridgeToolUsage": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -13445,11 +13193,6 @@ const docTemplate = `{
|
||||
"chat:delete",
|
||||
"chat:read",
|
||||
"chat:update",
|
||||
"chat_automation:*",
|
||||
"chat_automation:create",
|
||||
"chat_automation:delete",
|
||||
"chat_automation:read",
|
||||
"chat_automation:update",
|
||||
"coder:all",
|
||||
"coder:apikeys.manage_self",
|
||||
"coder:application_connect",
|
||||
@@ -13659,11 +13402,6 @@ const docTemplate = `{
|
||||
"APIKeyScopeChatDelete",
|
||||
"APIKeyScopeChatRead",
|
||||
"APIKeyScopeChatUpdate",
|
||||
"APIKeyScopeChatAutomationAll",
|
||||
"APIKeyScopeChatAutomationCreate",
|
||||
"APIKeyScopeChatAutomationDelete",
|
||||
"APIKeyScopeChatAutomationRead",
|
||||
"APIKeyScopeChatAutomationUpdate",
|
||||
"APIKeyScopeCoderAll",
|
||||
"APIKeyScopeCoderApikeysManageSelf",
|
||||
"APIKeyScopeCoderApplicationConnect",
|
||||
@@ -17688,10 +17426,6 @@ const docTemplate = `{
|
||||
"$ref": "#/definitions/codersdk.SlimRole"
|
||||
}
|
||||
},
|
||||
"has_ai_seat": {
|
||||
"description": "HasAISeat intentionally omits omitempty so the API always includes the\nfield, even when false.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"is_service_account": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -19048,7 +18782,6 @@ const docTemplate = `{
|
||||
"audit_log",
|
||||
"boundary_usage",
|
||||
"chat",
|
||||
"chat_automation",
|
||||
"connection_log",
|
||||
"crypto_key",
|
||||
"debug_info",
|
||||
@@ -19095,7 +18828,6 @@ const docTemplate = `{
|
||||
"ResourceAuditLog",
|
||||
"ResourceBoundaryUsage",
|
||||
"ResourceChat",
|
||||
"ResourceChatAutomation",
|
||||
"ResourceConnectionLog",
|
||||
"ResourceCryptoKey",
|
||||
"ResourceDebugInfo",
|
||||
@@ -20490,10 +20222,6 @@ const docTemplate = `{
|
||||
"type": "string",
|
||||
"format": "email"
|
||||
},
|
||||
"has_ai_seat": {
|
||||
"description": "HasAISeat intentionally omits omitempty so the API always includes the\nfield, even when false.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
@@ -21343,10 +21071,6 @@ const docTemplate = `{
|
||||
"type": "string",
|
||||
"format": "email"
|
||||
},
|
||||
"has_ai_seat": {
|
||||
"description": "HasAISeat intentionally omits omitempty so the API always includes the\nfield, even when false.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
|
||||
Generated
-268
@@ -65,30 +65,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/aibridge/clients": {
|
||||
"get": {
|
||||
"produces": ["application/json"],
|
||||
"tags": ["AI Bridge"],
|
||||
"summary": "List AI Bridge clients",
|
||||
"operationId": "list-ai-bridge-clients",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/aibridge/interceptions": {
|
||||
"get": {
|
||||
"produces": ["application/json"],
|
||||
@@ -207,54 +183,6 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/aibridge/sessions/{session_id}": {
|
||||
"get": {
|
||||
"produces": ["application/json"],
|
||||
"tags": ["AI Bridge"],
|
||||
"summary": "Get AI Bridge session threads",
|
||||
"operationId": "get-ai-bridge-session-threads",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Session ID (client_session_id or interception UUID)",
|
||||
"name": "session_id",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Thread pagination cursor (forward/older)",
|
||||
"name": "after_id",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Thread pagination cursor (backward/newer)",
|
||||
"name": "before_id",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "integer",
|
||||
"description": "Number of threads per page (default 50)",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/codersdk.AIBridgeSessionThreadsResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/appearance": {
|
||||
"get": {
|
||||
"produces": ["application/json"],
|
||||
@@ -11333,29 +11261,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AIBridgeAgenticAction": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"thinking": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.AIBridgeModelThought"
|
||||
}
|
||||
},
|
||||
"token_usage": {
|
||||
"$ref": "#/definitions/codersdk.AIBridgeSessionThreadsTokenUsage"
|
||||
},
|
||||
"tool_calls": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.AIBridgeToolCall"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AIBridgeAnthropicConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -11524,14 +11429,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AIBridgeModelThought": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AIBridgeOpenAIConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -11631,76 +11528,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AIBridgeSessionThreadsResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"client": {
|
||||
"type": "string"
|
||||
},
|
||||
"ended_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"initiator": {
|
||||
"$ref": "#/definitions/codersdk.MinimalUser"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"additionalProperties": {}
|
||||
},
|
||||
"models": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"page_ended_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"page_started_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"providers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"started_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"threads": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.AIBridgeThread"
|
||||
}
|
||||
},
|
||||
"token_usage_summary": {
|
||||
"$ref": "#/definitions/codersdk.AIBridgeSessionThreadsTokenUsage"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AIBridgeSessionThreadsTokenUsage": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"input_tokens": {
|
||||
"type": "integer"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"additionalProperties": {}
|
||||
},
|
||||
"output_tokens": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AIBridgeSessionTokenUsageSummary": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -11712,41 +11539,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AIBridgeThread": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"agentic_actions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.AIBridgeAgenticAction"
|
||||
}
|
||||
},
|
||||
"ended_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"model": {
|
||||
"type": "string"
|
||||
},
|
||||
"prompt": {
|
||||
"type": "string"
|
||||
},
|
||||
"provider": {
|
||||
"type": "string"
|
||||
},
|
||||
"started_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"token_usage": {
|
||||
"$ref": "#/definitions/codersdk.AIBridgeSessionThreadsTokenUsage"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AIBridgeTokenUsage": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -11777,42 +11569,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AIBridgeToolCall": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"injected": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"input": {
|
||||
"type": "string"
|
||||
},
|
||||
"interception_id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"additionalProperties": {}
|
||||
},
|
||||
"provider_response_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"server_url": {
|
||||
"type": "string"
|
||||
},
|
||||
"tool": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AIBridgeToolUsage": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -12015,11 +11771,6 @@
|
||||
"chat:delete",
|
||||
"chat:read",
|
||||
"chat:update",
|
||||
"chat_automation:*",
|
||||
"chat_automation:create",
|
||||
"chat_automation:delete",
|
||||
"chat_automation:read",
|
||||
"chat_automation:update",
|
||||
"coder:all",
|
||||
"coder:apikeys.manage_self",
|
||||
"coder:application_connect",
|
||||
@@ -12229,11 +11980,6 @@
|
||||
"APIKeyScopeChatDelete",
|
||||
"APIKeyScopeChatRead",
|
||||
"APIKeyScopeChatUpdate",
|
||||
"APIKeyScopeChatAutomationAll",
|
||||
"APIKeyScopeChatAutomationCreate",
|
||||
"APIKeyScopeChatAutomationDelete",
|
||||
"APIKeyScopeChatAutomationRead",
|
||||
"APIKeyScopeChatAutomationUpdate",
|
||||
"APIKeyScopeCoderAll",
|
||||
"APIKeyScopeCoderApikeysManageSelf",
|
||||
"APIKeyScopeCoderApplicationConnect",
|
||||
@@ -16105,10 +15851,6 @@
|
||||
"$ref": "#/definitions/codersdk.SlimRole"
|
||||
}
|
||||
},
|
||||
"has_ai_seat": {
|
||||
"description": "HasAISeat intentionally omits omitempty so the API always includes the\nfield, even when false.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"is_service_account": {
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -17420,7 +17162,6 @@
|
||||
"audit_log",
|
||||
"boundary_usage",
|
||||
"chat",
|
||||
"chat_automation",
|
||||
"connection_log",
|
||||
"crypto_key",
|
||||
"debug_info",
|
||||
@@ -17467,7 +17208,6 @@
|
||||
"ResourceAuditLog",
|
||||
"ResourceBoundaryUsage",
|
||||
"ResourceChat",
|
||||
"ResourceChatAutomation",
|
||||
"ResourceConnectionLog",
|
||||
"ResourceCryptoKey",
|
||||
"ResourceDebugInfo",
|
||||
@@ -18807,10 +18547,6 @@
|
||||
"type": "string",
|
||||
"format": "email"
|
||||
},
|
||||
"has_ai_seat": {
|
||||
"description": "HasAISeat intentionally omits omitempty so the API always includes the\nfield, even when false.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
@@ -19603,10 +19339,6 @@
|
||||
"type": "string",
|
||||
"format": "email"
|
||||
},
|
||||
"has_ai_seat": {
|
||||
"description": "HasAISeat intentionally omits omitempty so the API always includes the\nfield, even when false.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
|
||||
+1
-1
@@ -220,7 +220,7 @@ func (api *API) checkAuthorization(rw http.ResponseWriter, r *http.Request) {
|
||||
Type: string(v.Object.ResourceType),
|
||||
AnyOrgOwner: v.Object.AnyOrgOwner,
|
||||
}
|
||||
if obj.Owner == codersdk.Me {
|
||||
if obj.Owner == "me" {
|
||||
obj.Owner = auth.ID
|
||||
}
|
||||
|
||||
|
||||
@@ -1155,7 +1155,6 @@ func New(options *Options) *API {
|
||||
apiKeyMiddleware,
|
||||
httpmw.RequireExperimentWithDevBypass(api.Experiments, codersdk.ExperimentAgents),
|
||||
)
|
||||
r.Get("/by-workspace", api.chatsByWorkspace)
|
||||
r.Get("/", api.listChats)
|
||||
r.Post("/", api.postChats)
|
||||
r.Get("/models", api.listChatModels)
|
||||
@@ -1234,7 +1233,6 @@ func New(options *Options) *API {
|
||||
r.Get("/git", api.watchChatGit)
|
||||
})
|
||||
r.Post("/interrupt", api.interruptChat)
|
||||
r.Post("/title/regenerate", api.regenerateChatTitle)
|
||||
r.Get("/diff", api.getChatDiffContents)
|
||||
r.Route("/queue/{queuedMessage}", func(r chi.Router) {
|
||||
r.Delete("/", api.deleteChatQueuedMessage)
|
||||
|
||||
@@ -7,11 +7,6 @@ type CheckConstraint string
|
||||
// CheckConstraint enums.
|
||||
const (
|
||||
CheckAPIKeysAllowListNotEmpty CheckConstraint = "api_keys_allow_list_not_empty" // api_keys
|
||||
CheckChatAutomationEventsChatExclusivity CheckConstraint = "chat_automation_events_chat_exclusivity" // chat_automation_events
|
||||
CheckChatAutomationTriggersCronFields CheckConstraint = "chat_automation_triggers_cron_fields" // chat_automation_triggers
|
||||
CheckChatAutomationTriggersWebhookFields CheckConstraint = "chat_automation_triggers_webhook_fields" // chat_automation_triggers
|
||||
CheckChatAutomationsMaxChatCreatesPerHourCheck CheckConstraint = "chat_automations_max_chat_creates_per_hour_check" // chat_automations
|
||||
CheckChatAutomationsMaxMessagesPerHourCheck CheckConstraint = "chat_automations_max_messages_per_hour_check" // chat_automations
|
||||
CheckChatModelConfigsCompressionThresholdCheck CheckConstraint = "chat_model_configs_compression_threshold_check" // chat_model_configs
|
||||
CheckChatModelConfigsContextLimitCheck CheckConstraint = "chat_model_configs_context_limit_check" // chat_model_configs
|
||||
CheckChatProvidersProviderCheck CheckConstraint = "chat_providers_provider_check" // chat_providers
|
||||
|
||||
@@ -1097,287 +1097,6 @@ func AIBridgeToolUsage(usage database.AIBridgeToolUsage) codersdk.AIBridgeToolUs
|
||||
}
|
||||
}
|
||||
|
||||
// AIBridgeSessionThreads converts session metadata and thread interceptions
|
||||
// into the threads response. It groups interceptions into threads, builds
|
||||
// agentic actions from tool usages and model thoughts, and aggregates
|
||||
// token usage with metadata.
|
||||
func AIBridgeSessionThreads(
|
||||
session database.ListAIBridgeSessionsRow,
|
||||
interceptions []database.ListAIBridgeSessionThreadsRow,
|
||||
tokenUsages []database.AIBridgeTokenUsage,
|
||||
toolUsages []database.AIBridgeToolUsage,
|
||||
userPrompts []database.AIBridgeUserPrompt,
|
||||
modelThoughts []database.AIBridgeModelThought,
|
||||
) codersdk.AIBridgeSessionThreadsResponse {
|
||||
// Index subresources by interception ID.
|
||||
tokensByInterception := make(map[uuid.UUID][]database.AIBridgeTokenUsage, len(interceptions))
|
||||
for _, tu := range tokenUsages {
|
||||
tokensByInterception[tu.InterceptionID] = append(tokensByInterception[tu.InterceptionID], tu)
|
||||
}
|
||||
toolsByInterception := make(map[uuid.UUID][]database.AIBridgeToolUsage, len(interceptions))
|
||||
for _, tu := range toolUsages {
|
||||
toolsByInterception[tu.InterceptionID] = append(toolsByInterception[tu.InterceptionID], tu)
|
||||
}
|
||||
promptsByInterception := make(map[uuid.UUID][]database.AIBridgeUserPrompt, len(interceptions))
|
||||
for _, up := range userPrompts {
|
||||
promptsByInterception[up.InterceptionID] = append(promptsByInterception[up.InterceptionID], up)
|
||||
}
|
||||
thoughtsByInterception := make(map[uuid.UUID][]database.AIBridgeModelThought, len(interceptions))
|
||||
for _, mt := range modelThoughts {
|
||||
thoughtsByInterception[mt.InterceptionID] = append(thoughtsByInterception[mt.InterceptionID], mt)
|
||||
}
|
||||
|
||||
// Group interceptions by thread_id, preserving the order returned by the
|
||||
// SQL query.
|
||||
interceptionsByThread := make(map[uuid.UUID][]database.AIBridgeInterception, len(interceptions))
|
||||
var threadIDs []uuid.UUID
|
||||
for _, row := range interceptions {
|
||||
if _, ok := interceptionsByThread[row.ThreadID]; !ok {
|
||||
threadIDs = append(threadIDs, row.ThreadID)
|
||||
}
|
||||
interceptionsByThread[row.ThreadID] = append(interceptionsByThread[row.ThreadID], row.AIBridgeInterception)
|
||||
}
|
||||
|
||||
// Build threads and track page time bounds.
|
||||
threads := make([]codersdk.AIBridgeThread, 0, len(threadIDs))
|
||||
var pageStartedAt, pageEndedAt *time.Time
|
||||
for _, threadID := range threadIDs {
|
||||
intcs := interceptionsByThread[threadID]
|
||||
thread := buildAIBridgeThread(threadID, intcs, tokensByInterception, toolsByInterception, promptsByInterception, thoughtsByInterception)
|
||||
for _, intc := range intcs {
|
||||
if pageStartedAt == nil || intc.StartedAt.Before(*pageStartedAt) {
|
||||
t := intc.StartedAt
|
||||
pageStartedAt = &t
|
||||
}
|
||||
if intc.EndedAt.Valid {
|
||||
if pageEndedAt == nil || intc.EndedAt.Time.After(*pageEndedAt) {
|
||||
t := intc.EndedAt.Time
|
||||
pageEndedAt = &t
|
||||
}
|
||||
}
|
||||
}
|
||||
threads = append(threads, thread)
|
||||
}
|
||||
|
||||
// Aggregate session-level token usage metadata from all token
|
||||
// usages in the session (not just the page).
|
||||
sessionTokenMeta := aggregateTokenMetadata(tokenUsages)
|
||||
|
||||
resp := codersdk.AIBridgeSessionThreadsResponse{
|
||||
ID: session.SessionID,
|
||||
Initiator: MinimalUserFromVisibleUser(database.VisibleUser{
|
||||
ID: session.UserID,
|
||||
Username: session.UserUsername,
|
||||
Name: session.UserName,
|
||||
AvatarURL: session.UserAvatarUrl,
|
||||
}),
|
||||
Providers: session.Providers,
|
||||
Models: session.Models,
|
||||
Metadata: jsonOrEmptyMap(pqtype.NullRawMessage{RawMessage: session.Metadata, Valid: len(session.Metadata) > 0}),
|
||||
StartedAt: session.StartedAt,
|
||||
PageStartedAt: pageStartedAt,
|
||||
PageEndedAt: pageEndedAt,
|
||||
TokenUsageSummary: codersdk.AIBridgeSessionThreadsTokenUsage{
|
||||
InputTokens: session.InputTokens,
|
||||
OutputTokens: session.OutputTokens,
|
||||
Metadata: sessionTokenMeta,
|
||||
},
|
||||
Threads: threads,
|
||||
}
|
||||
if resp.Providers == nil {
|
||||
resp.Providers = []string{}
|
||||
}
|
||||
if resp.Models == nil {
|
||||
resp.Models = []string{}
|
||||
}
|
||||
if session.Client != "" {
|
||||
resp.Client = &session.Client
|
||||
}
|
||||
if !session.EndedAt.IsZero() {
|
||||
resp.EndedAt = &session.EndedAt
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
func buildAIBridgeThread(
|
||||
threadID uuid.UUID,
|
||||
interceptions []database.AIBridgeInterception,
|
||||
tokensByInterception map[uuid.UUID][]database.AIBridgeTokenUsage,
|
||||
toolsByInterception map[uuid.UUID][]database.AIBridgeToolUsage,
|
||||
promptsByInterception map[uuid.UUID][]database.AIBridgeUserPrompt,
|
||||
thoughtsByInterception map[uuid.UUID][]database.AIBridgeModelThought,
|
||||
) codersdk.AIBridgeThread {
|
||||
// Find the root interception (where id == threadID) to get the
|
||||
// thread prompt and model.
|
||||
var rootIntc *database.AIBridgeInterception
|
||||
for i := range interceptions {
|
||||
if interceptions[i].ID == threadID {
|
||||
rootIntc = &interceptions[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
// Fallback to first interception if root not found.
|
||||
if rootIntc == nil && len(interceptions) > 0 {
|
||||
rootIntc = &interceptions[0]
|
||||
}
|
||||
|
||||
thread := codersdk.AIBridgeThread{
|
||||
ID: threadID,
|
||||
}
|
||||
if rootIntc != nil {
|
||||
thread.Model = rootIntc.Model
|
||||
thread.Provider = rootIntc.Provider
|
||||
// Get first user prompt from root interception.
|
||||
// A thread can only have one prompt, by definition, since we currently
|
||||
// only store the last prompt observed in an interception.
|
||||
if prompts := promptsByInterception[rootIntc.ID]; len(prompts) > 0 {
|
||||
thread.Prompt = &prompts[0].Prompt
|
||||
}
|
||||
}
|
||||
|
||||
// Compute thread time bounds from interceptions.
|
||||
for _, intc := range interceptions {
|
||||
if thread.StartedAt.IsZero() || intc.StartedAt.Before(thread.StartedAt) {
|
||||
thread.StartedAt = intc.StartedAt
|
||||
}
|
||||
if intc.EndedAt.Valid {
|
||||
if thread.EndedAt == nil || intc.EndedAt.Time.After(*thread.EndedAt) {
|
||||
t := intc.EndedAt.Time
|
||||
thread.EndedAt = &t
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build agentic actions grouped by interception. Each interception that
|
||||
// has tool calls produces one action with all its tool calls, thinking
|
||||
// blocks, and token usage.
|
||||
var actions []codersdk.AIBridgeAgenticAction
|
||||
for _, intc := range interceptions {
|
||||
tools := toolsByInterception[intc.ID]
|
||||
if len(tools) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Thinking blocks for this interception.
|
||||
thoughts := thoughtsByInterception[intc.ID]
|
||||
thinking := make([]codersdk.AIBridgeModelThought, 0, len(thoughts))
|
||||
for _, mt := range thoughts {
|
||||
thinking = append(thinking, codersdk.AIBridgeModelThought{
|
||||
Text: mt.Content,
|
||||
})
|
||||
}
|
||||
|
||||
// Token usage for the interception.
|
||||
actionTokenUsage := aggregateTokenUsage(tokensByInterception[intc.ID])
|
||||
|
||||
// Build tool call list.
|
||||
toolCalls := make([]codersdk.AIBridgeToolCall, 0, len(tools))
|
||||
for _, tu := range tools {
|
||||
toolCalls = append(toolCalls, codersdk.AIBridgeToolCall{
|
||||
ID: tu.ID,
|
||||
InterceptionID: tu.InterceptionID,
|
||||
ProviderResponseID: tu.ProviderResponseID,
|
||||
ServerURL: tu.ServerUrl.String,
|
||||
Tool: tu.Tool,
|
||||
Injected: tu.Injected,
|
||||
Input: tu.Input,
|
||||
Metadata: jsonOrEmptyMap(tu.Metadata),
|
||||
CreatedAt: tu.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
actions = append(actions, codersdk.AIBridgeAgenticAction{
|
||||
Model: intc.Model,
|
||||
TokenUsage: actionTokenUsage,
|
||||
Thinking: thinking,
|
||||
ToolCalls: toolCalls,
|
||||
})
|
||||
}
|
||||
|
||||
if actions == nil {
|
||||
// Make an empty slice so we don't serialize `null`.
|
||||
actions = make([]codersdk.AIBridgeAgenticAction, 0)
|
||||
}
|
||||
|
||||
thread.AgenticActions = actions
|
||||
|
||||
// Aggregate thread-level token usage.
|
||||
var threadTokens []database.AIBridgeTokenUsage
|
||||
for _, intc := range interceptions {
|
||||
threadTokens = append(threadTokens, tokensByInterception[intc.ID]...)
|
||||
}
|
||||
thread.TokenUsage = aggregateTokenUsage(threadTokens)
|
||||
|
||||
return thread
|
||||
}
|
||||
|
||||
// aggregateTokenUsage sums token usage rows and aggregates metadata.
|
||||
func aggregateTokenUsage(tokens []database.AIBridgeTokenUsage) codersdk.AIBridgeSessionThreadsTokenUsage {
|
||||
var inputTokens, outputTokens int64
|
||||
for _, tu := range tokens {
|
||||
inputTokens += tu.InputTokens
|
||||
outputTokens += tu.OutputTokens
|
||||
// TODO: once https://github.com/coder/aibridge/issues/150 lands we
|
||||
// should aggregate the other token types.
|
||||
}
|
||||
return codersdk.AIBridgeSessionThreadsTokenUsage{
|
||||
InputTokens: inputTokens,
|
||||
OutputTokens: outputTokens,
|
||||
Metadata: aggregateTokenMetadata(tokens),
|
||||
}
|
||||
}
|
||||
|
||||
// aggregateTokenMetadata sums all numeric values from the metadata
|
||||
// JSONB across the given token usage rows by key. Nested objects are
|
||||
// flattened using dot-notation (e.g. {"cache": {"read_tokens": 10}}
|
||||
// becomes "cache.read_tokens"). Non-numeric leaves (strings,
|
||||
// booleans, arrays, nulls) are silently skipped.
|
||||
func aggregateTokenMetadata(tokens []database.AIBridgeTokenUsage) map[string]any {
|
||||
sums := make(map[string]int64)
|
||||
for _, tu := range tokens {
|
||||
if !tu.Metadata.Valid || len(tu.Metadata.RawMessage) == 0 {
|
||||
continue
|
||||
}
|
||||
var m map[string]json.RawMessage
|
||||
if err := json.Unmarshal(tu.Metadata.RawMessage, &m); err != nil {
|
||||
continue
|
||||
}
|
||||
flattenAndSum(sums, "", m)
|
||||
}
|
||||
result := make(map[string]any, len(sums))
|
||||
for k, v := range sums {
|
||||
result[k] = v
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// flattenAndSum recursively walks a JSON object and sums all numeric
|
||||
// leaf values into sums, using dot-separated keys for nested objects.
|
||||
func flattenAndSum(sums map[string]int64, prefix string, m map[string]json.RawMessage) {
|
||||
for k, raw := range m {
|
||||
key := k
|
||||
if prefix != "" {
|
||||
key = prefix + "." + k
|
||||
}
|
||||
|
||||
// Try as a number first.
|
||||
var n json.Number
|
||||
if err := json.Unmarshal(raw, &n); err == nil {
|
||||
if v, err := n.Int64(); err == nil {
|
||||
sums[key] += v
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Try as a nested object.
|
||||
var nested map[string]json.RawMessage
|
||||
if err := json.Unmarshal(raw, &nested); err == nil {
|
||||
flattenAndSum(sums, key, nested)
|
||||
}
|
||||
// Arrays, strings, booleans, nulls are skipped.
|
||||
}
|
||||
}
|
||||
|
||||
func InvalidatedPresets(invalidatedPresets []database.UpdatePresetsLastInvalidatedAtRow) []codersdk.InvalidatedPreset {
|
||||
var presets []codersdk.InvalidatedPreset
|
||||
for _, p := range invalidatedPresets {
|
||||
@@ -1516,98 +1235,6 @@ func nullInt64Ptr(v sql.NullInt64) *int64 {
|
||||
return &value
|
||||
}
|
||||
|
||||
// Chat converts a database.Chat to a codersdk.Chat. It coalesces
|
||||
// nil slices and maps to empty values for JSON serialization and
|
||||
// derives RootChatID from the parent chain when not explicitly set.
|
||||
func Chat(c database.Chat, diffStatus *database.ChatDiffStatus) codersdk.Chat {
|
||||
mcpServerIDs := c.MCPServerIDs
|
||||
if mcpServerIDs == nil {
|
||||
mcpServerIDs = []uuid.UUID{}
|
||||
}
|
||||
labels := map[string]string(c.Labels)
|
||||
if labels == nil {
|
||||
labels = map[string]string{}
|
||||
}
|
||||
chat := codersdk.Chat{
|
||||
ID: c.ID,
|
||||
OwnerID: c.OwnerID,
|
||||
LastModelConfigID: c.LastModelConfigID,
|
||||
Title: c.Title,
|
||||
Status: codersdk.ChatStatus(c.Status),
|
||||
Archived: c.Archived,
|
||||
PinOrder: c.PinOrder,
|
||||
CreatedAt: c.CreatedAt,
|
||||
UpdatedAt: c.UpdatedAt,
|
||||
MCPServerIDs: mcpServerIDs,
|
||||
Labels: labels,
|
||||
}
|
||||
if c.LastError.Valid {
|
||||
chat.LastError = &c.LastError.String
|
||||
}
|
||||
if c.ParentChatID.Valid {
|
||||
parentChatID := c.ParentChatID.UUID
|
||||
chat.ParentChatID = &parentChatID
|
||||
}
|
||||
switch {
|
||||
case c.RootChatID.Valid:
|
||||
rootChatID := c.RootChatID.UUID
|
||||
chat.RootChatID = &rootChatID
|
||||
case c.ParentChatID.Valid:
|
||||
rootChatID := c.ParentChatID.UUID
|
||||
chat.RootChatID = &rootChatID
|
||||
default:
|
||||
rootChatID := c.ID
|
||||
chat.RootChatID = &rootChatID
|
||||
}
|
||||
if c.WorkspaceID.Valid {
|
||||
chat.WorkspaceID = &c.WorkspaceID.UUID
|
||||
}
|
||||
if c.BuildID.Valid {
|
||||
chat.BuildID = &c.BuildID.UUID
|
||||
}
|
||||
if c.AgentID.Valid {
|
||||
chat.AgentID = &c.AgentID.UUID
|
||||
}
|
||||
if diffStatus != nil {
|
||||
convertedDiffStatus := ChatDiffStatus(c.ID, diffStatus)
|
||||
chat.DiffStatus = &convertedDiffStatus
|
||||
}
|
||||
if c.LastInjectedContext.Valid {
|
||||
var parts []codersdk.ChatMessagePart
|
||||
// Internal fields are stripped at write time in
|
||||
// chatd.updateLastInjectedContext, so no
|
||||
// StripInternal call is needed here. Unmarshal
|
||||
// errors are suppressed — the column is written by
|
||||
// us with a known schema.
|
||||
if err := json.Unmarshal(c.LastInjectedContext.RawMessage, &parts); err == nil {
|
||||
chat.LastInjectedContext = parts
|
||||
}
|
||||
}
|
||||
return chat
|
||||
}
|
||||
|
||||
// ChatRows converts a slice of database.GetChatsRow (which embeds
|
||||
// Chat plus HasUnread) to codersdk.Chat, looking up diff statuses
|
||||
// from the provided map. When diffStatusesByChatID is non-nil,
|
||||
// chats without an entry receive an empty DiffStatus.
|
||||
func ChatRows(rows []database.GetChatsRow, diffStatusesByChatID map[uuid.UUID]database.ChatDiffStatus) []codersdk.Chat {
|
||||
result := make([]codersdk.Chat, len(rows))
|
||||
for i, row := range rows {
|
||||
diffStatus, ok := diffStatusesByChatID[row.Chat.ID]
|
||||
if ok {
|
||||
result[i] = Chat(row.Chat, &diffStatus)
|
||||
} else {
|
||||
result[i] = Chat(row.Chat, nil)
|
||||
if diffStatusesByChatID != nil {
|
||||
emptyDiffStatus := ChatDiffStatus(row.Chat.ID, nil)
|
||||
result[i].DiffStatus = &emptyDiffStatus
|
||||
}
|
||||
}
|
||||
result[i].HasUnread = row.HasUnread
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// ChatDiffStatus converts a database.ChatDiffStatus to a
|
||||
// codersdk.ChatDiffStatus. When status is nil an empty value
|
||||
// containing only the chatID is returned.
|
||||
|
||||
@@ -1,308 +0,0 @@
|
||||
package db2sdk
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/sqlc-dev/pqtype"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
)
|
||||
|
||||
func TestAggregateTokenMetadata(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("empty_input", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
result := aggregateTokenMetadata(nil)
|
||||
require.Empty(t, result)
|
||||
})
|
||||
|
||||
t.Run("sums_across_rows", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
tokens := []database.AIBridgeTokenUsage{
|
||||
{
|
||||
ID: uuid.New(),
|
||||
Metadata: pqtype.NullRawMessage{
|
||||
RawMessage: json.RawMessage(`{"cache_read_tokens":100,"reasoning_tokens":50}`),
|
||||
Valid: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: uuid.New(),
|
||||
Metadata: pqtype.NullRawMessage{
|
||||
RawMessage: json.RawMessage(`{"cache_read_tokens":200,"reasoning_tokens":75}`),
|
||||
Valid: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result := aggregateTokenMetadata(tokens)
|
||||
require.Equal(t, int64(300), result["cache_read_tokens"])
|
||||
require.Equal(t, int64(125), result["reasoning_tokens"])
|
||||
require.Len(t, result, 2)
|
||||
})
|
||||
|
||||
t.Run("skips_null_and_invalid_metadata", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
tokens := []database.AIBridgeTokenUsage{
|
||||
{
|
||||
ID: uuid.New(),
|
||||
Metadata: pqtype.NullRawMessage{Valid: false},
|
||||
},
|
||||
{
|
||||
ID: uuid.New(),
|
||||
Metadata: pqtype.NullRawMessage{
|
||||
RawMessage: nil,
|
||||
Valid: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: uuid.New(),
|
||||
Metadata: pqtype.NullRawMessage{
|
||||
RawMessage: json.RawMessage(`{"tokens":42}`),
|
||||
Valid: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result := aggregateTokenMetadata(tokens)
|
||||
require.Equal(t, int64(42), result["tokens"])
|
||||
require.Len(t, result, 1)
|
||||
})
|
||||
|
||||
t.Run("skips_non_integer_values", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
tokens := []database.AIBridgeTokenUsage{
|
||||
{
|
||||
ID: uuid.New(),
|
||||
Metadata: pqtype.NullRawMessage{
|
||||
// Float values fail json.Number.Int64(), so they
|
||||
// are silently dropped.
|
||||
RawMessage: json.RawMessage(`{"good":10,"fractional":1.5}`),
|
||||
Valid: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result := aggregateTokenMetadata(tokens)
|
||||
require.Equal(t, int64(10), result["good"])
|
||||
_, hasFractional := result["fractional"]
|
||||
require.False(t, hasFractional)
|
||||
})
|
||||
|
||||
t.Run("skips_malformed_json", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
tokens := []database.AIBridgeTokenUsage{
|
||||
{
|
||||
ID: uuid.New(),
|
||||
Metadata: pqtype.NullRawMessage{
|
||||
RawMessage: json.RawMessage(`not json`),
|
||||
Valid: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: uuid.New(),
|
||||
Metadata: pqtype.NullRawMessage{
|
||||
RawMessage: json.RawMessage(`{"tokens":5}`),
|
||||
Valid: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result := aggregateTokenMetadata(tokens)
|
||||
// The malformed row is skipped, the valid one is counted.
|
||||
require.Equal(t, int64(5), result["tokens"])
|
||||
require.Len(t, result, 1)
|
||||
})
|
||||
|
||||
t.Run("flattens_nested_objects", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
tokens := []database.AIBridgeTokenUsage{
|
||||
{
|
||||
ID: uuid.New(),
|
||||
Metadata: pqtype.NullRawMessage{
|
||||
RawMessage: json.RawMessage(`{
|
||||
"cache_read_tokens": 100,
|
||||
"cache": {"creation_tokens": 40, "read_tokens": 60},
|
||||
"reasoning_tokens": 50,
|
||||
"tags": ["a", "b"]
|
||||
}`),
|
||||
Valid: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: uuid.New(),
|
||||
Metadata: pqtype.NullRawMessage{
|
||||
RawMessage: json.RawMessage(`{
|
||||
"cache_read_tokens": 200,
|
||||
"cache": {"creation_tokens": 10}
|
||||
}`),
|
||||
Valid: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result := aggregateTokenMetadata(tokens)
|
||||
require.Equal(t, int64(300), result["cache_read_tokens"])
|
||||
require.Equal(t, int64(50), result["reasoning_tokens"])
|
||||
require.Equal(t, int64(50), result["cache.creation_tokens"])
|
||||
require.Equal(t, int64(60), result["cache.read_tokens"])
|
||||
// Arrays are skipped.
|
||||
_, hasTags := result["tags"]
|
||||
require.False(t, hasTags)
|
||||
require.Len(t, result, 4)
|
||||
})
|
||||
|
||||
t.Run("flattens_deeply_nested_objects", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
tokens := []database.AIBridgeTokenUsage{
|
||||
{
|
||||
ID: uuid.New(),
|
||||
Metadata: pqtype.NullRawMessage{
|
||||
RawMessage: json.RawMessage(`{
|
||||
"provider": {
|
||||
"anthropic": {"cache_creation_tokens": 100, "cache_read_tokens": 200},
|
||||
"openai": {"reasoning_tokens": 50}
|
||||
},
|
||||
"total": 500
|
||||
}`),
|
||||
Valid: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result := aggregateTokenMetadata(tokens)
|
||||
require.Equal(t, int64(100), result["provider.anthropic.cache_creation_tokens"])
|
||||
require.Equal(t, int64(200), result["provider.anthropic.cache_read_tokens"])
|
||||
require.Equal(t, int64(50), result["provider.openai.reasoning_tokens"])
|
||||
require.Equal(t, int64(500), result["total"])
|
||||
require.Len(t, result, 4)
|
||||
})
|
||||
|
||||
// Real-world provider metadata shapes from
|
||||
// https://github.com/coder/aibridge/issues/150.
|
||||
t.Run("aggregates_real_provider_metadata", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
tokens := []database.AIBridgeTokenUsage{
|
||||
{
|
||||
// Anthropic-style: cache fields are top-level.
|
||||
ID: uuid.New(),
|
||||
Metadata: pqtype.NullRawMessage{
|
||||
RawMessage: json.RawMessage(`{
|
||||
"cache_creation_input_tokens": 0,
|
||||
"cache_read_input_tokens": 23490
|
||||
}`),
|
||||
Valid: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
// OpenAI-style: cache fields are nested inside
|
||||
// input_tokens_details.
|
||||
ID: uuid.New(),
|
||||
Metadata: pqtype.NullRawMessage{
|
||||
RawMessage: json.RawMessage(`{
|
||||
"input_tokens_details": {"cached_tokens": 11904}
|
||||
}`),
|
||||
Valid: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
// Second Anthropic row to verify summing.
|
||||
ID: uuid.New(),
|
||||
Metadata: pqtype.NullRawMessage{
|
||||
RawMessage: json.RawMessage(`{
|
||||
"cache_creation_input_tokens": 500,
|
||||
"cache_read_input_tokens": 10000
|
||||
}`),
|
||||
Valid: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result := aggregateTokenMetadata(tokens)
|
||||
// Anthropic fields are summed across two rows.
|
||||
require.Equal(t, int64(500), result["cache_creation_input_tokens"])
|
||||
require.Equal(t, int64(33490), result["cache_read_input_tokens"])
|
||||
// OpenAI nested field is flattened with dot notation.
|
||||
require.Equal(t, int64(11904), result["input_tokens_details.cached_tokens"])
|
||||
require.Len(t, result, 3)
|
||||
})
|
||||
|
||||
t.Run("skips_string_boolean_null_values", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
tokens := []database.AIBridgeTokenUsage{
|
||||
{
|
||||
ID: uuid.New(),
|
||||
Metadata: pqtype.NullRawMessage{
|
||||
RawMessage: json.RawMessage(`{"tokens":10,"name":"test","enabled":true,"nothing":null}`),
|
||||
Valid: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result := aggregateTokenMetadata(tokens)
|
||||
require.Equal(t, int64(10), result["tokens"])
|
||||
require.Len(t, result, 1)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAggregateTokenUsage(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("empty_input", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
result := aggregateTokenUsage(nil)
|
||||
require.Equal(t, int64(0), result.InputTokens)
|
||||
require.Equal(t, int64(0), result.OutputTokens)
|
||||
require.Empty(t, result.Metadata)
|
||||
})
|
||||
|
||||
t.Run("sums_tokens_and_metadata", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
tokens := []database.AIBridgeTokenUsage{
|
||||
{
|
||||
ID: uuid.New(),
|
||||
InputTokens: 100,
|
||||
OutputTokens: 50,
|
||||
Metadata: pqtype.NullRawMessage{
|
||||
RawMessage: json.RawMessage(`{"reasoning_tokens":20}`),
|
||||
Valid: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: uuid.New(),
|
||||
InputTokens: 200,
|
||||
OutputTokens: 75,
|
||||
Metadata: pqtype.NullRawMessage{
|
||||
RawMessage: json.RawMessage(`{"reasoning_tokens":30}`),
|
||||
Valid: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result := aggregateTokenUsage(tokens)
|
||||
require.Equal(t, int64(300), result.InputTokens)
|
||||
require.Equal(t, int64(125), result.OutputTokens)
|
||||
require.Equal(t, int64(50), result.Metadata["reasoning_tokens"])
|
||||
})
|
||||
|
||||
t.Run("handles_rows_without_metadata", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
tokens := []database.AIBridgeTokenUsage{
|
||||
{
|
||||
ID: uuid.New(),
|
||||
InputTokens: 500,
|
||||
OutputTokens: 200,
|
||||
Metadata: pqtype.NullRawMessage{Valid: false},
|
||||
},
|
||||
}
|
||||
|
||||
result := aggregateTokenUsage(tokens)
|
||||
require.Equal(t, int64(500), result.InputTokens)
|
||||
require.Equal(t, int64(200), result.OutputTokens)
|
||||
require.Empty(t, result.Metadata)
|
||||
})
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -514,69 +513,6 @@ func TestChatQueuedMessage_ParsesUserContentParts(t *testing.T) {
|
||||
require.Equal(t, "queued text", queued.Content[0].Text)
|
||||
}
|
||||
|
||||
func TestChat_AllFieldsPopulated(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Every field of database.Chat is set to a non-zero value so
|
||||
// that the reflection check below catches any field that
|
||||
// db2sdk.Chat forgets to populate. When someone adds a new
|
||||
// field to codersdk.Chat, this test will fail until the
|
||||
// converter is updated.
|
||||
now := dbtime.Now()
|
||||
input := database.Chat{
|
||||
ID: uuid.New(),
|
||||
OwnerID: uuid.New(),
|
||||
WorkspaceID: uuid.NullUUID{UUID: uuid.New(), Valid: true},
|
||||
BuildID: uuid.NullUUID{UUID: uuid.New(), Valid: true},
|
||||
AgentID: uuid.NullUUID{UUID: uuid.New(), Valid: true},
|
||||
ParentChatID: uuid.NullUUID{UUID: uuid.New(), Valid: true},
|
||||
RootChatID: uuid.NullUUID{UUID: uuid.New(), Valid: true},
|
||||
LastModelConfigID: uuid.New(),
|
||||
Title: "all-fields-test",
|
||||
Status: database.ChatStatusRunning,
|
||||
LastError: sql.NullString{String: "boom", Valid: true},
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
Archived: true,
|
||||
PinOrder: 1,
|
||||
MCPServerIDs: []uuid.UUID{uuid.New()},
|
||||
Labels: database.StringMap{"env": "prod"},
|
||||
LastInjectedContext: pqtype.NullRawMessage{
|
||||
// Use a context-file part to verify internal
|
||||
// fields are not present (they are stripped at
|
||||
// write time by chatd, not at read time).
|
||||
RawMessage: json.RawMessage(`[{"type":"context-file","context_file_path":"/AGENTS.md"}]`),
|
||||
Valid: true,
|
||||
},
|
||||
}
|
||||
// Only ChatID is needed here. This test checks that
|
||||
// Chat.DiffStatus is non-nil, not that every DiffStatus
|
||||
// field is populated — that would be a separate test for
|
||||
// the ChatDiffStatus converter.
|
||||
diffStatus := &database.ChatDiffStatus{
|
||||
ChatID: input.ID,
|
||||
}
|
||||
|
||||
got := db2sdk.Chat(input, diffStatus)
|
||||
|
||||
v := reflect.ValueOf(got)
|
||||
typ := v.Type()
|
||||
// HasUnread is populated by ChatRows (which joins the
|
||||
// read-cursor query), not by Chat, so it is expected
|
||||
// to remain zero here.
|
||||
skip := map[string]bool{"HasUnread": true}
|
||||
for i := range typ.NumField() {
|
||||
field := typ.Field(i)
|
||||
if skip[field.Name] {
|
||||
continue
|
||||
}
|
||||
require.False(t, v.Field(i).IsZero(),
|
||||
"codersdk.Chat field %q is zero-valued — db2sdk.Chat may not be populating it",
|
||||
field.Name,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatQueuedMessage_MalformedContent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -1570,13 +1570,13 @@ func (q *querier) AllUserIDs(ctx context.Context, includeSystem bool) ([]uuid.UU
|
||||
return q.db.AllUserIDs(ctx, includeSystem)
|
||||
}
|
||||
|
||||
func (q *querier) ArchiveChatByID(ctx context.Context, id uuid.UUID) ([]database.Chat, error) {
|
||||
func (q *querier) ArchiveChatByID(ctx context.Context, id uuid.UUID) error {
|
||||
chat, err := q.db.GetChatByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, chat); err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
return q.db.ArchiveChatByID(ctx, id)
|
||||
}
|
||||
@@ -1694,13 +1694,6 @@ func (q *querier) CleanTailnetTunnels(ctx context.Context) error {
|
||||
return q.db.CleanTailnetTunnels(ctx)
|
||||
}
|
||||
|
||||
func (q *querier) CleanupDeletedMCPServerIDsFromChatAutomations(ctx context.Context) error {
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceChatAutomation); err != nil {
|
||||
return err
|
||||
}
|
||||
return q.db.CleanupDeletedMCPServerIDsFromChatAutomations(ctx)
|
||||
}
|
||||
|
||||
func (q *querier) CleanupDeletedMCPServerIDsFromChats(ctx context.Context) error {
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceChat); err != nil {
|
||||
return err
|
||||
@@ -1738,28 +1731,6 @@ func (q *querier) CountAuditLogs(ctx context.Context, arg database.CountAuditLog
|
||||
return q.db.CountAuthorizedAuditLogs(ctx, arg, prep)
|
||||
}
|
||||
|
||||
func (q *querier) CountChatAutomationChatCreatesInWindow(ctx context.Context, arg database.CountChatAutomationChatCreatesInWindowParams) (int64, error) {
|
||||
automation, err := q.db.GetChatAutomationByID(ctx, arg.AutomationID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, automation); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return q.db.CountChatAutomationChatCreatesInWindow(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) CountChatAutomationMessagesInWindow(ctx context.Context, arg database.CountChatAutomationMessagesInWindowParams) (int64, error) {
|
||||
automation, err := q.db.GetChatAutomationByID(ctx, arg.AutomationID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, automation); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return q.db.CountChatAutomationMessagesInWindow(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) CountConnectionLogs(ctx context.Context, arg database.CountConnectionLogsParams) (int64, error) {
|
||||
// Just like the actual query, shortcut if the user is an owner.
|
||||
err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceConnectionLog)
|
||||
@@ -1871,28 +1842,6 @@ func (q *querier) DeleteApplicationConnectAPIKeysByUserID(ctx context.Context, u
|
||||
return q.db.DeleteApplicationConnectAPIKeysByUserID(ctx, userID)
|
||||
}
|
||||
|
||||
func (q *querier) DeleteChatAutomationByID(ctx context.Context, id uuid.UUID) error {
|
||||
return deleteQ(q.log, q.auth, q.db.GetChatAutomationByID, q.db.DeleteChatAutomationByID)(ctx, id)
|
||||
}
|
||||
|
||||
// Triggers are sub-resources of an automation. Deleting a trigger
|
||||
// is a configuration change, so we authorize ActionUpdate on the
|
||||
// parent rather than ActionDelete.
|
||||
func (q *querier) DeleteChatAutomationTriggerByID(ctx context.Context, id uuid.UUID) error {
|
||||
trigger, err := q.db.GetChatAutomationTriggerByID(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
automation, err := q.db.GetChatAutomationByID(ctx, trigger.AutomationID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, automation); err != nil {
|
||||
return err
|
||||
}
|
||||
return q.db.DeleteChatAutomationTriggerByID(ctx, id)
|
||||
}
|
||||
|
||||
func (q *querier) DeleteChatModelConfigByID(ctx context.Context, id uuid.UUID) error {
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil {
|
||||
return err
|
||||
@@ -2437,16 +2386,6 @@ func (q *querier) GetActiveAISeatCount(ctx context.Context) (int64, error) {
|
||||
return q.db.GetActiveAISeatCount(ctx)
|
||||
}
|
||||
|
||||
// GetActiveChatAutomationCronTriggers is a system-level query used by
|
||||
// the cron scheduler. It requires read permission on all automations
|
||||
// (admin gate) because it fetches triggers across all orgs and owners.
|
||||
func (q *querier) GetActiveChatAutomationCronTriggers(ctx context.Context) ([]database.GetActiveChatAutomationCronTriggersRow, error) {
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceChatAutomation.All()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return q.db.GetActiveChatAutomationCronTriggers(ctx)
|
||||
}
|
||||
|
||||
func (q *querier) GetActivePresetPrebuildSchedules(ctx context.Context) ([]database.TemplateVersionPresetPrebuildSchedule, error) {
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceTemplate.All()); err != nil {
|
||||
return nil, err
|
||||
@@ -2538,64 +2477,6 @@ func (q *querier) GetAuthorizationUserRoles(ctx context.Context, userID uuid.UUI
|
||||
return q.db.GetAuthorizationUserRoles(ctx, userID)
|
||||
}
|
||||
|
||||
func (q *querier) GetChatAutomationByID(ctx context.Context, id uuid.UUID) (database.ChatAutomation, error) {
|
||||
return fetch(q.log, q.auth, q.db.GetChatAutomationByID)(ctx, id)
|
||||
}
|
||||
|
||||
func (q *querier) GetChatAutomationEventsByAutomationID(ctx context.Context, arg database.GetChatAutomationEventsByAutomationIDParams) ([]database.ChatAutomationEvent, error) {
|
||||
automation, err := q.db.GetChatAutomationByID(ctx, arg.AutomationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, automation); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return q.db.GetChatAutomationEventsByAutomationID(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) GetChatAutomationTriggerByID(ctx context.Context, id uuid.UUID) (database.ChatAutomationTrigger, error) {
|
||||
trigger, err := q.db.GetChatAutomationTriggerByID(ctx, id)
|
||||
if err != nil {
|
||||
return database.ChatAutomationTrigger{}, err
|
||||
}
|
||||
automation, err := q.db.GetChatAutomationByID(ctx, trigger.AutomationID)
|
||||
if err != nil {
|
||||
return database.ChatAutomationTrigger{}, err
|
||||
}
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, automation); err != nil {
|
||||
return database.ChatAutomationTrigger{}, err
|
||||
}
|
||||
return trigger, nil
|
||||
}
|
||||
|
||||
func (q *querier) GetChatAutomationTriggersByAutomationID(ctx context.Context, automationID uuid.UUID) ([]database.ChatAutomationTrigger, error) {
|
||||
automation, err := q.db.GetChatAutomationByID(ctx, automationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, automation); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return q.db.GetChatAutomationTriggersByAutomationID(ctx, automationID)
|
||||
}
|
||||
|
||||
func (q *querier) GetChatAutomations(ctx context.Context, arg database.GetChatAutomationsParams) ([]database.ChatAutomation, error) {
|
||||
// Shortcut if the caller has broad read access (e.g. site admins
|
||||
// / owners). The SQL filter is noticeable, so skip it when we
|
||||
// can.
|
||||
err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceChatAutomation.All())
|
||||
if err == nil {
|
||||
return q.db.GetChatAutomations(ctx, arg)
|
||||
}
|
||||
|
||||
// Fall back to SQL-level row filtering for normal users.
|
||||
prep, err := prepareSQLFilter(ctx, q.auth, policy.ActionRead, rbac.ResourceChatAutomation.Type)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("prepare chat automation SQL filter: %w", err)
|
||||
}
|
||||
return q.db.GetAuthorizedChatAutomations(ctx, arg, prep)
|
||||
}
|
||||
|
||||
func (q *querier) GetChatByID(ctx context.Context, id uuid.UUID) (database.Chat, error) {
|
||||
return fetch(q.log, q.auth, q.db.GetChatByID)(ctx, id)
|
||||
}
|
||||
@@ -2697,18 +2578,6 @@ func (q *querier) GetChatFilesByIDs(ctx context.Context, ids []uuid.UUID) ([]dat
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (q *querier) GetChatIncludeDefaultSystemPrompt(ctx context.Context) (bool, error) {
|
||||
// The include-default-system-prompt flag is a deployment-wide setting read
|
||||
// during chat creation by every authenticated user, so no RBAC policy
|
||||
// check is needed. We still verify that a valid actor exists in the
|
||||
// context to ensure this is never callable by an unauthenticated or
|
||||
// system-internal path without an explicit actor.
|
||||
if _, ok := ActorFromContext(ctx); !ok {
|
||||
return false, ErrNoActor
|
||||
}
|
||||
return q.db.GetChatIncludeDefaultSystemPrompt(ctx)
|
||||
}
|
||||
|
||||
func (q *querier) GetChatMessageByID(ctx context.Context, id int64) (database.ChatMessage, error) {
|
||||
// ChatMessages are authorized through their parent Chat.
|
||||
// We need to fetch the message first to get its chat_id.
|
||||
@@ -2733,14 +2602,6 @@ func (q *querier) GetChatMessagesByChatID(ctx context.Context, arg database.GetC
|
||||
return q.db.GetChatMessagesByChatID(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) GetChatMessagesByChatIDAscPaginated(ctx context.Context, arg database.GetChatMessagesByChatIDAscPaginatedParams) ([]database.ChatMessage, error) {
|
||||
_, err := q.GetChatByID(ctx, arg.ChatID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return q.db.GetChatMessagesByChatIDAscPaginated(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) GetChatMessagesByChatIDDescPaginated(ctx context.Context, arg database.GetChatMessagesByChatIDDescPaginatedParams) ([]database.ChatMessage, error) {
|
||||
_, err := q.GetChatByID(ctx, arg.ChatID)
|
||||
if err != nil {
|
||||
@@ -2813,18 +2674,6 @@ func (q *querier) GetChatSystemPrompt(ctx context.Context) (string, error) {
|
||||
return q.db.GetChatSystemPrompt(ctx)
|
||||
}
|
||||
|
||||
func (q *querier) GetChatSystemPromptConfig(ctx context.Context) (database.GetChatSystemPromptConfigRow, error) {
|
||||
// The system prompt configuration is a deployment-wide setting read during
|
||||
// chat creation by every authenticated user, so no RBAC policy check is
|
||||
// needed. We still verify that a valid actor exists in the context to
|
||||
// ensure this is never callable by an unauthenticated or system-internal
|
||||
// path without an explicit actor.
|
||||
if _, ok := ActorFromContext(ctx); !ok {
|
||||
return database.GetChatSystemPromptConfigRow{}, ErrNoActor
|
||||
}
|
||||
return q.db.GetChatSystemPromptConfig(ctx)
|
||||
}
|
||||
|
||||
// GetChatTemplateAllowlist requires deployment-config read permission,
|
||||
// unlike the peer getters (GetChatDesktopEnabled, etc.) which only
|
||||
// check actor presence. The allowlist is admin-configuration that
|
||||
@@ -2867,7 +2716,7 @@ func (q *querier) GetChatWorkspaceTTL(ctx context.Context) (string, error) {
|
||||
return q.db.GetChatWorkspaceTTL(ctx)
|
||||
}
|
||||
|
||||
func (q *querier) GetChats(ctx context.Context, arg database.GetChatsParams) ([]database.GetChatsRow, error) {
|
||||
func (q *querier) GetChats(ctx context.Context, arg database.GetChatsParams) ([]database.Chat, error) {
|
||||
prep, err := prepareSQLFilter(ctx, q.auth, policy.ActionRead, rbac.ResourceChat.Type)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("(dev error) prepare sql filter: %w", err)
|
||||
@@ -2875,10 +2724,6 @@ func (q *querier) GetChats(ctx context.Context, arg database.GetChatsParams) ([]
|
||||
return q.db.GetAuthorizedChats(ctx, arg, prep)
|
||||
}
|
||||
|
||||
func (q *querier) GetChatsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]database.Chat, error) {
|
||||
return fetchWithPostFilter(q.auth, policy.ActionRead, q.db.GetChatsByWorkspaceIDs)(ctx, ids)
|
||||
}
|
||||
|
||||
func (q *querier) GetConnectionLogsOffset(ctx context.Context, arg database.GetConnectionLogsOffsetParams) ([]database.GetConnectionLogsOffsetRow, error) {
|
||||
// Just like with the audit logs query, shortcut if the user is an owner.
|
||||
err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceConnectionLog)
|
||||
@@ -2930,15 +2775,7 @@ func (q *querier) GetDERPMeshKey(ctx context.Context) (string, error) {
|
||||
}
|
||||
|
||||
func (q *querier) GetDefaultChatModelConfig(ctx context.Context) (database.ChatModelConfig, error) {
|
||||
// Any user who can read chat resources can read the default
|
||||
// model config, since model resolution is required to create
|
||||
// a chat. This avoids gating on ResourceDeploymentConfig
|
||||
// which regular members lack.
|
||||
act, ok := ActorFromContext(ctx)
|
||||
if !ok {
|
||||
return database.ChatModelConfig{}, ErrNoActor
|
||||
}
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceChat.WithOwner(act.ID)); err != nil {
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceDeploymentConfig); err != nil {
|
||||
return database.ChatModelConfig{}, err
|
||||
}
|
||||
return q.db.GetDefaultChatModelConfig(ctx)
|
||||
@@ -4084,13 +3921,6 @@ func (q *querier) GetUnexpiredLicenses(ctx context.Context) ([]database.License,
|
||||
return q.db.GetUnexpiredLicenses(ctx)
|
||||
}
|
||||
|
||||
func (q *querier) GetUserAISeatStates(ctx context.Context, userIDs []uuid.UUID) ([]uuid.UUID, error) {
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceUser); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return q.db.GetUserAISeatStates(ctx, userIDs)
|
||||
}
|
||||
|
||||
func (q *querier) GetUserActivityInsights(ctx context.Context, arg database.GetUserActivityInsightsParams) ([]database.GetUserActivityInsightsRow, error) {
|
||||
// Used by insights endpoints. Need to check both for auditors and for regular users with template acl perms.
|
||||
if err := q.authorizeContext(ctx, policy.ActionViewInsights, rbac.ResourceTemplate); err != nil {
|
||||
@@ -4891,36 +4721,6 @@ func (q *querier) InsertChat(ctx context.Context, arg database.InsertChatParams)
|
||||
return insert(q.log, q.auth, rbac.ResourceChat.WithOwner(arg.OwnerID.String()), q.db.InsertChat)(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) InsertChatAutomation(ctx context.Context, arg database.InsertChatAutomationParams) (database.ChatAutomation, error) {
|
||||
return insert(q.log, q.auth, rbac.ResourceChatAutomation.WithOwner(arg.OwnerID.String()).InOrg(arg.OrganizationID), q.db.InsertChatAutomation)(ctx, arg)
|
||||
}
|
||||
|
||||
// Events are append-only records produced by the system when
|
||||
// triggers fire. We authorize ActionUpdate on the parent
|
||||
// automation because inserting an event is a side-effect of
|
||||
// processing the automation, not an independent create action.
|
||||
func (q *querier) InsertChatAutomationEvent(ctx context.Context, arg database.InsertChatAutomationEventParams) (database.ChatAutomationEvent, error) {
|
||||
automation, err := q.db.GetChatAutomationByID(ctx, arg.AutomationID)
|
||||
if err != nil {
|
||||
return database.ChatAutomationEvent{}, err
|
||||
}
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, automation); err != nil {
|
||||
return database.ChatAutomationEvent{}, err
|
||||
}
|
||||
return q.db.InsertChatAutomationEvent(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) InsertChatAutomationTrigger(ctx context.Context, arg database.InsertChatAutomationTriggerParams) (database.ChatAutomationTrigger, error) {
|
||||
automation, err := q.db.GetChatAutomationByID(ctx, arg.AutomationID)
|
||||
if err != nil {
|
||||
return database.ChatAutomationTrigger{}, err
|
||||
}
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, automation); err != nil {
|
||||
return database.ChatAutomationTrigger{}, err
|
||||
}
|
||||
return q.db.InsertChatAutomationTrigger(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) InsertChatFile(ctx context.Context, arg database.InsertChatFileParams) (database.InsertChatFileRow, error) {
|
||||
// Authorize create on chat resource scoped to the owner and org.
|
||||
return insert(q.log, q.auth, rbac.ResourceChat.WithOwner(arg.OwnerID.String()).InOrg(arg.OrganizationID), q.db.InsertChatFile)(ctx, arg)
|
||||
@@ -5513,14 +5313,6 @@ func (q *querier) InsertWorkspaceResourceMetadata(ctx context.Context, arg datab
|
||||
return q.db.InsertWorkspaceResourceMetadata(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) ListAIBridgeClients(ctx context.Context, arg database.ListAIBridgeClientsParams) ([]string, error) {
|
||||
prep, err := prepareSQLFilter(ctx, q.auth, policy.ActionRead, rbac.ResourceAibridgeInterception.Type)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("(dev error) prepare sql filter: %w", err)
|
||||
}
|
||||
return q.db.ListAuthorizedAIBridgeClients(ctx, arg, prep)
|
||||
}
|
||||
|
||||
func (q *querier) ListAIBridgeInterceptions(ctx context.Context, arg database.ListAIBridgeInterceptionsParams) ([]database.ListAIBridgeInterceptionsRow, error) {
|
||||
prep, err := prepareSQLFilter(ctx, q.auth, policy.ActionRead, rbac.ResourceAibridgeInterception.Type)
|
||||
if err != nil {
|
||||
@@ -5536,13 +5328,6 @@ func (q *querier) ListAIBridgeInterceptionsTelemetrySummaries(ctx context.Contex
|
||||
return q.db.ListAIBridgeInterceptionsTelemetrySummaries(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) ListAIBridgeModelThoughtsByInterceptionIDs(ctx context.Context, interceptionIDs []uuid.UUID) ([]database.AIBridgeModelThought, error) {
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceAibridgeInterception); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return q.db.ListAIBridgeModelThoughtsByInterceptionIDs(ctx, interceptionIDs)
|
||||
}
|
||||
|
||||
func (q *querier) ListAIBridgeModels(ctx context.Context, arg database.ListAIBridgeModelsParams) ([]string, error) {
|
||||
prep, err := prepareSQLFilter(ctx, q.auth, policy.ActionRead, rbac.ResourceAibridgeInterception.Type)
|
||||
if err != nil {
|
||||
@@ -5551,14 +5336,6 @@ func (q *querier) ListAIBridgeModels(ctx context.Context, arg database.ListAIBri
|
||||
return q.db.ListAuthorizedAIBridgeModels(ctx, arg, prep)
|
||||
}
|
||||
|
||||
func (q *querier) ListAIBridgeSessionThreads(ctx context.Context, arg database.ListAIBridgeSessionThreadsParams) ([]database.ListAIBridgeSessionThreadsRow, error) {
|
||||
prep, err := prepareSQLFilter(ctx, q.auth, policy.ActionRead, rbac.ResourceAibridgeInterception.Type)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("(dev error) prepare sql filter: %w", err)
|
||||
}
|
||||
return q.db.ListAuthorizedAIBridgeSessionThreads(ctx, arg, prep)
|
||||
}
|
||||
|
||||
func (q *querier) ListAIBridgeSessions(ctx context.Context, arg database.ListAIBridgeSessionsParams) ([]database.ListAIBridgeSessionsRow, error) {
|
||||
prep, err := prepareSQLFilter(ctx, q.auth, policy.ActionRead, rbac.ResourceAibridgeInterception.Type)
|
||||
if err != nil {
|
||||
@@ -5696,17 +5473,6 @@ func (q *querier) PaginatedOrganizationMembers(ctx context.Context, arg database
|
||||
return q.db.PaginatedOrganizationMembers(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) PinChatByID(ctx context.Context, id uuid.UUID) error {
|
||||
chat, err := q.db.GetChatByID(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, chat); err != nil {
|
||||
return err
|
||||
}
|
||||
return q.db.PinChatByID(ctx, id)
|
||||
}
|
||||
|
||||
func (q *querier) PopNextQueuedMessage(ctx context.Context, chatID uuid.UUID) (database.ChatQueuedMessage, error) {
|
||||
chat, err := q.db.GetChatByID(ctx, chatID)
|
||||
if err != nil {
|
||||
@@ -5718,13 +5484,6 @@ func (q *querier) PopNextQueuedMessage(ctx context.Context, chatID uuid.UUID) (d
|
||||
return q.db.PopNextQueuedMessage(ctx, chatID)
|
||||
}
|
||||
|
||||
func (q *querier) PurgeOldChatAutomationEvents(ctx context.Context, arg database.PurgeOldChatAutomationEventsParams) (int64, error) {
|
||||
if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceChatAutomation.All()); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return q.db.PurgeOldChatAutomationEvents(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate(ctx context.Context, templateID uuid.UUID) error {
|
||||
template, err := q.db.GetTemplateByID(ctx, templateID)
|
||||
if err != nil {
|
||||
@@ -5805,13 +5564,13 @@ func (q *querier) TryAcquireLock(ctx context.Context, id int64) (bool, error) {
|
||||
return q.db.TryAcquireLock(ctx, id)
|
||||
}
|
||||
|
||||
func (q *querier) UnarchiveChatByID(ctx context.Context, id uuid.UUID) ([]database.Chat, error) {
|
||||
func (q *querier) UnarchiveChatByID(ctx context.Context, id uuid.UUID) error {
|
||||
chat, err := q.db.GetChatByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, chat); err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
return q.db.UnarchiveChatByID(ctx, id)
|
||||
}
|
||||
@@ -5839,17 +5598,6 @@ func (q *querier) UnfavoriteWorkspace(ctx context.Context, id uuid.UUID) error {
|
||||
return update(q.log, q.auth, fetch, q.db.UnfavoriteWorkspace)(ctx, id)
|
||||
}
|
||||
|
||||
func (q *querier) UnpinChatByID(ctx context.Context, id uuid.UUID) error {
|
||||
chat, err := q.db.GetChatByID(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, chat); err != nil {
|
||||
return err
|
||||
}
|
||||
return q.db.UnpinChatByID(ctx, id)
|
||||
}
|
||||
|
||||
func (q *querier) UnsetDefaultChatModelConfigs(ctx context.Context) error {
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceSystem); err != nil {
|
||||
return err
|
||||
@@ -5871,70 +5619,6 @@ func (q *querier) UpdateAPIKeyByID(ctx context.Context, arg database.UpdateAPIKe
|
||||
return update(q.log, q.auth, fetch, q.db.UpdateAPIKeyByID)(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateChatAutomation(ctx context.Context, arg database.UpdateChatAutomationParams) (database.ChatAutomation, error) {
|
||||
fetchFunc := func(ctx context.Context, arg database.UpdateChatAutomationParams) (database.ChatAutomation, error) {
|
||||
return q.db.GetChatAutomationByID(ctx, arg.ID)
|
||||
}
|
||||
return updateWithReturn(q.log, q.auth, fetchFunc, q.db.UpdateChatAutomation)(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateChatAutomationTrigger(ctx context.Context, arg database.UpdateChatAutomationTriggerParams) (database.ChatAutomationTrigger, error) {
|
||||
trigger, err := q.db.GetChatAutomationTriggerByID(ctx, arg.ID)
|
||||
if err != nil {
|
||||
return database.ChatAutomationTrigger{}, err
|
||||
}
|
||||
automation, err := q.db.GetChatAutomationByID(ctx, trigger.AutomationID)
|
||||
if err != nil {
|
||||
return database.ChatAutomationTrigger{}, err
|
||||
}
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, automation); err != nil {
|
||||
return database.ChatAutomationTrigger{}, err
|
||||
}
|
||||
return q.db.UpdateChatAutomationTrigger(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateChatAutomationTriggerLastTriggeredAt(ctx context.Context, arg database.UpdateChatAutomationTriggerLastTriggeredAtParams) error {
|
||||
trigger, err := q.db.GetChatAutomationTriggerByID(ctx, arg.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
automation, err := q.db.GetChatAutomationByID(ctx, trigger.AutomationID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, automation); err != nil {
|
||||
return err
|
||||
}
|
||||
return q.db.UpdateChatAutomationTriggerLastTriggeredAt(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateChatAutomationTriggerWebhookSecret(ctx context.Context, arg database.UpdateChatAutomationTriggerWebhookSecretParams) (database.ChatAutomationTrigger, error) {
|
||||
trigger, err := q.db.GetChatAutomationTriggerByID(ctx, arg.ID)
|
||||
if err != nil {
|
||||
return database.ChatAutomationTrigger{}, err
|
||||
}
|
||||
automation, err := q.db.GetChatAutomationByID(ctx, trigger.AutomationID)
|
||||
if err != nil {
|
||||
return database.ChatAutomationTrigger{}, err
|
||||
}
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, automation); err != nil {
|
||||
return database.ChatAutomationTrigger{}, err
|
||||
}
|
||||
return q.db.UpdateChatAutomationTriggerWebhookSecret(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateChatBuildAgentBinding(ctx context.Context, arg database.UpdateChatBuildAgentBindingParams) (database.Chat, error) {
|
||||
chat, err := q.db.GetChatByID(ctx, arg.ID)
|
||||
if err != nil {
|
||||
return database.Chat{}, err
|
||||
}
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, chat); err != nil {
|
||||
return database.Chat{}, err
|
||||
}
|
||||
|
||||
return q.db.UpdateChatBuildAgentBinding(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateChatByID(ctx context.Context, arg database.UpdateChatByIDParams) (database.Chat, error) {
|
||||
chat, err := q.db.GetChatByID(ctx, arg.ID)
|
||||
if err != nil {
|
||||
@@ -5968,39 +5652,6 @@ func (q *querier) UpdateChatLabelsByID(ctx context.Context, arg database.UpdateC
|
||||
return q.db.UpdateChatLabelsByID(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateChatLastInjectedContext(ctx context.Context, arg database.UpdateChatLastInjectedContextParams) (database.Chat, error) {
|
||||
chat, err := q.db.GetChatByID(ctx, arg.ID)
|
||||
if err != nil {
|
||||
return database.Chat{}, err
|
||||
}
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, chat); err != nil {
|
||||
return database.Chat{}, err
|
||||
}
|
||||
return q.db.UpdateChatLastInjectedContext(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateChatLastModelConfigByID(ctx context.Context, arg database.UpdateChatLastModelConfigByIDParams) (database.Chat, error) {
|
||||
chat, err := q.db.GetChatByID(ctx, arg.ID)
|
||||
if err != nil {
|
||||
return database.Chat{}, err
|
||||
}
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, chat); err != nil {
|
||||
return database.Chat{}, err
|
||||
}
|
||||
return q.db.UpdateChatLastModelConfigByID(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateChatLastReadMessageID(ctx context.Context, arg database.UpdateChatLastReadMessageIDParams) error {
|
||||
chat, err := q.db.GetChatByID(ctx, arg.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, chat); err != nil {
|
||||
return err
|
||||
}
|
||||
return q.db.UpdateChatLastReadMessageID(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateChatMCPServerIDs(ctx context.Context, arg database.UpdateChatMCPServerIDsParams) (database.Chat, error) {
|
||||
chat, err := q.db.GetChatByID(ctx, arg.ID)
|
||||
if err != nil {
|
||||
@@ -6035,17 +5686,6 @@ func (q *querier) UpdateChatModelConfig(ctx context.Context, arg database.Update
|
||||
return q.db.UpdateChatModelConfig(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateChatPinOrder(ctx context.Context, arg database.UpdateChatPinOrderParams) error {
|
||||
chat, err := q.db.GetChatByID(ctx, arg.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, chat); err != nil {
|
||||
return err
|
||||
}
|
||||
return q.db.UpdateChatPinOrder(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateChatProvider(ctx context.Context, arg database.UpdateChatProviderParams) (database.ChatProvider, error) {
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil {
|
||||
return database.ChatProvider{}, err
|
||||
@@ -6066,18 +5706,7 @@ func (q *querier) UpdateChatStatus(ctx context.Context, arg database.UpdateChatS
|
||||
return q.db.UpdateChatStatus(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateChatStatusPreserveUpdatedAt(ctx context.Context, arg database.UpdateChatStatusPreserveUpdatedAtParams) (database.Chat, error) {
|
||||
chat, err := q.db.GetChatByID(ctx, arg.ID)
|
||||
if err != nil {
|
||||
return database.Chat{}, err
|
||||
}
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, chat); err != nil {
|
||||
return database.Chat{}, err
|
||||
}
|
||||
return q.db.UpdateChatStatusPreserveUpdatedAt(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateChatWorkspaceBinding(ctx context.Context, arg database.UpdateChatWorkspaceBindingParams) (database.Chat, error) {
|
||||
func (q *querier) UpdateChatWorkspace(ctx context.Context, arg database.UpdateChatWorkspaceParams) (database.Chat, error) {
|
||||
chat, err := q.db.GetChatByID(ctx, arg.ID)
|
||||
if err != nil {
|
||||
return database.Chat{}, err
|
||||
@@ -6086,7 +5715,15 @@ func (q *querier) UpdateChatWorkspaceBinding(ctx context.Context, arg database.U
|
||||
return database.Chat{}, err
|
||||
}
|
||||
|
||||
return q.db.UpdateChatWorkspaceBinding(ctx, arg)
|
||||
// UpdateChatWorkspace is manually implemented for chat tables and may not be
|
||||
// present on every wrapped store interface yet.
|
||||
chatWorkspaceUpdater, ok := q.db.(interface {
|
||||
UpdateChatWorkspace(context.Context, database.UpdateChatWorkspaceParams) (database.Chat, error)
|
||||
})
|
||||
if !ok {
|
||||
return database.Chat{}, xerrors.New("update chat workspace is not implemented by wrapped store")
|
||||
}
|
||||
return chatWorkspaceUpdater.UpdateChatWorkspace(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpdateCryptoKeyDeletesAt(ctx context.Context, arg database.UpdateCryptoKeyDeletesAtParams) (database.CryptoKey, error) {
|
||||
@@ -7190,13 +6827,6 @@ func (q *querier) UpsertChatDiffStatusReference(ctx context.Context, arg databas
|
||||
return q.db.UpsertChatDiffStatusReference(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpsertChatIncludeDefaultSystemPrompt(ctx context.Context, includeDefaultSystemPrompt bool) error {
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
return q.db.UpsertChatIncludeDefaultSystemPrompt(ctx, includeDefaultSystemPrompt)
|
||||
}
|
||||
|
||||
func (q *querier) UpsertChatSystemPrompt(ctx context.Context, value string) error {
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceDeploymentConfig); err != nil {
|
||||
return err
|
||||
@@ -7537,14 +7167,6 @@ func (q *querier) ListAuthorizedAIBridgeModels(ctx context.Context, arg database
|
||||
return q.ListAIBridgeModels(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) ListAuthorizedAIBridgeClients(ctx context.Context, arg database.ListAIBridgeClientsParams, _ rbac.PreparedAuthorized) ([]string, error) {
|
||||
// TODO: Delete this function, all ListAIBridgeClients should be
|
||||
// authorized. For now just call ListAIBridgeClients on the authz
|
||||
// querier. This cannot be deleted for now because it's included in
|
||||
// the database.Store interface, so dbauthz needs to implement it.
|
||||
return q.ListAIBridgeClients(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) ListAuthorizedAIBridgeSessions(ctx context.Context, arg database.ListAIBridgeSessionsParams, prepared rbac.PreparedAuthorized) ([]database.ListAIBridgeSessionsRow, error) {
|
||||
return q.db.ListAuthorizedAIBridgeSessions(ctx, arg, prepared)
|
||||
}
|
||||
@@ -7553,14 +7175,6 @@ func (q *querier) CountAuthorizedAIBridgeSessions(ctx context.Context, arg datab
|
||||
return q.db.CountAuthorizedAIBridgeSessions(ctx, arg, prepared)
|
||||
}
|
||||
|
||||
func (q *querier) ListAuthorizedAIBridgeSessionThreads(ctx context.Context, arg database.ListAIBridgeSessionThreadsParams, prepared rbac.PreparedAuthorized) ([]database.ListAIBridgeSessionThreadsRow, error) {
|
||||
return q.db.ListAuthorizedAIBridgeSessionThreads(ctx, arg, prepared)
|
||||
}
|
||||
|
||||
func (q *querier) GetAuthorizedChats(ctx context.Context, arg database.GetChatsParams, _ rbac.PreparedAuthorized) ([]database.GetChatsRow, error) {
|
||||
func (q *querier) GetAuthorizedChats(ctx context.Context, arg database.GetChatsParams, _ rbac.PreparedAuthorized) ([]database.Chat, error) {
|
||||
return q.GetChats(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) GetAuthorizedChatAutomations(ctx context.Context, arg database.GetChatAutomationsParams, _ rbac.PreparedAuthorized) ([]database.ChatAutomation, error) {
|
||||
return q.GetChatAutomations(ctx, arg)
|
||||
}
|
||||
|
||||
@@ -392,25 +392,13 @@ func (s *MethodTestSuite) TestChats() {
|
||||
s.Run("ArchiveChatByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
chat := testutil.Fake(s.T(), faker, database.Chat{})
|
||||
dbm.EXPECT().GetChatByID(gomock.Any(), chat.ID).Return(chat, nil).AnyTimes()
|
||||
dbm.EXPECT().ArchiveChatByID(gomock.Any(), chat.ID).Return([]database.Chat{chat}, nil).AnyTimes()
|
||||
check.Args(chat.ID).Asserts(chat, policy.ActionUpdate).Returns([]database.Chat{chat})
|
||||
dbm.EXPECT().ArchiveChatByID(gomock.Any(), chat.ID).Return(nil).AnyTimes()
|
||||
check.Args(chat.ID).Asserts(chat, policy.ActionUpdate).Returns()
|
||||
}))
|
||||
s.Run("UnarchiveChatByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
chat := testutil.Fake(s.T(), faker, database.Chat{})
|
||||
dbm.EXPECT().GetChatByID(gomock.Any(), chat.ID).Return(chat, nil).AnyTimes()
|
||||
dbm.EXPECT().UnarchiveChatByID(gomock.Any(), chat.ID).Return([]database.Chat{chat}, nil).AnyTimes()
|
||||
check.Args(chat.ID).Asserts(chat, policy.ActionUpdate).Returns([]database.Chat{chat})
|
||||
}))
|
||||
s.Run("PinChatByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
chat := testutil.Fake(s.T(), faker, database.Chat{})
|
||||
dbm.EXPECT().GetChatByID(gomock.Any(), chat.ID).Return(chat, nil).AnyTimes()
|
||||
dbm.EXPECT().PinChatByID(gomock.Any(), chat.ID).Return(nil).AnyTimes()
|
||||
check.Args(chat.ID).Asserts(chat, policy.ActionUpdate).Returns()
|
||||
}))
|
||||
s.Run("UnpinChatByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
chat := testutil.Fake(s.T(), faker, database.Chat{})
|
||||
dbm.EXPECT().GetChatByID(gomock.Any(), chat.ID).Return(chat, nil).AnyTimes()
|
||||
dbm.EXPECT().UnpinChatByID(gomock.Any(), chat.ID).Return(nil).AnyTimes()
|
||||
dbm.EXPECT().UnarchiveChatByID(gomock.Any(), chat.ID).Return(nil).AnyTimes()
|
||||
check.Args(chat.ID).Asserts(chat, policy.ActionUpdate).Returns()
|
||||
}))
|
||||
s.Run("SoftDeleteChatMessagesAfterID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
@@ -461,13 +449,6 @@ func (s *MethodTestSuite) TestChats() {
|
||||
dbm.EXPECT().GetChatByIDForUpdate(gomock.Any(), chat.ID).Return(chat, nil).AnyTimes()
|
||||
check.Args(chat.ID).Asserts(chat, policy.ActionRead).Returns(chat)
|
||||
}))
|
||||
s.Run("GetChatsByWorkspaceIDs", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
chatA := testutil.Fake(s.T(), faker, database.Chat{})
|
||||
chatB := testutil.Fake(s.T(), faker, database.Chat{})
|
||||
arg := []uuid.UUID{chatA.WorkspaceID.UUID, chatB.WorkspaceID.UUID}
|
||||
dbm.EXPECT().GetChatsByWorkspaceIDs(gomock.Any(), arg).Return([]database.Chat{chatA, chatB}, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(chatA, policy.ActionRead, chatB, policy.ActionRead).Returns([]database.Chat{chatA, chatB})
|
||||
}))
|
||||
s.Run("GetChatCostPerChat", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
|
||||
arg := database.GetChatCostPerChatParams{
|
||||
OwnerID: uuid.New(),
|
||||
@@ -592,14 +573,6 @@ func (s *MethodTestSuite) TestChats() {
|
||||
dbm.EXPECT().GetChatMessagesByChatID(gomock.Any(), arg).Return(msgs, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(chat, policy.ActionRead).Returns(msgs)
|
||||
}))
|
||||
s.Run("GetChatMessagesByChatIDAscPaginated", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
chat := testutil.Fake(s.T(), faker, database.Chat{})
|
||||
msgs := []database.ChatMessage{testutil.Fake(s.T(), faker, database.ChatMessage{ChatID: chat.ID})}
|
||||
arg := database.GetChatMessagesByChatIDAscPaginatedParams{ChatID: chat.ID, AfterID: 0, LimitVal: 50}
|
||||
dbm.EXPECT().GetChatByID(gomock.Any(), chat.ID).Return(chat, nil).AnyTimes()
|
||||
dbm.EXPECT().GetChatMessagesByChatIDAscPaginated(gomock.Any(), arg).Return(msgs, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(chat, policy.ActionRead).Returns(msgs)
|
||||
}))
|
||||
s.Run("GetChatMessagesByChatIDDescPaginated", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
chat := testutil.Fake(s.T(), faker, database.Chat{})
|
||||
msgs := []database.ChatMessage{testutil.Fake(s.T(), faker, database.ChatMessage{ChatID: chat.ID})}
|
||||
@@ -631,7 +604,7 @@ func (s *MethodTestSuite) TestChats() {
|
||||
s.Run("GetDefaultChatModelConfig", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
config := testutil.Fake(s.T(), faker, database.ChatModelConfig{})
|
||||
dbm.EXPECT().GetDefaultChatModelConfig(gomock.Any()).Return(config, nil).AnyTimes()
|
||||
check.Asserts(rbac.ResourceChat.WithOwner(testActorID.String()), policy.ActionRead).Returns(config)
|
||||
check.Asserts(rbac.ResourceDeploymentConfig, policy.ActionRead).Returns(config)
|
||||
}))
|
||||
s.Run("GetChatModelConfigs", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
configA := testutil.Fake(s.T(), faker, database.ChatModelConfig{})
|
||||
@@ -658,13 +631,13 @@ func (s *MethodTestSuite) TestChats() {
|
||||
}))
|
||||
s.Run("GetChats", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
|
||||
params := database.GetChatsParams{}
|
||||
dbm.EXPECT().GetAuthorizedChats(gomock.Any(), params, gomock.Any()).Return([]database.GetChatsRow{}, nil).AnyTimes()
|
||||
dbm.EXPECT().GetAuthorizedChats(gomock.Any(), params, gomock.Any()).Return([]database.Chat{}, nil).AnyTimes()
|
||||
// No asserts here because SQLFilter.
|
||||
check.Args(params).Asserts()
|
||||
}))
|
||||
s.Run("GetAuthorizedChats", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
|
||||
params := database.GetChatsParams{}
|
||||
dbm.EXPECT().GetAuthorizedChats(gomock.Any(), params, gomock.Any()).Return([]database.GetChatsRow{}, nil).AnyTimes()
|
||||
dbm.EXPECT().GetAuthorizedChats(gomock.Any(), params, gomock.Any()).Return([]database.Chat{}, nil).AnyTimes()
|
||||
// No asserts here because it re-routes through GetChats which uses SQLFilter.
|
||||
check.Args(params, emptyPreparedAuthorized{}).Asserts()
|
||||
}))
|
||||
@@ -675,17 +648,6 @@ func (s *MethodTestSuite) TestChats() {
|
||||
dbm.EXPECT().GetChatQueuedMessages(gomock.Any(), chat.ID).Return(qms, nil).AnyTimes()
|
||||
check.Args(chat.ID).Asserts(chat, policy.ActionRead).Returns(qms)
|
||||
}))
|
||||
s.Run("GetChatIncludeDefaultSystemPrompt", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
|
||||
dbm.EXPECT().GetChatIncludeDefaultSystemPrompt(gomock.Any()).Return(true, nil).AnyTimes()
|
||||
check.Args().Asserts()
|
||||
}))
|
||||
s.Run("GetChatSystemPromptConfig", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
|
||||
dbm.EXPECT().GetChatSystemPromptConfig(gomock.Any()).Return(database.GetChatSystemPromptConfigRow{
|
||||
ChatSystemPrompt: "prompt",
|
||||
IncludeDefaultSystemPrompt: true,
|
||||
}, nil).AnyTimes()
|
||||
check.Args().Asserts()
|
||||
}))
|
||||
s.Run("GetChatSystemPrompt", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
|
||||
dbm.EXPECT().GetChatSystemPrompt(gomock.Any()).Return("prompt", nil).AnyTimes()
|
||||
check.Args().Asserts()
|
||||
@@ -797,26 +759,6 @@ func (s *MethodTestSuite) TestChats() {
|
||||
dbm.EXPECT().UpdateChatLabelsByID(gomock.Any(), arg).Return(chat, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(chat, policy.ActionUpdate).Returns(chat)
|
||||
}))
|
||||
s.Run("UpdateChatLastModelConfigByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
chat := testutil.Fake(s.T(), faker, database.Chat{})
|
||||
arg := database.UpdateChatLastModelConfigByIDParams{
|
||||
ID: chat.ID,
|
||||
LastModelConfigID: uuid.New(),
|
||||
}
|
||||
dbm.EXPECT().GetChatByID(gomock.Any(), chat.ID).Return(chat, nil).AnyTimes()
|
||||
dbm.EXPECT().UpdateChatLastModelConfigByID(gomock.Any(), arg).Return(chat, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(chat, policy.ActionUpdate).Returns(chat)
|
||||
}))
|
||||
s.Run("UpdateChatStatusPreserveUpdatedAt", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
chat := testutil.Fake(s.T(), faker, database.Chat{})
|
||||
arg := database.UpdateChatStatusPreserveUpdatedAtParams{
|
||||
ID: chat.ID,
|
||||
Status: database.ChatStatusRunning,
|
||||
}
|
||||
dbm.EXPECT().GetChatByID(gomock.Any(), chat.ID).Return(chat, nil).AnyTimes()
|
||||
dbm.EXPECT().UpdateChatStatusPreserveUpdatedAt(gomock.Any(), arg).Return(chat, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(chat, policy.ActionUpdate).Returns(chat)
|
||||
}))
|
||||
s.Run("UpdateChatHeartbeat", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
chat := testutil.Fake(s.T(), faker, database.Chat{})
|
||||
arg := database.UpdateChatHeartbeatParams{
|
||||
@@ -867,16 +809,6 @@ func (s *MethodTestSuite) TestChats() {
|
||||
dbm.EXPECT().UpdateChatProvider(gomock.Any(), arg).Return(provider, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(rbac.ResourceDeploymentConfig, policy.ActionUpdate).Returns(provider)
|
||||
}))
|
||||
s.Run("UpdateChatPinOrder", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
chat := testutil.Fake(s.T(), faker, database.Chat{})
|
||||
arg := database.UpdateChatPinOrderParams{
|
||||
ID: chat.ID,
|
||||
PinOrder: 2,
|
||||
}
|
||||
dbm.EXPECT().GetChatByID(gomock.Any(), chat.ID).Return(chat, nil).AnyTimes()
|
||||
dbm.EXPECT().UpdateChatPinOrder(gomock.Any(), arg).Return(nil).AnyTimes()
|
||||
check.Args(arg).Asserts(chat, policy.ActionUpdate).Returns()
|
||||
}))
|
||||
s.Run("UpdateChatStatus", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
chat := testutil.Fake(s.T(), faker, database.Chat{})
|
||||
arg := database.UpdateChatStatusParams{
|
||||
@@ -887,29 +819,15 @@ func (s *MethodTestSuite) TestChats() {
|
||||
dbm.EXPECT().UpdateChatStatus(gomock.Any(), arg).Return(chat, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(chat, policy.ActionUpdate).Returns(chat)
|
||||
}))
|
||||
s.Run("UpdateChatBuildAgentBinding", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
s.Run("UpdateChatWorkspace", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
chat := testutil.Fake(s.T(), faker, database.Chat{})
|
||||
arg := database.UpdateChatBuildAgentBindingParams{
|
||||
ID: chat.ID,
|
||||
BuildID: uuid.NullUUID{UUID: uuid.New(), Valid: true},
|
||||
AgentID: uuid.NullUUID{UUID: uuid.New(), Valid: true},
|
||||
}
|
||||
updatedChat := testutil.Fake(s.T(), faker, database.Chat{ID: chat.ID})
|
||||
dbm.EXPECT().GetChatByID(gomock.Any(), chat.ID).Return(chat, nil).AnyTimes()
|
||||
dbm.EXPECT().UpdateChatBuildAgentBinding(gomock.Any(), arg).Return(updatedChat, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(chat, policy.ActionUpdate).Returns(updatedChat)
|
||||
}))
|
||||
s.Run("UpdateChatWorkspaceBinding", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
chat := testutil.Fake(s.T(), faker, database.Chat{})
|
||||
arg := database.UpdateChatWorkspaceBindingParams{
|
||||
arg := database.UpdateChatWorkspaceParams{
|
||||
ID: chat.ID,
|
||||
WorkspaceID: uuid.NullUUID{UUID: uuid.New(), Valid: true},
|
||||
BuildID: uuid.NullUUID{UUID: uuid.New(), Valid: true},
|
||||
AgentID: uuid.NullUUID{UUID: uuid.New(), Valid: true},
|
||||
}
|
||||
updatedChat := testutil.Fake(s.T(), faker, database.Chat{ID: chat.ID})
|
||||
dbm.EXPECT().GetChatByID(gomock.Any(), chat.ID).Return(chat, nil).AnyTimes()
|
||||
dbm.EXPECT().UpdateChatWorkspaceBinding(gomock.Any(), arg).Return(updatedChat, nil).AnyTimes()
|
||||
dbm.EXPECT().UpdateChatWorkspace(gomock.Any(), arg).Return(updatedChat, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(chat, policy.ActionUpdate).Returns(updatedChat)
|
||||
}))
|
||||
s.Run("UnsetDefaultChatModelConfigs", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
|
||||
@@ -961,10 +879,6 @@ func (s *MethodTestSuite) TestChats() {
|
||||
dbm.EXPECT().BackoffChatDiffStatus(gomock.Any(), arg).Return(nil).AnyTimes()
|
||||
check.Args(arg).Asserts(rbac.ResourceChat, policy.ActionUpdate).Returns()
|
||||
}))
|
||||
s.Run("UpsertChatIncludeDefaultSystemPrompt", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
|
||||
dbm.EXPECT().UpsertChatIncludeDefaultSystemPrompt(gomock.Any(), false).Return(nil).AnyTimes()
|
||||
check.Args(false).Asserts(rbac.ResourceDeploymentConfig, policy.ActionUpdate)
|
||||
}))
|
||||
s.Run("UpsertChatSystemPrompt", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
|
||||
dbm.EXPECT().UpsertChatSystemPrompt(gomock.Any(), "").Return(nil).AnyTimes()
|
||||
check.Args("").Asserts(rbac.ResourceDeploymentConfig, policy.ActionUpdate)
|
||||
@@ -1121,10 +1035,6 @@ func (s *MethodTestSuite) TestChats() {
|
||||
dbm.EXPECT().CleanupDeletedMCPServerIDsFromChats(gomock.Any()).Return(nil).AnyTimes()
|
||||
check.Args().Asserts(rbac.ResourceChat, policy.ActionUpdate)
|
||||
}))
|
||||
s.Run("CleanupDeletedMCPServerIDsFromChatAutomations", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
|
||||
dbm.EXPECT().CleanupDeletedMCPServerIDsFromChatAutomations(gomock.Any()).Return(nil).AnyTimes()
|
||||
check.Args().Asserts(rbac.ResourceChatAutomation, policy.ActionUpdate)
|
||||
}))
|
||||
s.Run("DeleteMCPServerConfigByID", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
|
||||
id := uuid.New()
|
||||
dbm.EXPECT().DeleteMCPServerConfigByID(gomock.Any(), id).Return(nil).AnyTimes()
|
||||
@@ -1208,29 +1118,6 @@ func (s *MethodTestSuite) TestChats() {
|
||||
dbm.EXPECT().UpdateChatMCPServerIDs(gomock.Any(), arg).Return(chat, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(chat, policy.ActionUpdate).Returns(chat)
|
||||
}))
|
||||
s.Run("UpdateChatLastInjectedContext", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
chat := testutil.Fake(s.T(), faker, database.Chat{})
|
||||
arg := database.UpdateChatLastInjectedContextParams{
|
||||
ID: chat.ID,
|
||||
LastInjectedContext: pqtype.NullRawMessage{
|
||||
RawMessage: json.RawMessage(`[{"type":"text","text":"test"}]`),
|
||||
Valid: true,
|
||||
},
|
||||
}
|
||||
dbm.EXPECT().GetChatByID(gomock.Any(), chat.ID).Return(chat, nil).AnyTimes()
|
||||
dbm.EXPECT().UpdateChatLastInjectedContext(gomock.Any(), arg).Return(chat, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(chat, policy.ActionUpdate).Returns(chat)
|
||||
}))
|
||||
s.Run("UpdateChatLastReadMessageID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
chat := testutil.Fake(s.T(), faker, database.Chat{})
|
||||
arg := database.UpdateChatLastReadMessageIDParams{
|
||||
ID: chat.ID,
|
||||
LastReadMessageID: 42,
|
||||
}
|
||||
dbm.EXPECT().GetChatByID(gomock.Any(), chat.ID).Return(chat, nil).AnyTimes()
|
||||
dbm.EXPECT().UpdateChatLastReadMessageID(gomock.Any(), arg).Return(nil).AnyTimes()
|
||||
check.Args(arg).Asserts(chat, policy.ActionUpdate).Returns()
|
||||
}))
|
||||
s.Run("UpdateMCPServerConfig", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
config := testutil.Fake(s.T(), faker, database.MCPServerConfig{})
|
||||
arg := database.UpdateMCPServerConfigParams{
|
||||
@@ -1254,226 +1141,6 @@ func (s *MethodTestSuite) TestChats() {
|
||||
}))
|
||||
}
|
||||
|
||||
func (s *MethodTestSuite) TestChatAutomations() {
|
||||
s.Run("CountChatAutomationChatCreatesInWindow", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
automation := testutil.Fake(s.T(), faker, database.ChatAutomation{Status: database.ChatAutomationStatusActive})
|
||||
arg := database.CountChatAutomationChatCreatesInWindowParams{
|
||||
AutomationID: automation.ID,
|
||||
WindowStart: dbtime.Now().Add(-time.Hour),
|
||||
}
|
||||
dbm.EXPECT().GetChatAutomationByID(gomock.Any(), automation.ID).Return(automation, nil).AnyTimes()
|
||||
dbm.EXPECT().CountChatAutomationChatCreatesInWindow(gomock.Any(), arg).Return(int64(3), nil).AnyTimes()
|
||||
check.Args(arg).Asserts(automation, policy.ActionRead).Returns(int64(3))
|
||||
}))
|
||||
s.Run("CountChatAutomationMessagesInWindow", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
automation := testutil.Fake(s.T(), faker, database.ChatAutomation{Status: database.ChatAutomationStatusActive})
|
||||
arg := database.CountChatAutomationMessagesInWindowParams{
|
||||
AutomationID: automation.ID,
|
||||
WindowStart: dbtime.Now().Add(-time.Hour),
|
||||
}
|
||||
dbm.EXPECT().GetChatAutomationByID(gomock.Any(), automation.ID).Return(automation, nil).AnyTimes()
|
||||
dbm.EXPECT().CountChatAutomationMessagesInWindow(gomock.Any(), arg).Return(int64(5), nil).AnyTimes()
|
||||
check.Args(arg).Asserts(automation, policy.ActionRead).Returns(int64(5))
|
||||
}))
|
||||
s.Run("DeleteChatAutomationByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
automation := testutil.Fake(s.T(), faker, database.ChatAutomation{Status: database.ChatAutomationStatusActive})
|
||||
dbm.EXPECT().GetChatAutomationByID(gomock.Any(), automation.ID).Return(automation, nil).AnyTimes()
|
||||
dbm.EXPECT().DeleteChatAutomationByID(gomock.Any(), automation.ID).Return(nil).AnyTimes()
|
||||
check.Args(automation.ID).Asserts(automation, policy.ActionDelete).Returns()
|
||||
}))
|
||||
s.Run("DeleteChatAutomationTriggerByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
automation := testutil.Fake(s.T(), faker, database.ChatAutomation{Status: database.ChatAutomationStatusActive})
|
||||
trigger := testutil.Fake(s.T(), faker, database.ChatAutomationTrigger{
|
||||
AutomationID: automation.ID,
|
||||
Type: database.ChatAutomationTriggerTypeWebhook,
|
||||
})
|
||||
dbm.EXPECT().GetChatAutomationTriggerByID(gomock.Any(), trigger.ID).Return(trigger, nil).AnyTimes()
|
||||
dbm.EXPECT().GetChatAutomationByID(gomock.Any(), automation.ID).Return(automation, nil).AnyTimes()
|
||||
dbm.EXPECT().DeleteChatAutomationTriggerByID(gomock.Any(), trigger.ID).Return(nil).AnyTimes()
|
||||
check.Args(trigger.ID).Asserts(automation, policy.ActionUpdate).Returns()
|
||||
}))
|
||||
s.Run("GetActiveChatAutomationCronTriggers", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
|
||||
rows := []database.GetActiveChatAutomationCronTriggersRow{}
|
||||
dbm.EXPECT().GetActiveChatAutomationCronTriggers(gomock.Any()).Return(rows, nil).AnyTimes()
|
||||
check.Args().Asserts(rbac.ResourceChatAutomation.All(), policy.ActionRead).Returns(rows)
|
||||
}))
|
||||
s.Run("GetChatAutomationByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
automation := testutil.Fake(s.T(), faker, database.ChatAutomation{Status: database.ChatAutomationStatusActive})
|
||||
dbm.EXPECT().GetChatAutomationByID(gomock.Any(), automation.ID).Return(automation, nil).AnyTimes()
|
||||
check.Args(automation.ID).Asserts(automation, policy.ActionRead).Returns(automation)
|
||||
}))
|
||||
s.Run("GetChatAutomationEventsByAutomationID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
automation := testutil.Fake(s.T(), faker, database.ChatAutomation{Status: database.ChatAutomationStatusActive})
|
||||
arg := database.GetChatAutomationEventsByAutomationIDParams{
|
||||
AutomationID: automation.ID,
|
||||
}
|
||||
events := []database.ChatAutomationEvent{}
|
||||
dbm.EXPECT().GetChatAutomationByID(gomock.Any(), automation.ID).Return(automation, nil).AnyTimes()
|
||||
dbm.EXPECT().GetChatAutomationEventsByAutomationID(gomock.Any(), arg).Return(events, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(automation, policy.ActionRead).Returns(events)
|
||||
}))
|
||||
s.Run("GetChatAutomationTriggerByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
automation := testutil.Fake(s.T(), faker, database.ChatAutomation{Status: database.ChatAutomationStatusActive})
|
||||
trigger := testutil.Fake(s.T(), faker, database.ChatAutomationTrigger{
|
||||
AutomationID: automation.ID,
|
||||
Type: database.ChatAutomationTriggerTypeWebhook,
|
||||
})
|
||||
dbm.EXPECT().GetChatAutomationTriggerByID(gomock.Any(), trigger.ID).Return(trigger, nil).AnyTimes()
|
||||
dbm.EXPECT().GetChatAutomationByID(gomock.Any(), automation.ID).Return(automation, nil).AnyTimes()
|
||||
check.Args(trigger.ID).Asserts(automation, policy.ActionRead).Returns(trigger)
|
||||
}))
|
||||
s.Run("GetChatAutomationTriggersByAutomationID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
automation := testutil.Fake(s.T(), faker, database.ChatAutomation{Status: database.ChatAutomationStatusActive})
|
||||
triggers := []database.ChatAutomationTrigger{}
|
||||
dbm.EXPECT().GetChatAutomationByID(gomock.Any(), automation.ID).Return(automation, nil).AnyTimes()
|
||||
dbm.EXPECT().GetChatAutomationTriggersByAutomationID(gomock.Any(), automation.ID).Return(triggers, nil).AnyTimes()
|
||||
check.Args(automation.ID).Asserts(automation, policy.ActionRead).Returns(triggers)
|
||||
}))
|
||||
s.Run("GetChatAutomations", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
|
||||
params := database.GetChatAutomationsParams{}
|
||||
dbm.EXPECT().GetChatAutomations(gomock.Any(), params).Return([]database.ChatAutomation{}, nil).AnyTimes()
|
||||
dbm.EXPECT().GetAuthorizedChatAutomations(gomock.Any(), params, gomock.Any()).Return([]database.ChatAutomation{}, nil).AnyTimes()
|
||||
check.Args(params).Asserts(rbac.ResourceChatAutomation.All(), policy.ActionRead).WithNotAuthorized("nil")
|
||||
}))
|
||||
s.Run("GetAuthorizedChatAutomations", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
|
||||
params := database.GetChatAutomationsParams{}
|
||||
dbm.EXPECT().GetAuthorizedChatAutomations(gomock.Any(), params, gomock.Any()).Return([]database.ChatAutomation{}, nil).AnyTimes()
|
||||
dbm.EXPECT().GetChatAutomations(gomock.Any(), params).Return([]database.ChatAutomation{}, nil).AnyTimes()
|
||||
check.Args(params, emptyPreparedAuthorized{}).Asserts(rbac.ResourceChatAutomation.All(), policy.ActionRead)
|
||||
}))
|
||||
s.Run("InsertChatAutomation", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
arg := database.InsertChatAutomationParams{
|
||||
ID: uuid.New(),
|
||||
OwnerID: uuid.New(),
|
||||
OrganizationID: uuid.New(),
|
||||
Name: "test-automation",
|
||||
Description: "test description",
|
||||
Instructions: "test instructions",
|
||||
Status: database.ChatAutomationStatusActive,
|
||||
CreatedAt: dbtime.Now(),
|
||||
UpdatedAt: dbtime.Now(),
|
||||
}
|
||||
automation := testutil.Fake(s.T(), faker, database.ChatAutomation{
|
||||
ID: arg.ID,
|
||||
OwnerID: arg.OwnerID,
|
||||
OrganizationID: arg.OrganizationID,
|
||||
Status: arg.Status,
|
||||
})
|
||||
dbm.EXPECT().InsertChatAutomation(gomock.Any(), arg).Return(automation, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(rbac.ResourceChatAutomation.WithOwner(arg.OwnerID.String()).InOrg(arg.OrganizationID), policy.ActionCreate).Returns(automation)
|
||||
}))
|
||||
s.Run("InsertChatAutomationEvent", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
automation := testutil.Fake(s.T(), faker, database.ChatAutomation{Status: database.ChatAutomationStatusActive})
|
||||
arg := database.InsertChatAutomationEventParams{
|
||||
ID: uuid.New(),
|
||||
AutomationID: automation.ID,
|
||||
ReceivedAt: dbtime.Now(),
|
||||
Payload: json.RawMessage(`{}`),
|
||||
Status: database.ChatAutomationEventStatusFiltered,
|
||||
}
|
||||
event := testutil.Fake(s.T(), faker, database.ChatAutomationEvent{
|
||||
ID: arg.ID,
|
||||
AutomationID: automation.ID,
|
||||
Status: arg.Status,
|
||||
})
|
||||
dbm.EXPECT().GetChatAutomationByID(gomock.Any(), automation.ID).Return(automation, nil).AnyTimes()
|
||||
dbm.EXPECT().InsertChatAutomationEvent(gomock.Any(), arg).Return(event, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(automation, policy.ActionUpdate).Returns(event)
|
||||
}))
|
||||
s.Run("InsertChatAutomationTrigger", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
automation := testutil.Fake(s.T(), faker, database.ChatAutomation{Status: database.ChatAutomationStatusActive})
|
||||
arg := database.InsertChatAutomationTriggerParams{
|
||||
ID: uuid.New(),
|
||||
AutomationID: automation.ID,
|
||||
Type: database.ChatAutomationTriggerTypeWebhook,
|
||||
CreatedAt: dbtime.Now(),
|
||||
UpdatedAt: dbtime.Now(),
|
||||
}
|
||||
trigger := testutil.Fake(s.T(), faker, database.ChatAutomationTrigger{
|
||||
ID: arg.ID,
|
||||
AutomationID: automation.ID,
|
||||
Type: arg.Type,
|
||||
})
|
||||
dbm.EXPECT().GetChatAutomationByID(gomock.Any(), automation.ID).Return(automation, nil).AnyTimes()
|
||||
dbm.EXPECT().InsertChatAutomationTrigger(gomock.Any(), arg).Return(trigger, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(automation, policy.ActionUpdate).Returns(trigger)
|
||||
}))
|
||||
s.Run("PurgeOldChatAutomationEvents", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
|
||||
arg := database.PurgeOldChatAutomationEventsParams{
|
||||
Before: dbtime.Now().Add(-7 * 24 * time.Hour),
|
||||
LimitCount: 1000,
|
||||
}
|
||||
dbm.EXPECT().PurgeOldChatAutomationEvents(gomock.Any(), arg).Return(int64(5), nil).AnyTimes()
|
||||
check.Args(arg).Asserts(rbac.ResourceChatAutomation.All(), policy.ActionDelete).Returns(int64(5))
|
||||
}))
|
||||
s.Run("UpdateChatAutomation", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
automation := testutil.Fake(s.T(), faker, database.ChatAutomation{Status: database.ChatAutomationStatusActive})
|
||||
arg := database.UpdateChatAutomationParams{
|
||||
ID: automation.ID,
|
||||
Name: "updated-name",
|
||||
Description: "updated description",
|
||||
Status: database.ChatAutomationStatusActive,
|
||||
UpdatedAt: dbtime.Now(),
|
||||
}
|
||||
updated := automation
|
||||
updated.Name = arg.Name
|
||||
dbm.EXPECT().GetChatAutomationByID(gomock.Any(), automation.ID).Return(automation, nil).AnyTimes()
|
||||
dbm.EXPECT().UpdateChatAutomation(gomock.Any(), arg).Return(updated, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(automation, policy.ActionUpdate).Returns(updated)
|
||||
}))
|
||||
s.Run("UpdateChatAutomationTrigger", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
automation := testutil.Fake(s.T(), faker, database.ChatAutomation{Status: database.ChatAutomationStatusActive})
|
||||
trigger := testutil.Fake(s.T(), faker, database.ChatAutomationTrigger{
|
||||
AutomationID: automation.ID,
|
||||
Type: database.ChatAutomationTriggerTypeCron,
|
||||
})
|
||||
arg := database.UpdateChatAutomationTriggerParams{
|
||||
ID: trigger.ID,
|
||||
UpdatedAt: dbtime.Now(),
|
||||
}
|
||||
updated := trigger
|
||||
dbm.EXPECT().GetChatAutomationTriggerByID(gomock.Any(), trigger.ID).Return(trigger, nil).AnyTimes()
|
||||
dbm.EXPECT().GetChatAutomationByID(gomock.Any(), automation.ID).Return(automation, nil).AnyTimes()
|
||||
dbm.EXPECT().UpdateChatAutomationTrigger(gomock.Any(), arg).Return(updated, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(automation, policy.ActionUpdate).Returns(updated)
|
||||
}))
|
||||
s.Run("UpdateChatAutomationTriggerLastTriggeredAt", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
automation := testutil.Fake(s.T(), faker, database.ChatAutomation{Status: database.ChatAutomationStatusActive})
|
||||
trigger := testutil.Fake(s.T(), faker, database.ChatAutomationTrigger{
|
||||
AutomationID: automation.ID,
|
||||
Type: database.ChatAutomationTriggerTypeCron,
|
||||
})
|
||||
arg := database.UpdateChatAutomationTriggerLastTriggeredAtParams{
|
||||
ID: trigger.ID,
|
||||
LastTriggeredAt: dbtime.Now(),
|
||||
}
|
||||
dbm.EXPECT().GetChatAutomationTriggerByID(gomock.Any(), trigger.ID).Return(trigger, nil).AnyTimes()
|
||||
dbm.EXPECT().GetChatAutomationByID(gomock.Any(), automation.ID).Return(automation, nil).AnyTimes()
|
||||
dbm.EXPECT().UpdateChatAutomationTriggerLastTriggeredAt(gomock.Any(), arg).Return(nil).AnyTimes()
|
||||
check.Args(arg).Asserts(automation, policy.ActionUpdate).Returns()
|
||||
}))
|
||||
s.Run("UpdateChatAutomationTriggerWebhookSecret", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
automation := testutil.Fake(s.T(), faker, database.ChatAutomation{Status: database.ChatAutomationStatusActive})
|
||||
trigger := testutil.Fake(s.T(), faker, database.ChatAutomationTrigger{
|
||||
AutomationID: automation.ID,
|
||||
Type: database.ChatAutomationTriggerTypeWebhook,
|
||||
})
|
||||
arg := database.UpdateChatAutomationTriggerWebhookSecretParams{
|
||||
ID: trigger.ID,
|
||||
UpdatedAt: dbtime.Now(),
|
||||
WebhookSecret: sql.NullString{
|
||||
String: "new-secret",
|
||||
Valid: true,
|
||||
},
|
||||
}
|
||||
updated := trigger
|
||||
dbm.EXPECT().GetChatAutomationTriggerByID(gomock.Any(), trigger.ID).Return(trigger, nil).AnyTimes()
|
||||
dbm.EXPECT().GetChatAutomationByID(gomock.Any(), automation.ID).Return(automation, nil).AnyTimes()
|
||||
dbm.EXPECT().UpdateChatAutomationTriggerWebhookSecret(gomock.Any(), arg).Return(updated, nil).AnyTimes()
|
||||
check.Args(arg).Asserts(automation, policy.ActionUpdate).Returns(updated)
|
||||
}))
|
||||
}
|
||||
|
||||
func (s *MethodTestSuite) TestFile() {
|
||||
s.Run("GetFileByHashAndCreator", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
f := testutil.Fake(s.T(), faker, database.File{})
|
||||
@@ -2492,14 +2159,6 @@ func (s *MethodTestSuite) TestUser() {
|
||||
dbm.EXPECT().GetQuotaConsumedForUser(gomock.Any(), arg).Return(int64(0), nil).AnyTimes()
|
||||
check.Args(arg).Asserts(u, policy.ActionRead).Returns(int64(0))
|
||||
}))
|
||||
s.Run("GetUserAISeatStates", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
a := testutil.Fake(s.T(), faker, database.User{})
|
||||
b := testutil.Fake(s.T(), faker, database.User{})
|
||||
ids := []uuid.UUID{a.ID, b.ID}
|
||||
seatStates := []uuid.UUID{a.ID}
|
||||
dbm.EXPECT().GetUserAISeatStates(gomock.Any(), ids).Return(seatStates, nil).AnyTimes()
|
||||
check.Args(ids).Asserts(rbac.ResourceUser, policy.ActionRead).Returns(seatStates)
|
||||
}))
|
||||
s.Run("GetUserByEmailOrUsername", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
u := testutil.Fake(s.T(), faker, database.User{})
|
||||
arg := database.GetUserByEmailOrUsernameParams{Email: u.Email}
|
||||
@@ -5779,20 +5438,6 @@ func (s *MethodTestSuite) TestAIBridge() {
|
||||
check.Args(params, emptyPreparedAuthorized{}).Asserts()
|
||||
}))
|
||||
|
||||
s.Run("ListAIBridgeClients", s.Mocked(func(db *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
params := database.ListAIBridgeClientsParams{}
|
||||
db.EXPECT().ListAuthorizedAIBridgeClients(gomock.Any(), params, gomock.Any()).Return([]string{}, nil).AnyTimes()
|
||||
// No asserts here because SQLFilter.
|
||||
check.Args(params).Asserts()
|
||||
}))
|
||||
|
||||
s.Run("ListAuthorizedAIBridgeClients", s.Mocked(func(db *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
params := database.ListAIBridgeClientsParams{}
|
||||
db.EXPECT().ListAuthorizedAIBridgeClients(gomock.Any(), params, gomock.Any()).Return([]string{}, nil).AnyTimes()
|
||||
// No asserts here because SQLFilter.
|
||||
check.Args(params, emptyPreparedAuthorized{}).Asserts()
|
||||
}))
|
||||
|
||||
s.Run("ListAIBridgeSessions", s.Mocked(func(db *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
params := database.ListAIBridgeSessionsParams{}
|
||||
db.EXPECT().ListAuthorizedAIBridgeSessions(gomock.Any(), params, gomock.Any()).Return([]database.ListAIBridgeSessionsRow{}, nil).AnyTimes()
|
||||
@@ -5839,26 +5484,6 @@ func (s *MethodTestSuite) TestAIBridge() {
|
||||
check.Args(ids).Asserts(rbac.ResourceAibridgeInterception, policy.ActionRead).Returns([]database.AIBridgeToolUsage{})
|
||||
}))
|
||||
|
||||
s.Run("ListAIBridgeModelThoughtsByInterceptionIDs", s.Mocked(func(db *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
ids := []uuid.UUID{{1}}
|
||||
db.EXPECT().ListAIBridgeModelThoughtsByInterceptionIDs(gomock.Any(), ids).Return([]database.AIBridgeModelThought{}, nil).AnyTimes()
|
||||
check.Args(ids).Asserts(rbac.ResourceAibridgeInterception, policy.ActionRead).Returns([]database.AIBridgeModelThought{})
|
||||
}))
|
||||
|
||||
s.Run("ListAIBridgeSessionThreads", s.Mocked(func(db *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
params := database.ListAIBridgeSessionThreadsParams{}
|
||||
db.EXPECT().ListAuthorizedAIBridgeSessionThreads(gomock.Any(), params, gomock.Any()).Return([]database.ListAIBridgeSessionThreadsRow{}, nil).AnyTimes()
|
||||
// No asserts here because SQLFilter.
|
||||
check.Args(params).Asserts()
|
||||
}))
|
||||
|
||||
s.Run("ListAuthorizedAIBridgeSessionThreads", s.Mocked(func(db *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
params := database.ListAIBridgeSessionThreadsParams{}
|
||||
db.EXPECT().ListAuthorizedAIBridgeSessionThreads(gomock.Any(), params, gomock.Any()).Return([]database.ListAIBridgeSessionThreadsRow{}, nil).AnyTimes()
|
||||
// No asserts here because SQLFilter.
|
||||
check.Args(params, emptyPreparedAuthorized{}).Asserts()
|
||||
}))
|
||||
|
||||
s.Run("UpdateAIBridgeInterceptionEnded", s.Mocked(func(db *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
intcID := uuid.UUID{1}
|
||||
params := database.UpdateAIBridgeInterceptionEndedParams{ID: intcID}
|
||||
|
||||
@@ -1663,17 +1663,6 @@ func AIBridgeToolUsage(t testing.TB, db database.Store, seed database.InsertAIBr
|
||||
return toolUsage
|
||||
}
|
||||
|
||||
func AIBridgeModelThought(t testing.TB, db database.Store, seed database.InsertAIBridgeModelThoughtParams) database.AIBridgeModelThought {
|
||||
thought, err := db.InsertAIBridgeModelThought(genCtx, database.InsertAIBridgeModelThoughtParams{
|
||||
InterceptionID: takeFirst(seed.InterceptionID, uuid.New()),
|
||||
Content: takeFirst(seed.Content, ""),
|
||||
Metadata: takeFirstSlice(seed.Metadata, json.RawMessage("{}")),
|
||||
CreatedAt: takeFirst(seed.CreatedAt, dbtime.Now()),
|
||||
})
|
||||
require.NoError(t, err, "insert aibridge model thought")
|
||||
return thought
|
||||
}
|
||||
|
||||
func Task(t testing.TB, db database.Store, orig database.TaskTable) database.Task {
|
||||
t.Helper()
|
||||
|
||||
|
||||
@@ -160,12 +160,12 @@ func (m queryMetricsStore) AllUserIDs(ctx context.Context, includeSystem bool) (
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) ArchiveChatByID(ctx context.Context, id uuid.UUID) ([]database.Chat, error) {
|
||||
func (m queryMetricsStore) ArchiveChatByID(ctx context.Context, id uuid.UUID) error {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.ArchiveChatByID(ctx, id)
|
||||
r0 := m.s.ArchiveChatByID(ctx, id)
|
||||
m.queryLatencies.WithLabelValues("ArchiveChatByID").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "ArchiveChatByID").Inc()
|
||||
return r0, r1
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) ArchiveUnusedTemplateVersions(ctx context.Context, arg database.ArchiveUnusedTemplateVersionsParams) ([]uuid.UUID, error) {
|
||||
@@ -264,14 +264,6 @@ func (m queryMetricsStore) CleanTailnetTunnels(ctx context.Context) error {
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) CleanupDeletedMCPServerIDsFromChatAutomations(ctx context.Context) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.CleanupDeletedMCPServerIDsFromChatAutomations(ctx)
|
||||
m.queryLatencies.WithLabelValues("CleanupDeletedMCPServerIDsFromChatAutomations").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "CleanupDeletedMCPServerIDsFromChatAutomations").Inc()
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) CleanupDeletedMCPServerIDsFromChats(ctx context.Context) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.CleanupDeletedMCPServerIDsFromChats(ctx)
|
||||
@@ -304,22 +296,6 @@ func (m queryMetricsStore) CountAuditLogs(ctx context.Context, arg database.Coun
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) CountChatAutomationChatCreatesInWindow(ctx context.Context, arg database.CountChatAutomationChatCreatesInWindowParams) (int64, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.CountChatAutomationChatCreatesInWindow(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("CountChatAutomationChatCreatesInWindow").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "CountChatAutomationChatCreatesInWindow").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) CountChatAutomationMessagesInWindow(ctx context.Context, arg database.CountChatAutomationMessagesInWindowParams) (int64, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.CountChatAutomationMessagesInWindow(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("CountChatAutomationMessagesInWindow").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "CountChatAutomationMessagesInWindow").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) CountConnectionLogs(ctx context.Context, arg database.CountConnectionLogsParams) (int64, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.CountConnectionLogs(ctx, arg)
|
||||
@@ -424,22 +400,6 @@ func (m queryMetricsStore) DeleteApplicationConnectAPIKeysByUserID(ctx context.C
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) DeleteChatAutomationByID(ctx context.Context, id uuid.UUID) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.DeleteChatAutomationByID(ctx, id)
|
||||
m.queryLatencies.WithLabelValues("DeleteChatAutomationByID").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "DeleteChatAutomationByID").Inc()
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) DeleteChatAutomationTriggerByID(ctx context.Context, id uuid.UUID) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.DeleteChatAutomationTriggerByID(ctx, id)
|
||||
m.queryLatencies.WithLabelValues("DeleteChatAutomationTriggerByID").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "DeleteChatAutomationTriggerByID").Inc()
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) DeleteChatModelConfigByID(ctx context.Context, id uuid.UUID) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.DeleteChatModelConfigByID(ctx, id)
|
||||
@@ -976,14 +936,6 @@ func (m queryMetricsStore) GetActiveAISeatCount(ctx context.Context) (int64, err
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetActiveChatAutomationCronTriggers(ctx context.Context) ([]database.GetActiveChatAutomationCronTriggersRow, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetActiveChatAutomationCronTriggers(ctx)
|
||||
m.queryLatencies.WithLabelValues("GetActiveChatAutomationCronTriggers").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetActiveChatAutomationCronTriggers").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetActivePresetPrebuildSchedules(ctx context.Context) ([]database.TemplateVersionPresetPrebuildSchedule, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetActivePresetPrebuildSchedules(ctx)
|
||||
@@ -1080,46 +1032,6 @@ func (m queryMetricsStore) GetAuthorizationUserRoles(ctx context.Context, userID
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetChatAutomationByID(ctx context.Context, id uuid.UUID) (database.ChatAutomation, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetChatAutomationByID(ctx, id)
|
||||
m.queryLatencies.WithLabelValues("GetChatAutomationByID").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetChatAutomationByID").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetChatAutomationEventsByAutomationID(ctx context.Context, arg database.GetChatAutomationEventsByAutomationIDParams) ([]database.ChatAutomationEvent, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetChatAutomationEventsByAutomationID(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("GetChatAutomationEventsByAutomationID").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetChatAutomationEventsByAutomationID").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetChatAutomationTriggerByID(ctx context.Context, id uuid.UUID) (database.ChatAutomationTrigger, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetChatAutomationTriggerByID(ctx, id)
|
||||
m.queryLatencies.WithLabelValues("GetChatAutomationTriggerByID").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetChatAutomationTriggerByID").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetChatAutomationTriggersByAutomationID(ctx context.Context, automationID uuid.UUID) ([]database.ChatAutomationTrigger, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetChatAutomationTriggersByAutomationID(ctx, automationID)
|
||||
m.queryLatencies.WithLabelValues("GetChatAutomationTriggersByAutomationID").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetChatAutomationTriggersByAutomationID").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetChatAutomations(ctx context.Context, arg database.GetChatAutomationsParams) ([]database.ChatAutomation, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetChatAutomations(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("GetChatAutomations").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetChatAutomations").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetChatByID(ctx context.Context, id uuid.UUID) (database.Chat, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetChatByID(ctx, id)
|
||||
@@ -1208,14 +1120,6 @@ func (m queryMetricsStore) GetChatFilesByIDs(ctx context.Context, ids []uuid.UUI
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetChatIncludeDefaultSystemPrompt(ctx context.Context) (bool, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetChatIncludeDefaultSystemPrompt(ctx)
|
||||
m.queryLatencies.WithLabelValues("GetChatIncludeDefaultSystemPrompt").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetChatIncludeDefaultSystemPrompt").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetChatMessageByID(ctx context.Context, id int64) (database.ChatMessage, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetChatMessageByID(ctx, id)
|
||||
@@ -1232,14 +1136,6 @@ func (m queryMetricsStore) GetChatMessagesByChatID(ctx context.Context, chatID d
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetChatMessagesByChatIDAscPaginated(ctx context.Context, arg database.GetChatMessagesByChatIDAscPaginatedParams) ([]database.ChatMessage, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetChatMessagesByChatIDAscPaginated(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("GetChatMessagesByChatIDAscPaginated").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetChatMessagesByChatIDAscPaginated").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetChatMessagesByChatIDDescPaginated(ctx context.Context, arg database.GetChatMessagesByChatIDDescPaginatedParams) ([]database.ChatMessage, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetChatMessagesByChatIDDescPaginated(ctx, arg)
|
||||
@@ -1312,14 +1208,6 @@ func (m queryMetricsStore) GetChatSystemPrompt(ctx context.Context) (string, err
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetChatSystemPromptConfig(ctx context.Context) (database.GetChatSystemPromptConfigRow, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetChatSystemPromptConfig(ctx)
|
||||
m.queryLatencies.WithLabelValues("GetChatSystemPromptConfig").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetChatSystemPromptConfig").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetChatTemplateAllowlist(ctx context.Context) (string, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetChatTemplateAllowlist(ctx)
|
||||
@@ -1360,7 +1248,7 @@ func (m queryMetricsStore) GetChatWorkspaceTTL(ctx context.Context) (string, err
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetChats(ctx context.Context, arg database.GetChatsParams) ([]database.GetChatsRow, error) {
|
||||
func (m queryMetricsStore) GetChats(ctx context.Context, arg database.GetChatsParams) ([]database.Chat, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetChats(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("GetChats").Observe(time.Since(start).Seconds())
|
||||
@@ -1368,14 +1256,6 @@ func (m queryMetricsStore) GetChats(ctx context.Context, arg database.GetChatsPa
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetChatsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]database.Chat, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetChatsByWorkspaceIDs(ctx, ids)
|
||||
m.queryLatencies.WithLabelValues("GetChatsByWorkspaceIDs").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetChatsByWorkspaceIDs").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetConnectionLogsOffset(ctx context.Context, arg database.GetConnectionLogsOffsetParams) ([]database.GetConnectionLogsOffsetRow, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetConnectionLogsOffset(ctx, arg)
|
||||
@@ -2568,14 +2448,6 @@ func (m queryMetricsStore) GetUnexpiredLicenses(ctx context.Context) ([]database
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetUserAISeatStates(ctx context.Context, userIds []uuid.UUID) ([]uuid.UUID, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetUserAISeatStates(ctx, userIds)
|
||||
m.queryLatencies.WithLabelValues("GetUserAISeatStates").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetUserAISeatStates").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetUserActivityInsights(ctx context.Context, arg database.GetUserActivityInsightsParams) ([]database.GetUserActivityInsightsRow, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetUserActivityInsights(ctx, arg)
|
||||
@@ -3312,30 +3184,6 @@ func (m queryMetricsStore) InsertChat(ctx context.Context, arg database.InsertCh
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) InsertChatAutomation(ctx context.Context, arg database.InsertChatAutomationParams) (database.ChatAutomation, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.InsertChatAutomation(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("InsertChatAutomation").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "InsertChatAutomation").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) InsertChatAutomationEvent(ctx context.Context, arg database.InsertChatAutomationEventParams) (database.ChatAutomationEvent, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.InsertChatAutomationEvent(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("InsertChatAutomationEvent").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "InsertChatAutomationEvent").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) InsertChatAutomationTrigger(ctx context.Context, arg database.InsertChatAutomationTriggerParams) (database.ChatAutomationTrigger, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.InsertChatAutomationTrigger(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("InsertChatAutomationTrigger").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "InsertChatAutomationTrigger").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) InsertChatFile(ctx context.Context, arg database.InsertChatFileParams) (database.InsertChatFileRow, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.InsertChatFile(ctx, arg)
|
||||
@@ -3864,14 +3712,6 @@ func (m queryMetricsStore) InsertWorkspaceResourceMetadata(ctx context.Context,
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) ListAIBridgeClients(ctx context.Context, arg database.ListAIBridgeClientsParams) ([]string, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.ListAIBridgeClients(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("ListAIBridgeClients").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "ListAIBridgeClients").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) ListAIBridgeInterceptions(ctx context.Context, arg database.ListAIBridgeInterceptionsParams) ([]database.ListAIBridgeInterceptionsRow, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.ListAIBridgeInterceptions(ctx, arg)
|
||||
@@ -3888,14 +3728,6 @@ func (m queryMetricsStore) ListAIBridgeInterceptionsTelemetrySummaries(ctx conte
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) ListAIBridgeModelThoughtsByInterceptionIDs(ctx context.Context, interceptionIds []uuid.UUID) ([]database.AIBridgeModelThought, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.ListAIBridgeModelThoughtsByInterceptionIDs(ctx, interceptionIds)
|
||||
m.queryLatencies.WithLabelValues("ListAIBridgeModelThoughtsByInterceptionIDs").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "ListAIBridgeModelThoughtsByInterceptionIDs").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) ListAIBridgeModels(ctx context.Context, arg database.ListAIBridgeModelsParams) ([]string, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.ListAIBridgeModels(ctx, arg)
|
||||
@@ -3904,14 +3736,6 @@ func (m queryMetricsStore) ListAIBridgeModels(ctx context.Context, arg database.
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) ListAIBridgeSessionThreads(ctx context.Context, arg database.ListAIBridgeSessionThreadsParams) ([]database.ListAIBridgeSessionThreadsRow, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.ListAIBridgeSessionThreads(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("ListAIBridgeSessionThreads").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "ListAIBridgeSessionThreads").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) ListAIBridgeSessions(ctx context.Context, arg database.ListAIBridgeSessionsParams) ([]database.ListAIBridgeSessionsRow, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.ListAIBridgeSessions(ctx, arg)
|
||||
@@ -4048,14 +3872,6 @@ func (m queryMetricsStore) PaginatedOrganizationMembers(ctx context.Context, arg
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) PinChatByID(ctx context.Context, id uuid.UUID) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.PinChatByID(ctx, id)
|
||||
m.queryLatencies.WithLabelValues("PinChatByID").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "PinChatByID").Inc()
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) PopNextQueuedMessage(ctx context.Context, chatID uuid.UUID) (database.ChatQueuedMessage, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.PopNextQueuedMessage(ctx, chatID)
|
||||
@@ -4064,14 +3880,6 @@ func (m queryMetricsStore) PopNextQueuedMessage(ctx context.Context, chatID uuid
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) PurgeOldChatAutomationEvents(ctx context.Context, arg database.PurgeOldChatAutomationEventsParams) (int64, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.PurgeOldChatAutomationEvents(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("PurgeOldChatAutomationEvents").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "PurgeOldChatAutomationEvents").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate(ctx context.Context, templateID uuid.UUID) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate(ctx, templateID)
|
||||
@@ -4144,12 +3952,12 @@ func (m queryMetricsStore) TryAcquireLock(ctx context.Context, pgTryAdvisoryXact
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UnarchiveChatByID(ctx context.Context, id uuid.UUID) ([]database.Chat, error) {
|
||||
func (m queryMetricsStore) UnarchiveChatByID(ctx context.Context, id uuid.UUID) error {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UnarchiveChatByID(ctx, id)
|
||||
r0 := m.s.UnarchiveChatByID(ctx, id)
|
||||
m.queryLatencies.WithLabelValues("UnarchiveChatByID").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UnarchiveChatByID").Inc()
|
||||
return r0, r1
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UnarchiveTemplateVersion(ctx context.Context, arg database.UnarchiveTemplateVersionParams) error {
|
||||
@@ -4168,14 +3976,6 @@ func (m queryMetricsStore) UnfavoriteWorkspace(ctx context.Context, id uuid.UUID
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UnpinChatByID(ctx context.Context, id uuid.UUID) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.UnpinChatByID(ctx, id)
|
||||
m.queryLatencies.WithLabelValues("UnpinChatByID").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UnpinChatByID").Inc()
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UnsetDefaultChatModelConfigs(ctx context.Context) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.UnsetDefaultChatModelConfigs(ctx)
|
||||
@@ -4200,46 +4000,6 @@ func (m queryMetricsStore) UpdateAPIKeyByID(ctx context.Context, arg database.Up
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateChatAutomation(ctx context.Context, arg database.UpdateChatAutomationParams) (database.ChatAutomation, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpdateChatAutomation(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UpdateChatAutomation").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpdateChatAutomation").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateChatAutomationTrigger(ctx context.Context, arg database.UpdateChatAutomationTriggerParams) (database.ChatAutomationTrigger, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpdateChatAutomationTrigger(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UpdateChatAutomationTrigger").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpdateChatAutomationTrigger").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateChatAutomationTriggerLastTriggeredAt(ctx context.Context, arg database.UpdateChatAutomationTriggerLastTriggeredAtParams) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.UpdateChatAutomationTriggerLastTriggeredAt(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UpdateChatAutomationTriggerLastTriggeredAt").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpdateChatAutomationTriggerLastTriggeredAt").Inc()
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateChatAutomationTriggerWebhookSecret(ctx context.Context, arg database.UpdateChatAutomationTriggerWebhookSecretParams) (database.ChatAutomationTrigger, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpdateChatAutomationTriggerWebhookSecret(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UpdateChatAutomationTriggerWebhookSecret").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpdateChatAutomationTriggerWebhookSecret").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateChatBuildAgentBinding(ctx context.Context, arg database.UpdateChatBuildAgentBindingParams) (database.Chat, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpdateChatBuildAgentBinding(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UpdateChatBuildAgentBinding").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpdateChatBuildAgentBinding").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateChatByID(ctx context.Context, arg database.UpdateChatByIDParams) (database.Chat, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpdateChatByID(ctx, arg)
|
||||
@@ -4264,30 +4024,6 @@ func (m queryMetricsStore) UpdateChatLabelsByID(ctx context.Context, arg databas
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateChatLastInjectedContext(ctx context.Context, arg database.UpdateChatLastInjectedContextParams) (database.Chat, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpdateChatLastInjectedContext(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UpdateChatLastInjectedContext").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpdateChatLastInjectedContext").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateChatLastModelConfigByID(ctx context.Context, arg database.UpdateChatLastModelConfigByIDParams) (database.Chat, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpdateChatLastModelConfigByID(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UpdateChatLastModelConfigByID").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpdateChatLastModelConfigByID").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateChatLastReadMessageID(ctx context.Context, arg database.UpdateChatLastReadMessageIDParams) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.UpdateChatLastReadMessageID(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UpdateChatLastReadMessageID").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpdateChatLastReadMessageID").Inc()
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateChatMCPServerIDs(ctx context.Context, arg database.UpdateChatMCPServerIDsParams) (database.Chat, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpdateChatMCPServerIDs(ctx, arg)
|
||||
@@ -4312,14 +4048,6 @@ func (m queryMetricsStore) UpdateChatModelConfig(ctx context.Context, arg databa
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateChatPinOrder(ctx context.Context, arg database.UpdateChatPinOrderParams) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.UpdateChatPinOrder(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UpdateChatPinOrder").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpdateChatPinOrder").Inc()
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateChatProvider(ctx context.Context, arg database.UpdateChatProviderParams) (database.ChatProvider, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpdateChatProvider(ctx, arg)
|
||||
@@ -4336,19 +4064,11 @@ func (m queryMetricsStore) UpdateChatStatus(ctx context.Context, arg database.Up
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateChatStatusPreserveUpdatedAt(ctx context.Context, arg database.UpdateChatStatusPreserveUpdatedAtParams) (database.Chat, error) {
|
||||
func (m queryMetricsStore) UpdateChatWorkspace(ctx context.Context, arg database.UpdateChatWorkspaceParams) (database.Chat, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpdateChatStatusPreserveUpdatedAt(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UpdateChatStatusPreserveUpdatedAt").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpdateChatStatusPreserveUpdatedAt").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpdateChatWorkspaceBinding(ctx context.Context, arg database.UpdateChatWorkspaceBindingParams) (database.Chat, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpdateChatWorkspaceBinding(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UpdateChatWorkspaceBinding").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpdateChatWorkspaceBinding").Inc()
|
||||
r0, r1 := m.s.UpdateChatWorkspace(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("UpdateChatWorkspace").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpdateChatWorkspace").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
@@ -5096,14 +4816,6 @@ func (m queryMetricsStore) UpsertChatDiffStatusReference(ctx context.Context, ar
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpsertChatIncludeDefaultSystemPrompt(ctx context.Context, includeDefaultSystemPrompt bool) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.UpsertChatIncludeDefaultSystemPrompt(ctx, includeDefaultSystemPrompt)
|
||||
m.queryLatencies.WithLabelValues("UpsertChatIncludeDefaultSystemPrompt").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpsertChatIncludeDefaultSystemPrompt").Inc()
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpsertChatSystemPrompt(ctx context.Context, value string) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.UpsertChatSystemPrompt(ctx, value)
|
||||
@@ -5464,14 +5176,6 @@ func (m queryMetricsStore) ListAuthorizedAIBridgeModels(ctx context.Context, arg
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) ListAuthorizedAIBridgeClients(ctx context.Context, arg database.ListAIBridgeClientsParams, prepared rbac.PreparedAuthorized) ([]string, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.ListAuthorizedAIBridgeClients(ctx, arg, prepared)
|
||||
m.queryLatencies.WithLabelValues("ListAuthorizedAIBridgeClients").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "ListAuthorizedAIBridgeClients").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) ListAuthorizedAIBridgeSessions(ctx context.Context, arg database.ListAIBridgeSessionsParams, prepared rbac.PreparedAuthorized) ([]database.ListAIBridgeSessionsRow, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.ListAuthorizedAIBridgeSessions(ctx, arg, prepared)
|
||||
@@ -5488,26 +5192,10 @@ func (m queryMetricsStore) CountAuthorizedAIBridgeSessions(ctx context.Context,
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) ListAuthorizedAIBridgeSessionThreads(ctx context.Context, arg database.ListAIBridgeSessionThreadsParams, prepared rbac.PreparedAuthorized) ([]database.ListAIBridgeSessionThreadsRow, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.ListAuthorizedAIBridgeSessionThreads(ctx, arg, prepared)
|
||||
m.queryLatencies.WithLabelValues("ListAuthorizedAIBridgeSessionThreads").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "ListAuthorizedAIBridgeSessionThreads").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetAuthorizedChats(ctx context.Context, arg database.GetChatsParams, prepared rbac.PreparedAuthorized) ([]database.GetChatsRow, error) {
|
||||
func (m queryMetricsStore) GetAuthorizedChats(ctx context.Context, arg database.GetChatsParams, prepared rbac.PreparedAuthorized) ([]database.Chat, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetAuthorizedChats(ctx, arg, prepared)
|
||||
m.queryLatencies.WithLabelValues("GetAuthorizedChats").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetAuthorizedChats").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetAuthorizedChatAutomations(ctx context.Context, arg database.GetChatAutomationsParams, prepared rbac.PreparedAuthorized) ([]database.ChatAutomation, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetAuthorizedChatAutomations(ctx, arg, prepared)
|
||||
m.queryLatencies.WithLabelValues("GetAuthorizedChatAutomations").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetAuthorizedChatAutomations").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
@@ -148,12 +148,11 @@ func (mr *MockStoreMockRecorder) AllUserIDs(ctx, includeSystem any) *gomock.Call
|
||||
}
|
||||
|
||||
// ArchiveChatByID mocks base method.
|
||||
func (m *MockStore) ArchiveChatByID(ctx context.Context, id uuid.UUID) ([]database.Chat, error) {
|
||||
func (m *MockStore) ArchiveChatByID(ctx context.Context, id uuid.UUID) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ArchiveChatByID", ctx, id)
|
||||
ret0, _ := ret[0].([]database.Chat)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// ArchiveChatByID indicates an expected call of ArchiveChatByID.
|
||||
@@ -335,20 +334,6 @@ func (mr *MockStoreMockRecorder) CleanTailnetTunnels(ctx any) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CleanTailnetTunnels", reflect.TypeOf((*MockStore)(nil).CleanTailnetTunnels), ctx)
|
||||
}
|
||||
|
||||
// CleanupDeletedMCPServerIDsFromChatAutomations mocks base method.
|
||||
func (m *MockStore) CleanupDeletedMCPServerIDsFromChatAutomations(ctx context.Context) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "CleanupDeletedMCPServerIDsFromChatAutomations", ctx)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// CleanupDeletedMCPServerIDsFromChatAutomations indicates an expected call of CleanupDeletedMCPServerIDsFromChatAutomations.
|
||||
func (mr *MockStoreMockRecorder) CleanupDeletedMCPServerIDsFromChatAutomations(ctx any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CleanupDeletedMCPServerIDsFromChatAutomations", reflect.TypeOf((*MockStore)(nil).CleanupDeletedMCPServerIDsFromChatAutomations), ctx)
|
||||
}
|
||||
|
||||
// CleanupDeletedMCPServerIDsFromChats mocks base method.
|
||||
func (m *MockStore) CleanupDeletedMCPServerIDsFromChats(ctx context.Context) error {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -468,36 +453,6 @@ func (mr *MockStoreMockRecorder) CountAuthorizedConnectionLogs(ctx, arg, prepare
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CountAuthorizedConnectionLogs", reflect.TypeOf((*MockStore)(nil).CountAuthorizedConnectionLogs), ctx, arg, prepared)
|
||||
}
|
||||
|
||||
// CountChatAutomationChatCreatesInWindow mocks base method.
|
||||
func (m *MockStore) CountChatAutomationChatCreatesInWindow(ctx context.Context, arg database.CountChatAutomationChatCreatesInWindowParams) (int64, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "CountChatAutomationChatCreatesInWindow", ctx, arg)
|
||||
ret0, _ := ret[0].(int64)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// CountChatAutomationChatCreatesInWindow indicates an expected call of CountChatAutomationChatCreatesInWindow.
|
||||
func (mr *MockStoreMockRecorder) CountChatAutomationChatCreatesInWindow(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CountChatAutomationChatCreatesInWindow", reflect.TypeOf((*MockStore)(nil).CountChatAutomationChatCreatesInWindow), ctx, arg)
|
||||
}
|
||||
|
||||
// CountChatAutomationMessagesInWindow mocks base method.
|
||||
func (m *MockStore) CountChatAutomationMessagesInWindow(ctx context.Context, arg database.CountChatAutomationMessagesInWindowParams) (int64, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "CountChatAutomationMessagesInWindow", ctx, arg)
|
||||
ret0, _ := ret[0].(int64)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// CountChatAutomationMessagesInWindow indicates an expected call of CountChatAutomationMessagesInWindow.
|
||||
func (mr *MockStoreMockRecorder) CountChatAutomationMessagesInWindow(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CountChatAutomationMessagesInWindow", reflect.TypeOf((*MockStore)(nil).CountChatAutomationMessagesInWindow), ctx, arg)
|
||||
}
|
||||
|
||||
// CountConnectionLogs mocks base method.
|
||||
func (m *MockStore) CountConnectionLogs(ctx context.Context, arg database.CountConnectionLogsParams) (int64, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -687,34 +642,6 @@ func (mr *MockStoreMockRecorder) DeleteApplicationConnectAPIKeysByUserID(ctx, us
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteApplicationConnectAPIKeysByUserID", reflect.TypeOf((*MockStore)(nil).DeleteApplicationConnectAPIKeysByUserID), ctx, userID)
|
||||
}
|
||||
|
||||
// DeleteChatAutomationByID mocks base method.
|
||||
func (m *MockStore) DeleteChatAutomationByID(ctx context.Context, id uuid.UUID) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "DeleteChatAutomationByID", ctx, id)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// DeleteChatAutomationByID indicates an expected call of DeleteChatAutomationByID.
|
||||
func (mr *MockStoreMockRecorder) DeleteChatAutomationByID(ctx, id any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteChatAutomationByID", reflect.TypeOf((*MockStore)(nil).DeleteChatAutomationByID), ctx, id)
|
||||
}
|
||||
|
||||
// DeleteChatAutomationTriggerByID mocks base method.
|
||||
func (m *MockStore) DeleteChatAutomationTriggerByID(ctx context.Context, id uuid.UUID) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "DeleteChatAutomationTriggerByID", ctx, id)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// DeleteChatAutomationTriggerByID indicates an expected call of DeleteChatAutomationTriggerByID.
|
||||
func (mr *MockStoreMockRecorder) DeleteChatAutomationTriggerByID(ctx, id any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteChatAutomationTriggerByID", reflect.TypeOf((*MockStore)(nil).DeleteChatAutomationTriggerByID), ctx, id)
|
||||
}
|
||||
|
||||
// DeleteChatModelConfigByID mocks base method.
|
||||
func (m *MockStore) DeleteChatModelConfigByID(ctx context.Context, id uuid.UUID) error {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -1681,21 +1608,6 @@ func (mr *MockStoreMockRecorder) GetActiveAISeatCount(ctx any) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActiveAISeatCount", reflect.TypeOf((*MockStore)(nil).GetActiveAISeatCount), ctx)
|
||||
}
|
||||
|
||||
// GetActiveChatAutomationCronTriggers mocks base method.
|
||||
func (m *MockStore) GetActiveChatAutomationCronTriggers(ctx context.Context) ([]database.GetActiveChatAutomationCronTriggersRow, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetActiveChatAutomationCronTriggers", ctx)
|
||||
ret0, _ := ret[0].([]database.GetActiveChatAutomationCronTriggersRow)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetActiveChatAutomationCronTriggers indicates an expected call of GetActiveChatAutomationCronTriggers.
|
||||
func (mr *MockStoreMockRecorder) GetActiveChatAutomationCronTriggers(ctx any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActiveChatAutomationCronTriggers", reflect.TypeOf((*MockStore)(nil).GetActiveChatAutomationCronTriggers), ctx)
|
||||
}
|
||||
|
||||
// GetActivePresetPrebuildSchedules mocks base method.
|
||||
func (m *MockStore) GetActivePresetPrebuildSchedules(ctx context.Context) ([]database.TemplateVersionPresetPrebuildSchedule, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -1891,26 +1803,11 @@ func (mr *MockStoreMockRecorder) GetAuthorizedAuditLogsOffset(ctx, arg, prepared
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthorizedAuditLogsOffset", reflect.TypeOf((*MockStore)(nil).GetAuthorizedAuditLogsOffset), ctx, arg, prepared)
|
||||
}
|
||||
|
||||
// GetAuthorizedChatAutomations mocks base method.
|
||||
func (m *MockStore) GetAuthorizedChatAutomations(ctx context.Context, arg database.GetChatAutomationsParams, prepared rbac.PreparedAuthorized) ([]database.ChatAutomation, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetAuthorizedChatAutomations", ctx, arg, prepared)
|
||||
ret0, _ := ret[0].([]database.ChatAutomation)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetAuthorizedChatAutomations indicates an expected call of GetAuthorizedChatAutomations.
|
||||
func (mr *MockStoreMockRecorder) GetAuthorizedChatAutomations(ctx, arg, prepared any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthorizedChatAutomations", reflect.TypeOf((*MockStore)(nil).GetAuthorizedChatAutomations), ctx, arg, prepared)
|
||||
}
|
||||
|
||||
// GetAuthorizedChats mocks base method.
|
||||
func (m *MockStore) GetAuthorizedChats(ctx context.Context, arg database.GetChatsParams, prepared rbac.PreparedAuthorized) ([]database.GetChatsRow, error) {
|
||||
func (m *MockStore) GetAuthorizedChats(ctx context.Context, arg database.GetChatsParams, prepared rbac.PreparedAuthorized) ([]database.Chat, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetAuthorizedChats", ctx, arg, prepared)
|
||||
ret0, _ := ret[0].([]database.GetChatsRow)
|
||||
ret0, _ := ret[0].([]database.Chat)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
@@ -1996,81 +1893,6 @@ func (mr *MockStoreMockRecorder) GetAuthorizedWorkspacesAndAgentsByOwnerID(ctx,
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthorizedWorkspacesAndAgentsByOwnerID", reflect.TypeOf((*MockStore)(nil).GetAuthorizedWorkspacesAndAgentsByOwnerID), ctx, ownerID, prepared)
|
||||
}
|
||||
|
||||
// GetChatAutomationByID mocks base method.
|
||||
func (m *MockStore) GetChatAutomationByID(ctx context.Context, id uuid.UUID) (database.ChatAutomation, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetChatAutomationByID", ctx, id)
|
||||
ret0, _ := ret[0].(database.ChatAutomation)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetChatAutomationByID indicates an expected call of GetChatAutomationByID.
|
||||
func (mr *MockStoreMockRecorder) GetChatAutomationByID(ctx, id any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChatAutomationByID", reflect.TypeOf((*MockStore)(nil).GetChatAutomationByID), ctx, id)
|
||||
}
|
||||
|
||||
// GetChatAutomationEventsByAutomationID mocks base method.
|
||||
func (m *MockStore) GetChatAutomationEventsByAutomationID(ctx context.Context, arg database.GetChatAutomationEventsByAutomationIDParams) ([]database.ChatAutomationEvent, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetChatAutomationEventsByAutomationID", ctx, arg)
|
||||
ret0, _ := ret[0].([]database.ChatAutomationEvent)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetChatAutomationEventsByAutomationID indicates an expected call of GetChatAutomationEventsByAutomationID.
|
||||
func (mr *MockStoreMockRecorder) GetChatAutomationEventsByAutomationID(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChatAutomationEventsByAutomationID", reflect.TypeOf((*MockStore)(nil).GetChatAutomationEventsByAutomationID), ctx, arg)
|
||||
}
|
||||
|
||||
// GetChatAutomationTriggerByID mocks base method.
|
||||
func (m *MockStore) GetChatAutomationTriggerByID(ctx context.Context, id uuid.UUID) (database.ChatAutomationTrigger, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetChatAutomationTriggerByID", ctx, id)
|
||||
ret0, _ := ret[0].(database.ChatAutomationTrigger)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetChatAutomationTriggerByID indicates an expected call of GetChatAutomationTriggerByID.
|
||||
func (mr *MockStoreMockRecorder) GetChatAutomationTriggerByID(ctx, id any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChatAutomationTriggerByID", reflect.TypeOf((*MockStore)(nil).GetChatAutomationTriggerByID), ctx, id)
|
||||
}
|
||||
|
||||
// GetChatAutomationTriggersByAutomationID mocks base method.
|
||||
func (m *MockStore) GetChatAutomationTriggersByAutomationID(ctx context.Context, automationID uuid.UUID) ([]database.ChatAutomationTrigger, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetChatAutomationTriggersByAutomationID", ctx, automationID)
|
||||
ret0, _ := ret[0].([]database.ChatAutomationTrigger)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetChatAutomationTriggersByAutomationID indicates an expected call of GetChatAutomationTriggersByAutomationID.
|
||||
func (mr *MockStoreMockRecorder) GetChatAutomationTriggersByAutomationID(ctx, automationID any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChatAutomationTriggersByAutomationID", reflect.TypeOf((*MockStore)(nil).GetChatAutomationTriggersByAutomationID), ctx, automationID)
|
||||
}
|
||||
|
||||
// GetChatAutomations mocks base method.
|
||||
func (m *MockStore) GetChatAutomations(ctx context.Context, arg database.GetChatAutomationsParams) ([]database.ChatAutomation, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetChatAutomations", ctx, arg)
|
||||
ret0, _ := ret[0].([]database.ChatAutomation)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetChatAutomations indicates an expected call of GetChatAutomations.
|
||||
func (mr *MockStoreMockRecorder) GetChatAutomations(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChatAutomations", reflect.TypeOf((*MockStore)(nil).GetChatAutomations), ctx, arg)
|
||||
}
|
||||
|
||||
// GetChatByID mocks base method.
|
||||
func (m *MockStore) GetChatByID(ctx context.Context, id uuid.UUID) (database.Chat, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -2236,21 +2058,6 @@ func (mr *MockStoreMockRecorder) GetChatFilesByIDs(ctx, ids any) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChatFilesByIDs", reflect.TypeOf((*MockStore)(nil).GetChatFilesByIDs), ctx, ids)
|
||||
}
|
||||
|
||||
// GetChatIncludeDefaultSystemPrompt mocks base method.
|
||||
func (m *MockStore) GetChatIncludeDefaultSystemPrompt(ctx context.Context) (bool, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetChatIncludeDefaultSystemPrompt", ctx)
|
||||
ret0, _ := ret[0].(bool)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetChatIncludeDefaultSystemPrompt indicates an expected call of GetChatIncludeDefaultSystemPrompt.
|
||||
func (mr *MockStoreMockRecorder) GetChatIncludeDefaultSystemPrompt(ctx any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChatIncludeDefaultSystemPrompt", reflect.TypeOf((*MockStore)(nil).GetChatIncludeDefaultSystemPrompt), ctx)
|
||||
}
|
||||
|
||||
// GetChatMessageByID mocks base method.
|
||||
func (m *MockStore) GetChatMessageByID(ctx context.Context, id int64) (database.ChatMessage, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -2281,21 +2088,6 @@ func (mr *MockStoreMockRecorder) GetChatMessagesByChatID(ctx, arg any) *gomock.C
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChatMessagesByChatID", reflect.TypeOf((*MockStore)(nil).GetChatMessagesByChatID), ctx, arg)
|
||||
}
|
||||
|
||||
// GetChatMessagesByChatIDAscPaginated mocks base method.
|
||||
func (m *MockStore) GetChatMessagesByChatIDAscPaginated(ctx context.Context, arg database.GetChatMessagesByChatIDAscPaginatedParams) ([]database.ChatMessage, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetChatMessagesByChatIDAscPaginated", ctx, arg)
|
||||
ret0, _ := ret[0].([]database.ChatMessage)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetChatMessagesByChatIDAscPaginated indicates an expected call of GetChatMessagesByChatIDAscPaginated.
|
||||
func (mr *MockStoreMockRecorder) GetChatMessagesByChatIDAscPaginated(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChatMessagesByChatIDAscPaginated", reflect.TypeOf((*MockStore)(nil).GetChatMessagesByChatIDAscPaginated), ctx, arg)
|
||||
}
|
||||
|
||||
// GetChatMessagesByChatIDDescPaginated mocks base method.
|
||||
func (m *MockStore) GetChatMessagesByChatIDDescPaginated(ctx context.Context, arg database.GetChatMessagesByChatIDDescPaginatedParams) ([]database.ChatMessage, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -2431,21 +2223,6 @@ func (mr *MockStoreMockRecorder) GetChatSystemPrompt(ctx any) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChatSystemPrompt", reflect.TypeOf((*MockStore)(nil).GetChatSystemPrompt), ctx)
|
||||
}
|
||||
|
||||
// GetChatSystemPromptConfig mocks base method.
|
||||
func (m *MockStore) GetChatSystemPromptConfig(ctx context.Context) (database.GetChatSystemPromptConfigRow, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetChatSystemPromptConfig", ctx)
|
||||
ret0, _ := ret[0].(database.GetChatSystemPromptConfigRow)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetChatSystemPromptConfig indicates an expected call of GetChatSystemPromptConfig.
|
||||
func (mr *MockStoreMockRecorder) GetChatSystemPromptConfig(ctx any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChatSystemPromptConfig", reflect.TypeOf((*MockStore)(nil).GetChatSystemPromptConfig), ctx)
|
||||
}
|
||||
|
||||
// GetChatTemplateAllowlist mocks base method.
|
||||
func (m *MockStore) GetChatTemplateAllowlist(ctx context.Context) (string, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -2522,10 +2299,10 @@ func (mr *MockStoreMockRecorder) GetChatWorkspaceTTL(ctx any) *gomock.Call {
|
||||
}
|
||||
|
||||
// GetChats mocks base method.
|
||||
func (m *MockStore) GetChats(ctx context.Context, arg database.GetChatsParams) ([]database.GetChatsRow, error) {
|
||||
func (m *MockStore) GetChats(ctx context.Context, arg database.GetChatsParams) ([]database.Chat, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetChats", ctx, arg)
|
||||
ret0, _ := ret[0].([]database.GetChatsRow)
|
||||
ret0, _ := ret[0].([]database.Chat)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
@@ -2536,21 +2313,6 @@ func (mr *MockStoreMockRecorder) GetChats(ctx, arg any) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChats", reflect.TypeOf((*MockStore)(nil).GetChats), ctx, arg)
|
||||
}
|
||||
|
||||
// GetChatsByWorkspaceIDs mocks base method.
|
||||
func (m *MockStore) GetChatsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]database.Chat, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetChatsByWorkspaceIDs", ctx, ids)
|
||||
ret0, _ := ret[0].([]database.Chat)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetChatsByWorkspaceIDs indicates an expected call of GetChatsByWorkspaceIDs.
|
||||
func (mr *MockStoreMockRecorder) GetChatsByWorkspaceIDs(ctx, ids any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetChatsByWorkspaceIDs", reflect.TypeOf((*MockStore)(nil).GetChatsByWorkspaceIDs), ctx, ids)
|
||||
}
|
||||
|
||||
// GetConnectionLogsOffset mocks base method.
|
||||
func (m *MockStore) GetConnectionLogsOffset(ctx context.Context, arg database.GetConnectionLogsOffsetParams) ([]database.GetConnectionLogsOffsetRow, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -4816,21 +4578,6 @@ func (mr *MockStoreMockRecorder) GetUnexpiredLicenses(ctx any) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUnexpiredLicenses", reflect.TypeOf((*MockStore)(nil).GetUnexpiredLicenses), ctx)
|
||||
}
|
||||
|
||||
// GetUserAISeatStates mocks base method.
|
||||
func (m *MockStore) GetUserAISeatStates(ctx context.Context, userIds []uuid.UUID) ([]uuid.UUID, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetUserAISeatStates", ctx, userIds)
|
||||
ret0, _ := ret[0].([]uuid.UUID)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetUserAISeatStates indicates an expected call of GetUserAISeatStates.
|
||||
func (mr *MockStoreMockRecorder) GetUserAISeatStates(ctx, userIds any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserAISeatStates", reflect.TypeOf((*MockStore)(nil).GetUserAISeatStates), ctx, userIds)
|
||||
}
|
||||
|
||||
// GetUserActivityInsights mocks base method.
|
||||
func (m *MockStore) GetUserActivityInsights(ctx context.Context, arg database.GetUserActivityInsightsParams) ([]database.GetUserActivityInsightsRow, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -6225,51 +5972,6 @@ func (mr *MockStoreMockRecorder) InsertChat(ctx, arg any) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertChat", reflect.TypeOf((*MockStore)(nil).InsertChat), ctx, arg)
|
||||
}
|
||||
|
||||
// InsertChatAutomation mocks base method.
|
||||
func (m *MockStore) InsertChatAutomation(ctx context.Context, arg database.InsertChatAutomationParams) (database.ChatAutomation, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "InsertChatAutomation", ctx, arg)
|
||||
ret0, _ := ret[0].(database.ChatAutomation)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// InsertChatAutomation indicates an expected call of InsertChatAutomation.
|
||||
func (mr *MockStoreMockRecorder) InsertChatAutomation(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertChatAutomation", reflect.TypeOf((*MockStore)(nil).InsertChatAutomation), ctx, arg)
|
||||
}
|
||||
|
||||
// InsertChatAutomationEvent mocks base method.
|
||||
func (m *MockStore) InsertChatAutomationEvent(ctx context.Context, arg database.InsertChatAutomationEventParams) (database.ChatAutomationEvent, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "InsertChatAutomationEvent", ctx, arg)
|
||||
ret0, _ := ret[0].(database.ChatAutomationEvent)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// InsertChatAutomationEvent indicates an expected call of InsertChatAutomationEvent.
|
||||
func (mr *MockStoreMockRecorder) InsertChatAutomationEvent(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertChatAutomationEvent", reflect.TypeOf((*MockStore)(nil).InsertChatAutomationEvent), ctx, arg)
|
||||
}
|
||||
|
||||
// InsertChatAutomationTrigger mocks base method.
|
||||
func (m *MockStore) InsertChatAutomationTrigger(ctx context.Context, arg database.InsertChatAutomationTriggerParams) (database.ChatAutomationTrigger, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "InsertChatAutomationTrigger", ctx, arg)
|
||||
ret0, _ := ret[0].(database.ChatAutomationTrigger)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// InsertChatAutomationTrigger indicates an expected call of InsertChatAutomationTrigger.
|
||||
func (mr *MockStoreMockRecorder) InsertChatAutomationTrigger(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertChatAutomationTrigger", reflect.TypeOf((*MockStore)(nil).InsertChatAutomationTrigger), ctx, arg)
|
||||
}
|
||||
|
||||
// InsertChatFile mocks base method.
|
||||
func (m *MockStore) InsertChatFile(ctx context.Context, arg database.InsertChatFileParams) (database.InsertChatFileRow, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -7245,21 +6947,6 @@ func (mr *MockStoreMockRecorder) InsertWorkspaceResourceMetadata(ctx, arg any) *
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InsertWorkspaceResourceMetadata", reflect.TypeOf((*MockStore)(nil).InsertWorkspaceResourceMetadata), ctx, arg)
|
||||
}
|
||||
|
||||
// ListAIBridgeClients mocks base method.
|
||||
func (m *MockStore) ListAIBridgeClients(ctx context.Context, arg database.ListAIBridgeClientsParams) ([]string, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ListAIBridgeClients", ctx, arg)
|
||||
ret0, _ := ret[0].([]string)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ListAIBridgeClients indicates an expected call of ListAIBridgeClients.
|
||||
func (mr *MockStoreMockRecorder) ListAIBridgeClients(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAIBridgeClients", reflect.TypeOf((*MockStore)(nil).ListAIBridgeClients), ctx, arg)
|
||||
}
|
||||
|
||||
// ListAIBridgeInterceptions mocks base method.
|
||||
func (m *MockStore) ListAIBridgeInterceptions(ctx context.Context, arg database.ListAIBridgeInterceptionsParams) ([]database.ListAIBridgeInterceptionsRow, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -7290,21 +6977,6 @@ func (mr *MockStoreMockRecorder) ListAIBridgeInterceptionsTelemetrySummaries(ctx
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAIBridgeInterceptionsTelemetrySummaries", reflect.TypeOf((*MockStore)(nil).ListAIBridgeInterceptionsTelemetrySummaries), ctx, arg)
|
||||
}
|
||||
|
||||
// ListAIBridgeModelThoughtsByInterceptionIDs mocks base method.
|
||||
func (m *MockStore) ListAIBridgeModelThoughtsByInterceptionIDs(ctx context.Context, interceptionIds []uuid.UUID) ([]database.AIBridgeModelThought, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ListAIBridgeModelThoughtsByInterceptionIDs", ctx, interceptionIds)
|
||||
ret0, _ := ret[0].([]database.AIBridgeModelThought)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ListAIBridgeModelThoughtsByInterceptionIDs indicates an expected call of ListAIBridgeModelThoughtsByInterceptionIDs.
|
||||
func (mr *MockStoreMockRecorder) ListAIBridgeModelThoughtsByInterceptionIDs(ctx, interceptionIds any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAIBridgeModelThoughtsByInterceptionIDs", reflect.TypeOf((*MockStore)(nil).ListAIBridgeModelThoughtsByInterceptionIDs), ctx, interceptionIds)
|
||||
}
|
||||
|
||||
// ListAIBridgeModels mocks base method.
|
||||
func (m *MockStore) ListAIBridgeModels(ctx context.Context, arg database.ListAIBridgeModelsParams) ([]string, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -7320,21 +6992,6 @@ func (mr *MockStoreMockRecorder) ListAIBridgeModels(ctx, arg any) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAIBridgeModels", reflect.TypeOf((*MockStore)(nil).ListAIBridgeModels), ctx, arg)
|
||||
}
|
||||
|
||||
// ListAIBridgeSessionThreads mocks base method.
|
||||
func (m *MockStore) ListAIBridgeSessionThreads(ctx context.Context, arg database.ListAIBridgeSessionThreadsParams) ([]database.ListAIBridgeSessionThreadsRow, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ListAIBridgeSessionThreads", ctx, arg)
|
||||
ret0, _ := ret[0].([]database.ListAIBridgeSessionThreadsRow)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ListAIBridgeSessionThreads indicates an expected call of ListAIBridgeSessionThreads.
|
||||
func (mr *MockStoreMockRecorder) ListAIBridgeSessionThreads(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAIBridgeSessionThreads", reflect.TypeOf((*MockStore)(nil).ListAIBridgeSessionThreads), ctx, arg)
|
||||
}
|
||||
|
||||
// ListAIBridgeSessions mocks base method.
|
||||
func (m *MockStore) ListAIBridgeSessions(ctx context.Context, arg database.ListAIBridgeSessionsParams) ([]database.ListAIBridgeSessionsRow, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -7395,21 +7052,6 @@ func (mr *MockStoreMockRecorder) ListAIBridgeUserPromptsByInterceptionIDs(ctx, i
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAIBridgeUserPromptsByInterceptionIDs", reflect.TypeOf((*MockStore)(nil).ListAIBridgeUserPromptsByInterceptionIDs), ctx, interceptionIds)
|
||||
}
|
||||
|
||||
// ListAuthorizedAIBridgeClients mocks base method.
|
||||
func (m *MockStore) ListAuthorizedAIBridgeClients(ctx context.Context, arg database.ListAIBridgeClientsParams, prepared rbac.PreparedAuthorized) ([]string, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ListAuthorizedAIBridgeClients", ctx, arg, prepared)
|
||||
ret0, _ := ret[0].([]string)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ListAuthorizedAIBridgeClients indicates an expected call of ListAuthorizedAIBridgeClients.
|
||||
func (mr *MockStoreMockRecorder) ListAuthorizedAIBridgeClients(ctx, arg, prepared any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAuthorizedAIBridgeClients", reflect.TypeOf((*MockStore)(nil).ListAuthorizedAIBridgeClients), ctx, arg, prepared)
|
||||
}
|
||||
|
||||
// ListAuthorizedAIBridgeInterceptions mocks base method.
|
||||
func (m *MockStore) ListAuthorizedAIBridgeInterceptions(ctx context.Context, arg database.ListAIBridgeInterceptionsParams, prepared rbac.PreparedAuthorized) ([]database.ListAIBridgeInterceptionsRow, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -7440,21 +7082,6 @@ func (mr *MockStoreMockRecorder) ListAuthorizedAIBridgeModels(ctx, arg, prepared
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAuthorizedAIBridgeModels", reflect.TypeOf((*MockStore)(nil).ListAuthorizedAIBridgeModels), ctx, arg, prepared)
|
||||
}
|
||||
|
||||
// ListAuthorizedAIBridgeSessionThreads mocks base method.
|
||||
func (m *MockStore) ListAuthorizedAIBridgeSessionThreads(ctx context.Context, arg database.ListAIBridgeSessionThreadsParams, prepared rbac.PreparedAuthorized) ([]database.ListAIBridgeSessionThreadsRow, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ListAuthorizedAIBridgeSessionThreads", ctx, arg, prepared)
|
||||
ret0, _ := ret[0].([]database.ListAIBridgeSessionThreadsRow)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// ListAuthorizedAIBridgeSessionThreads indicates an expected call of ListAuthorizedAIBridgeSessionThreads.
|
||||
func (mr *MockStoreMockRecorder) ListAuthorizedAIBridgeSessionThreads(ctx, arg, prepared any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAuthorizedAIBridgeSessionThreads", reflect.TypeOf((*MockStore)(nil).ListAuthorizedAIBridgeSessionThreads), ctx, arg, prepared)
|
||||
}
|
||||
|
||||
// ListAuthorizedAIBridgeSessions mocks base method.
|
||||
func (m *MockStore) ListAuthorizedAIBridgeSessions(ctx context.Context, arg database.ListAIBridgeSessionsParams, prepared rbac.PreparedAuthorized) ([]database.ListAIBridgeSessionsRow, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -7679,20 +7306,6 @@ func (mr *MockStoreMockRecorder) PaginatedOrganizationMembers(ctx, arg any) *gom
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PaginatedOrganizationMembers", reflect.TypeOf((*MockStore)(nil).PaginatedOrganizationMembers), ctx, arg)
|
||||
}
|
||||
|
||||
// PinChatByID mocks base method.
|
||||
func (m *MockStore) PinChatByID(ctx context.Context, id uuid.UUID) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "PinChatByID", ctx, id)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// PinChatByID indicates an expected call of PinChatByID.
|
||||
func (mr *MockStoreMockRecorder) PinChatByID(ctx, id any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PinChatByID", reflect.TypeOf((*MockStore)(nil).PinChatByID), ctx, id)
|
||||
}
|
||||
|
||||
// Ping mocks base method.
|
||||
func (m *MockStore) Ping(ctx context.Context) (time.Duration, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -7723,21 +7336,6 @@ func (mr *MockStoreMockRecorder) PopNextQueuedMessage(ctx, chatID any) *gomock.C
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PopNextQueuedMessage", reflect.TypeOf((*MockStore)(nil).PopNextQueuedMessage), ctx, chatID)
|
||||
}
|
||||
|
||||
// PurgeOldChatAutomationEvents mocks base method.
|
||||
func (m *MockStore) PurgeOldChatAutomationEvents(ctx context.Context, arg database.PurgeOldChatAutomationEventsParams) (int64, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "PurgeOldChatAutomationEvents", ctx, arg)
|
||||
ret0, _ := ret[0].(int64)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// PurgeOldChatAutomationEvents indicates an expected call of PurgeOldChatAutomationEvents.
|
||||
func (mr *MockStoreMockRecorder) PurgeOldChatAutomationEvents(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PurgeOldChatAutomationEvents", reflect.TypeOf((*MockStore)(nil).PurgeOldChatAutomationEvents), ctx, arg)
|
||||
}
|
||||
|
||||
// ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate mocks base method.
|
||||
func (m *MockStore) ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate(ctx context.Context, templateID uuid.UUID) error {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -7870,12 +7468,11 @@ func (mr *MockStoreMockRecorder) TryAcquireLock(ctx, pgTryAdvisoryXactLock any)
|
||||
}
|
||||
|
||||
// UnarchiveChatByID mocks base method.
|
||||
func (m *MockStore) UnarchiveChatByID(ctx context.Context, id uuid.UUID) ([]database.Chat, error) {
|
||||
func (m *MockStore) UnarchiveChatByID(ctx context.Context, id uuid.UUID) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UnarchiveChatByID", ctx, id)
|
||||
ret0, _ := ret[0].([]database.Chat)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// UnarchiveChatByID indicates an expected call of UnarchiveChatByID.
|
||||
@@ -7912,20 +7509,6 @@ func (mr *MockStoreMockRecorder) UnfavoriteWorkspace(ctx, id any) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnfavoriteWorkspace", reflect.TypeOf((*MockStore)(nil).UnfavoriteWorkspace), ctx, id)
|
||||
}
|
||||
|
||||
// UnpinChatByID mocks base method.
|
||||
func (m *MockStore) UnpinChatByID(ctx context.Context, id uuid.UUID) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UnpinChatByID", ctx, id)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// UnpinChatByID indicates an expected call of UnpinChatByID.
|
||||
func (mr *MockStoreMockRecorder) UnpinChatByID(ctx, id any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnpinChatByID", reflect.TypeOf((*MockStore)(nil).UnpinChatByID), ctx, id)
|
||||
}
|
||||
|
||||
// UnsetDefaultChatModelConfigs mocks base method.
|
||||
func (m *MockStore) UnsetDefaultChatModelConfigs(ctx context.Context) error {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -7969,80 +7552,6 @@ func (mr *MockStoreMockRecorder) UpdateAPIKeyByID(ctx, arg any) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAPIKeyByID", reflect.TypeOf((*MockStore)(nil).UpdateAPIKeyByID), ctx, arg)
|
||||
}
|
||||
|
||||
// UpdateChatAutomation mocks base method.
|
||||
func (m *MockStore) UpdateChatAutomation(ctx context.Context, arg database.UpdateChatAutomationParams) (database.ChatAutomation, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateChatAutomation", ctx, arg)
|
||||
ret0, _ := ret[0].(database.ChatAutomation)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// UpdateChatAutomation indicates an expected call of UpdateChatAutomation.
|
||||
func (mr *MockStoreMockRecorder) UpdateChatAutomation(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateChatAutomation", reflect.TypeOf((*MockStore)(nil).UpdateChatAutomation), ctx, arg)
|
||||
}
|
||||
|
||||
// UpdateChatAutomationTrigger mocks base method.
|
||||
func (m *MockStore) UpdateChatAutomationTrigger(ctx context.Context, arg database.UpdateChatAutomationTriggerParams) (database.ChatAutomationTrigger, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateChatAutomationTrigger", ctx, arg)
|
||||
ret0, _ := ret[0].(database.ChatAutomationTrigger)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// UpdateChatAutomationTrigger indicates an expected call of UpdateChatAutomationTrigger.
|
||||
func (mr *MockStoreMockRecorder) UpdateChatAutomationTrigger(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateChatAutomationTrigger", reflect.TypeOf((*MockStore)(nil).UpdateChatAutomationTrigger), ctx, arg)
|
||||
}
|
||||
|
||||
// UpdateChatAutomationTriggerLastTriggeredAt mocks base method.
|
||||
func (m *MockStore) UpdateChatAutomationTriggerLastTriggeredAt(ctx context.Context, arg database.UpdateChatAutomationTriggerLastTriggeredAtParams) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateChatAutomationTriggerLastTriggeredAt", ctx, arg)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// UpdateChatAutomationTriggerLastTriggeredAt indicates an expected call of UpdateChatAutomationTriggerLastTriggeredAt.
|
||||
func (mr *MockStoreMockRecorder) UpdateChatAutomationTriggerLastTriggeredAt(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateChatAutomationTriggerLastTriggeredAt", reflect.TypeOf((*MockStore)(nil).UpdateChatAutomationTriggerLastTriggeredAt), ctx, arg)
|
||||
}
|
||||
|
||||
// UpdateChatAutomationTriggerWebhookSecret mocks base method.
|
||||
func (m *MockStore) UpdateChatAutomationTriggerWebhookSecret(ctx context.Context, arg database.UpdateChatAutomationTriggerWebhookSecretParams) (database.ChatAutomationTrigger, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateChatAutomationTriggerWebhookSecret", ctx, arg)
|
||||
ret0, _ := ret[0].(database.ChatAutomationTrigger)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// UpdateChatAutomationTriggerWebhookSecret indicates an expected call of UpdateChatAutomationTriggerWebhookSecret.
|
||||
func (mr *MockStoreMockRecorder) UpdateChatAutomationTriggerWebhookSecret(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateChatAutomationTriggerWebhookSecret", reflect.TypeOf((*MockStore)(nil).UpdateChatAutomationTriggerWebhookSecret), ctx, arg)
|
||||
}
|
||||
|
||||
// UpdateChatBuildAgentBinding mocks base method.
|
||||
func (m *MockStore) UpdateChatBuildAgentBinding(ctx context.Context, arg database.UpdateChatBuildAgentBindingParams) (database.Chat, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateChatBuildAgentBinding", ctx, arg)
|
||||
ret0, _ := ret[0].(database.Chat)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// UpdateChatBuildAgentBinding indicates an expected call of UpdateChatBuildAgentBinding.
|
||||
func (mr *MockStoreMockRecorder) UpdateChatBuildAgentBinding(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateChatBuildAgentBinding", reflect.TypeOf((*MockStore)(nil).UpdateChatBuildAgentBinding), ctx, arg)
|
||||
}
|
||||
|
||||
// UpdateChatByID mocks base method.
|
||||
func (m *MockStore) UpdateChatByID(ctx context.Context, arg database.UpdateChatByIDParams) (database.Chat, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -8088,50 +7597,6 @@ func (mr *MockStoreMockRecorder) UpdateChatLabelsByID(ctx, arg any) *gomock.Call
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateChatLabelsByID", reflect.TypeOf((*MockStore)(nil).UpdateChatLabelsByID), ctx, arg)
|
||||
}
|
||||
|
||||
// UpdateChatLastInjectedContext mocks base method.
|
||||
func (m *MockStore) UpdateChatLastInjectedContext(ctx context.Context, arg database.UpdateChatLastInjectedContextParams) (database.Chat, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateChatLastInjectedContext", ctx, arg)
|
||||
ret0, _ := ret[0].(database.Chat)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// UpdateChatLastInjectedContext indicates an expected call of UpdateChatLastInjectedContext.
|
||||
func (mr *MockStoreMockRecorder) UpdateChatLastInjectedContext(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateChatLastInjectedContext", reflect.TypeOf((*MockStore)(nil).UpdateChatLastInjectedContext), ctx, arg)
|
||||
}
|
||||
|
||||
// UpdateChatLastModelConfigByID mocks base method.
|
||||
func (m *MockStore) UpdateChatLastModelConfigByID(ctx context.Context, arg database.UpdateChatLastModelConfigByIDParams) (database.Chat, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateChatLastModelConfigByID", ctx, arg)
|
||||
ret0, _ := ret[0].(database.Chat)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// UpdateChatLastModelConfigByID indicates an expected call of UpdateChatLastModelConfigByID.
|
||||
func (mr *MockStoreMockRecorder) UpdateChatLastModelConfigByID(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateChatLastModelConfigByID", reflect.TypeOf((*MockStore)(nil).UpdateChatLastModelConfigByID), ctx, arg)
|
||||
}
|
||||
|
||||
// UpdateChatLastReadMessageID mocks base method.
|
||||
func (m *MockStore) UpdateChatLastReadMessageID(ctx context.Context, arg database.UpdateChatLastReadMessageIDParams) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateChatLastReadMessageID", ctx, arg)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// UpdateChatLastReadMessageID indicates an expected call of UpdateChatLastReadMessageID.
|
||||
func (mr *MockStoreMockRecorder) UpdateChatLastReadMessageID(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateChatLastReadMessageID", reflect.TypeOf((*MockStore)(nil).UpdateChatLastReadMessageID), ctx, arg)
|
||||
}
|
||||
|
||||
// UpdateChatMCPServerIDs mocks base method.
|
||||
func (m *MockStore) UpdateChatMCPServerIDs(ctx context.Context, arg database.UpdateChatMCPServerIDsParams) (database.Chat, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -8177,20 +7642,6 @@ func (mr *MockStoreMockRecorder) UpdateChatModelConfig(ctx, arg any) *gomock.Cal
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateChatModelConfig", reflect.TypeOf((*MockStore)(nil).UpdateChatModelConfig), ctx, arg)
|
||||
}
|
||||
|
||||
// UpdateChatPinOrder mocks base method.
|
||||
func (m *MockStore) UpdateChatPinOrder(ctx context.Context, arg database.UpdateChatPinOrderParams) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateChatPinOrder", ctx, arg)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// UpdateChatPinOrder indicates an expected call of UpdateChatPinOrder.
|
||||
func (mr *MockStoreMockRecorder) UpdateChatPinOrder(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateChatPinOrder", reflect.TypeOf((*MockStore)(nil).UpdateChatPinOrder), ctx, arg)
|
||||
}
|
||||
|
||||
// UpdateChatProvider mocks base method.
|
||||
func (m *MockStore) UpdateChatProvider(ctx context.Context, arg database.UpdateChatProviderParams) (database.ChatProvider, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -8221,34 +7672,19 @@ func (mr *MockStoreMockRecorder) UpdateChatStatus(ctx, arg any) *gomock.Call {
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateChatStatus", reflect.TypeOf((*MockStore)(nil).UpdateChatStatus), ctx, arg)
|
||||
}
|
||||
|
||||
// UpdateChatStatusPreserveUpdatedAt mocks base method.
|
||||
func (m *MockStore) UpdateChatStatusPreserveUpdatedAt(ctx context.Context, arg database.UpdateChatStatusPreserveUpdatedAtParams) (database.Chat, error) {
|
||||
// UpdateChatWorkspace mocks base method.
|
||||
func (m *MockStore) UpdateChatWorkspace(ctx context.Context, arg database.UpdateChatWorkspaceParams) (database.Chat, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateChatStatusPreserveUpdatedAt", ctx, arg)
|
||||
ret := m.ctrl.Call(m, "UpdateChatWorkspace", ctx, arg)
|
||||
ret0, _ := ret[0].(database.Chat)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// UpdateChatStatusPreserveUpdatedAt indicates an expected call of UpdateChatStatusPreserveUpdatedAt.
|
||||
func (mr *MockStoreMockRecorder) UpdateChatStatusPreserveUpdatedAt(ctx, arg any) *gomock.Call {
|
||||
// UpdateChatWorkspace indicates an expected call of UpdateChatWorkspace.
|
||||
func (mr *MockStoreMockRecorder) UpdateChatWorkspace(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateChatStatusPreserveUpdatedAt", reflect.TypeOf((*MockStore)(nil).UpdateChatStatusPreserveUpdatedAt), ctx, arg)
|
||||
}
|
||||
|
||||
// UpdateChatWorkspaceBinding mocks base method.
|
||||
func (m *MockStore) UpdateChatWorkspaceBinding(ctx context.Context, arg database.UpdateChatWorkspaceBindingParams) (database.Chat, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpdateChatWorkspaceBinding", ctx, arg)
|
||||
ret0, _ := ret[0].(database.Chat)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// UpdateChatWorkspaceBinding indicates an expected call of UpdateChatWorkspaceBinding.
|
||||
func (mr *MockStoreMockRecorder) UpdateChatWorkspaceBinding(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateChatWorkspaceBinding", reflect.TypeOf((*MockStore)(nil).UpdateChatWorkspaceBinding), ctx, arg)
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateChatWorkspace", reflect.TypeOf((*MockStore)(nil).UpdateChatWorkspace), ctx, arg)
|
||||
}
|
||||
|
||||
// UpdateCryptoKeyDeletesAt mocks base method.
|
||||
@@ -9593,20 +9029,6 @@ func (mr *MockStoreMockRecorder) UpsertChatDiffStatusReference(ctx, arg any) *go
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertChatDiffStatusReference", reflect.TypeOf((*MockStore)(nil).UpsertChatDiffStatusReference), ctx, arg)
|
||||
}
|
||||
|
||||
// UpsertChatIncludeDefaultSystemPrompt mocks base method.
|
||||
func (m *MockStore) UpsertChatIncludeDefaultSystemPrompt(ctx context.Context, includeDefaultSystemPrompt bool) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpsertChatIncludeDefaultSystemPrompt", ctx, includeDefaultSystemPrompt)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// UpsertChatIncludeDefaultSystemPrompt indicates an expected call of UpsertChatIncludeDefaultSystemPrompt.
|
||||
func (mr *MockStoreMockRecorder) UpsertChatIncludeDefaultSystemPrompt(ctx, includeDefaultSystemPrompt any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertChatIncludeDefaultSystemPrompt", reflect.TypeOf((*MockStore)(nil).UpsertChatIncludeDefaultSystemPrompt), ctx, includeDefaultSystemPrompt)
|
||||
}
|
||||
|
||||
// UpsertChatSystemPrompt mocks base method.
|
||||
func (m *MockStore) UpsertChatSystemPrompt(ctx context.Context, value string) error {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
Generated
+2
-199
@@ -220,12 +220,7 @@ CREATE TYPE api_key_scope AS ENUM (
|
||||
'chat:read',
|
||||
'chat:update',
|
||||
'chat:delete',
|
||||
'chat:*',
|
||||
'chat_automation:create',
|
||||
'chat_automation:read',
|
||||
'chat_automation:update',
|
||||
'chat_automation:delete',
|
||||
'chat_automation:*'
|
||||
'chat:*'
|
||||
);
|
||||
|
||||
CREATE TYPE app_sharing_level AS ENUM (
|
||||
@@ -275,32 +270,6 @@ CREATE TYPE build_reason AS ENUM (
|
||||
'task_resume'
|
||||
);
|
||||
|
||||
CREATE TYPE chat_automation_event_status AS ENUM (
|
||||
'filtered',
|
||||
'preview',
|
||||
'created',
|
||||
'continued',
|
||||
'rate_limited',
|
||||
'error'
|
||||
);
|
||||
|
||||
COMMENT ON TYPE chat_automation_event_status IS 'Outcome of a chat automation event: filtered, preview, created, continued, rate_limited, or error.';
|
||||
|
||||
CREATE TYPE chat_automation_status AS ENUM (
|
||||
'disabled',
|
||||
'preview',
|
||||
'active'
|
||||
);
|
||||
|
||||
COMMENT ON TYPE chat_automation_status IS 'Lifecycle state of a chat automation: disabled, preview, or active.';
|
||||
|
||||
CREATE TYPE chat_automation_trigger_type AS ENUM (
|
||||
'webhook',
|
||||
'cron'
|
||||
);
|
||||
|
||||
COMMENT ON TYPE chat_automation_trigger_type IS 'Discriminator for chat automation triggers: webhook or cron.';
|
||||
|
||||
CREATE TYPE chat_message_role AS ENUM (
|
||||
'system',
|
||||
'user',
|
||||
@@ -1269,104 +1238,6 @@ COMMENT ON COLUMN boundary_usage_stats.window_start IS 'Start of the time window
|
||||
|
||||
COMMENT ON COLUMN boundary_usage_stats.updated_at IS 'Timestamp of the last update to this row.';
|
||||
|
||||
CREATE TABLE chat_automation_events (
|
||||
id uuid NOT NULL,
|
||||
automation_id uuid NOT NULL,
|
||||
trigger_id uuid,
|
||||
received_at timestamp with time zone NOT NULL,
|
||||
payload jsonb NOT NULL,
|
||||
filter_matched boolean NOT NULL,
|
||||
resolved_labels jsonb,
|
||||
matched_chat_id uuid,
|
||||
created_chat_id uuid,
|
||||
status chat_automation_event_status NOT NULL,
|
||||
error text,
|
||||
CONSTRAINT chat_automation_events_chat_exclusivity CHECK (((matched_chat_id IS NULL) OR (created_chat_id IS NULL)))
|
||||
);
|
||||
|
||||
COMMENT ON TABLE chat_automation_events IS 'Every trigger invocation produces an event row regardless of outcome. This table is the audit trail and the data source for rate-limit window counts. Rows are append-only and expected to be purged by a background job after a retention period.';
|
||||
|
||||
COMMENT ON COLUMN chat_automation_events.payload IS 'The raw payload that was evaluated. For webhooks this is the HTTP body; for cron triggers it is a synthetic JSON envelope with schedule metadata.';
|
||||
|
||||
COMMENT ON COLUMN chat_automation_events.filter_matched IS 'Whether the trigger filter conditions matched. False means the event was dropped before any chat interaction.';
|
||||
|
||||
COMMENT ON COLUMN chat_automation_events.resolved_labels IS 'Labels resolved from the payload via label_paths. Stored so the event log shows exactly which labels were computed.';
|
||||
|
||||
COMMENT ON COLUMN chat_automation_events.matched_chat_id IS 'ID of an existing chat that was found via label matching and continued with a new message.';
|
||||
|
||||
COMMENT ON COLUMN chat_automation_events.created_chat_id IS 'ID of a newly created chat (mutually exclusive with matched_chat_id in practice).';
|
||||
|
||||
COMMENT ON COLUMN chat_automation_events.status IS 'Outcome of the event: filtered — filter did not match; preview — automation is in preview mode; created — new chat was created; continued — existing chat was continued; rate_limited — rate limit prevented chat action; error — something went wrong.';
|
||||
|
||||
CREATE TABLE chat_automation_triggers (
|
||||
id uuid NOT NULL,
|
||||
automation_id uuid NOT NULL,
|
||||
type chat_automation_trigger_type NOT NULL,
|
||||
webhook_secret text,
|
||||
webhook_secret_key_id text,
|
||||
cron_schedule text,
|
||||
last_triggered_at timestamp with time zone,
|
||||
filter jsonb,
|
||||
label_paths jsonb,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone NOT NULL,
|
||||
CONSTRAINT chat_automation_triggers_cron_fields CHECK (((type <> 'cron'::chat_automation_trigger_type) OR ((cron_schedule IS NOT NULL) AND (webhook_secret IS NULL) AND (webhook_secret_key_id IS NULL)))),
|
||||
CONSTRAINT chat_automation_triggers_webhook_fields CHECK (((type <> 'webhook'::chat_automation_trigger_type) OR ((webhook_secret IS NOT NULL) AND (cron_schedule IS NULL) AND (last_triggered_at IS NULL))))
|
||||
);
|
||||
|
||||
COMMENT ON TABLE chat_automation_triggers IS 'Triggers define how an automation is invoked. Each automation can have multiple triggers (e.g. one webhook + one cron schedule). Webhook and cron triggers share the same row shape with type-specific nullable columns to keep the schema simple.';
|
||||
|
||||
COMMENT ON COLUMN chat_automation_triggers.type IS 'Discriminator: webhook or cron. Determines which nullable columns are meaningful.';
|
||||
|
||||
COMMENT ON COLUMN chat_automation_triggers.webhook_secret IS 'HMAC-SHA256 shared secret for webhook signature verification (X-Hub-Signature-256 header). NULL for cron triggers.';
|
||||
|
||||
COMMENT ON COLUMN chat_automation_triggers.cron_schedule IS 'Standard 5-field cron expression (minute hour dom month dow), with optional CRON_TZ= prefix. NULL for webhook triggers.';
|
||||
|
||||
COMMENT ON COLUMN chat_automation_triggers.last_triggered_at IS 'Timestamp of the last successful cron fire. The scheduler computes next = cron.Next(last_triggered_at) and fires when next <= now. NULL means the trigger has never fired. Not used for webhook triggers.';
|
||||
|
||||
COMMENT ON COLUMN chat_automation_triggers.filter IS 'gjson path-to-value filter conditions evaluated against the incoming webhook payload. All conditions must match for the trigger to fire. NULL or empty means match everything.';
|
||||
|
||||
COMMENT ON COLUMN chat_automation_triggers.label_paths IS 'Maps chat label keys to gjson paths. When a trigger fires, labels are resolved from the payload and used to find an existing chat to continue (by label match) or set on a newly created chat.';
|
||||
|
||||
CREATE TABLE chat_automations (
|
||||
id uuid NOT NULL,
|
||||
owner_id uuid NOT NULL,
|
||||
organization_id uuid NOT NULL,
|
||||
name text NOT NULL,
|
||||
description text DEFAULT ''::text NOT NULL,
|
||||
instructions text DEFAULT ''::text NOT NULL,
|
||||
model_config_id uuid,
|
||||
mcp_server_ids uuid[] DEFAULT '{}'::uuid[] NOT NULL,
|
||||
allowed_tools text[] DEFAULT '{}'::text[] NOT NULL,
|
||||
status chat_automation_status DEFAULT 'disabled'::chat_automation_status NOT NULL,
|
||||
max_chat_creates_per_hour integer DEFAULT 10 NOT NULL,
|
||||
max_messages_per_hour integer DEFAULT 60 NOT NULL,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone NOT NULL,
|
||||
CONSTRAINT chat_automations_max_chat_creates_per_hour_check CHECK ((max_chat_creates_per_hour > 0)),
|
||||
CONSTRAINT chat_automations_max_messages_per_hour_check CHECK ((max_messages_per_hour > 0))
|
||||
);
|
||||
|
||||
COMMENT ON TABLE chat_automations IS 'Chat automations bridge external events (webhooks, cron schedules) to Coder chats. A chat automation defines what to say, which model and tools to use, and how fast it is allowed to create or continue chats.';
|
||||
|
||||
COMMENT ON COLUMN chat_automations.owner_id IS 'The user on whose behalf chats are created. All RBAC checks and chat ownership are scoped to this user.';
|
||||
|
||||
COMMENT ON COLUMN chat_automations.organization_id IS 'Organization scope for RBAC. Combined with owner_id and name to form a unique constraint so automations are namespaced per user per org.';
|
||||
|
||||
COMMENT ON COLUMN chat_automations.instructions IS 'The user-role message injected into every chat this automation creates. This is the core prompt that tells the LLM what to do.';
|
||||
|
||||
COMMENT ON COLUMN chat_automations.model_config_id IS 'Optional model configuration override. When NULL the deployment default is used. SET NULL on delete so automations survive config changes gracefully.';
|
||||
|
||||
COMMENT ON COLUMN chat_automations.mcp_server_ids IS 'MCP servers to attach to chats created by this automation. Stored as an array of UUIDs rather than a join table because the set is small and always read/written atomically.';
|
||||
|
||||
COMMENT ON COLUMN chat_automations.allowed_tools IS 'Tool allowlist. Empty means all tools available to the model config are permitted.';
|
||||
|
||||
COMMENT ON COLUMN chat_automations.status IS 'Lifecycle state: disabled — trigger events are silently dropped; preview — events are logged but no chat is created (dry-run); active — events create or continue chats.';
|
||||
|
||||
COMMENT ON COLUMN chat_automations.max_chat_creates_per_hour IS 'Maximum number of new chats this automation may create in a rolling one-hour window. Prevents runaway webhook storms from flooding the system.';
|
||||
|
||||
COMMENT ON COLUMN chat_automations.max_messages_per_hour IS 'Maximum total messages (creates + continues) this automation may send in a rolling one-hour window. A second, broader throttle that catches high-frequency continuation patterns.';
|
||||
|
||||
CREATE TABLE chat_diff_statuses (
|
||||
chat_id uuid NOT NULL,
|
||||
url text,
|
||||
@@ -1528,13 +1399,7 @@ CREATE TABLE chats (
|
||||
last_error text,
|
||||
mode chat_mode,
|
||||
mcp_server_ids uuid[] DEFAULT '{}'::uuid[] NOT NULL,
|
||||
labels jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
build_id uuid,
|
||||
agent_id uuid,
|
||||
pin_order integer DEFAULT 0 NOT NULL,
|
||||
last_read_message_id bigint,
|
||||
last_injected_context jsonb,
|
||||
automation_id uuid
|
||||
labels jsonb DEFAULT '{}'::jsonb NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE connection_logs (
|
||||
@@ -1838,7 +1703,6 @@ CREATE TABLE mcp_server_configs (
|
||||
updated_by uuid,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
model_intent boolean DEFAULT false NOT NULL,
|
||||
CONSTRAINT mcp_server_configs_auth_type_check CHECK ((auth_type = ANY (ARRAY['none'::text, 'oauth2'::text, 'api_key'::text, 'custom_headers'::text]))),
|
||||
CONSTRAINT mcp_server_configs_availability_check CHECK ((availability = ANY (ARRAY['force_on'::text, 'default_on'::text, 'default_off'::text]))),
|
||||
CONSTRAINT mcp_server_configs_transport_check CHECK ((transport = ANY (ARRAY['streamable_http'::text, 'sse'::text])))
|
||||
@@ -3450,15 +3314,6 @@ ALTER TABLE ONLY audit_logs
|
||||
ALTER TABLE ONLY boundary_usage_stats
|
||||
ADD CONSTRAINT boundary_usage_stats_pkey PRIMARY KEY (replica_id);
|
||||
|
||||
ALTER TABLE ONLY chat_automation_events
|
||||
ADD CONSTRAINT chat_automation_events_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY chat_automation_triggers
|
||||
ADD CONSTRAINT chat_automation_triggers_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY chat_automations
|
||||
ADD CONSTRAINT chat_automations_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY chat_diff_statuses
|
||||
ADD CONSTRAINT chat_diff_statuses_pkey PRIMARY KEY (chat_id);
|
||||
|
||||
@@ -3844,20 +3699,6 @@ CREATE INDEX idx_audit_log_user_id ON audit_logs USING btree (user_id);
|
||||
|
||||
CREATE INDEX idx_audit_logs_time_desc ON audit_logs USING btree ("time" DESC);
|
||||
|
||||
CREATE INDEX idx_chat_automation_events_automation_id_received_at ON chat_automation_events USING btree (automation_id, received_at DESC);
|
||||
|
||||
CREATE INDEX idx_chat_automation_events_rate_limit ON chat_automation_events USING btree (automation_id, received_at) WHERE (status = ANY (ARRAY['created'::chat_automation_event_status, 'continued'::chat_automation_event_status]));
|
||||
|
||||
CREATE INDEX idx_chat_automation_events_received_at ON chat_automation_events USING btree (received_at);
|
||||
|
||||
CREATE INDEX idx_chat_automation_triggers_automation_id ON chat_automation_triggers USING btree (automation_id);
|
||||
|
||||
CREATE INDEX idx_chat_automations_organization_id ON chat_automations USING btree (organization_id);
|
||||
|
||||
CREATE INDEX idx_chat_automations_owner_id ON chat_automations USING btree (owner_id);
|
||||
|
||||
CREATE UNIQUE INDEX idx_chat_automations_owner_org_name ON chat_automations USING btree (owner_id, organization_id, name);
|
||||
|
||||
CREATE INDEX idx_chat_diff_statuses_stale_at ON chat_diff_statuses USING btree (stale_at);
|
||||
|
||||
CREATE INDEX idx_chat_files_org ON chat_files USING btree (organization_id);
|
||||
@@ -3886,8 +3727,6 @@ CREATE INDEX idx_chat_providers_enabled ON chat_providers USING btree (enabled);
|
||||
|
||||
CREATE INDEX idx_chat_queued_messages_chat_id ON chat_queued_messages USING btree (chat_id);
|
||||
|
||||
CREATE INDEX idx_chats_automation_id ON chats USING btree (automation_id);
|
||||
|
||||
CREATE INDEX idx_chats_labels ON chats USING gin (labels);
|
||||
|
||||
CREATE INDEX idx_chats_last_model_config_id ON chats USING btree (last_model_config_id);
|
||||
@@ -4161,33 +4000,6 @@ ALTER TABLE ONLY aibridge_interceptions
|
||||
ALTER TABLE ONLY api_keys
|
||||
ADD CONSTRAINT api_keys_user_id_uuid_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY chat_automation_events
|
||||
ADD CONSTRAINT chat_automation_events_automation_id_fkey FOREIGN KEY (automation_id) REFERENCES chat_automations(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY chat_automation_events
|
||||
ADD CONSTRAINT chat_automation_events_created_chat_id_fkey FOREIGN KEY (created_chat_id) REFERENCES chats(id) ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE ONLY chat_automation_events
|
||||
ADD CONSTRAINT chat_automation_events_matched_chat_id_fkey FOREIGN KEY (matched_chat_id) REFERENCES chats(id) ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE ONLY chat_automation_events
|
||||
ADD CONSTRAINT chat_automation_events_trigger_id_fkey FOREIGN KEY (trigger_id) REFERENCES chat_automation_triggers(id) ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE ONLY chat_automation_triggers
|
||||
ADD CONSTRAINT chat_automation_triggers_automation_id_fkey FOREIGN KEY (automation_id) REFERENCES chat_automations(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY chat_automation_triggers
|
||||
ADD CONSTRAINT chat_automation_triggers_webhook_secret_key_id_fkey FOREIGN KEY (webhook_secret_key_id) REFERENCES dbcrypt_keys(active_key_digest);
|
||||
|
||||
ALTER TABLE ONLY chat_automations
|
||||
ADD CONSTRAINT chat_automations_model_config_id_fkey FOREIGN KEY (model_config_id) REFERENCES chat_model_configs(id) ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE ONLY chat_automations
|
||||
ADD CONSTRAINT chat_automations_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY chat_automations
|
||||
ADD CONSTRAINT chat_automations_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY chat_diff_statuses
|
||||
ADD CONSTRAINT chat_diff_statuses_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE;
|
||||
|
||||
@@ -4221,15 +4033,6 @@ ALTER TABLE ONLY chat_providers
|
||||
ALTER TABLE ONLY chat_queued_messages
|
||||
ADD CONSTRAINT chat_queued_messages_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY chats
|
||||
ADD CONSTRAINT chats_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE ONLY chats
|
||||
ADD CONSTRAINT chats_automation_id_fkey FOREIGN KEY (automation_id) REFERENCES chat_automations(id) ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE ONLY chats
|
||||
ADD CONSTRAINT chats_build_id_fkey FOREIGN KEY (build_id) REFERENCES workspace_builds(id) ON DELETE SET NULL;
|
||||
|
||||
ALTER TABLE ONLY chats
|
||||
ADD CONSTRAINT chats_last_model_config_id_fkey FOREIGN KEY (last_model_config_id) REFERENCES chat_model_configs(id);
|
||||
|
||||
|
||||
@@ -9,15 +9,6 @@ const (
|
||||
ForeignKeyAiSeatStateUserID ForeignKeyConstraint = "ai_seat_state_user_id_fkey" // ALTER TABLE ONLY ai_seat_state ADD CONSTRAINT ai_seat_state_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
ForeignKeyAibridgeInterceptionsInitiatorID ForeignKeyConstraint = "aibridge_interceptions_initiator_id_fkey" // ALTER TABLE ONLY aibridge_interceptions ADD CONSTRAINT aibridge_interceptions_initiator_id_fkey FOREIGN KEY (initiator_id) REFERENCES users(id);
|
||||
ForeignKeyAPIKeysUserIDUUID ForeignKeyConstraint = "api_keys_user_id_uuid_fkey" // ALTER TABLE ONLY api_keys ADD CONSTRAINT api_keys_user_id_uuid_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
ForeignKeyChatAutomationEventsAutomationID ForeignKeyConstraint = "chat_automation_events_automation_id_fkey" // ALTER TABLE ONLY chat_automation_events ADD CONSTRAINT chat_automation_events_automation_id_fkey FOREIGN KEY (automation_id) REFERENCES chat_automations(id) ON DELETE CASCADE;
|
||||
ForeignKeyChatAutomationEventsCreatedChatID ForeignKeyConstraint = "chat_automation_events_created_chat_id_fkey" // ALTER TABLE ONLY chat_automation_events ADD CONSTRAINT chat_automation_events_created_chat_id_fkey FOREIGN KEY (created_chat_id) REFERENCES chats(id) ON DELETE SET NULL;
|
||||
ForeignKeyChatAutomationEventsMatchedChatID ForeignKeyConstraint = "chat_automation_events_matched_chat_id_fkey" // ALTER TABLE ONLY chat_automation_events ADD CONSTRAINT chat_automation_events_matched_chat_id_fkey FOREIGN KEY (matched_chat_id) REFERENCES chats(id) ON DELETE SET NULL;
|
||||
ForeignKeyChatAutomationEventsTriggerID ForeignKeyConstraint = "chat_automation_events_trigger_id_fkey" // ALTER TABLE ONLY chat_automation_events ADD CONSTRAINT chat_automation_events_trigger_id_fkey FOREIGN KEY (trigger_id) REFERENCES chat_automation_triggers(id) ON DELETE SET NULL;
|
||||
ForeignKeyChatAutomationTriggersAutomationID ForeignKeyConstraint = "chat_automation_triggers_automation_id_fkey" // ALTER TABLE ONLY chat_automation_triggers ADD CONSTRAINT chat_automation_triggers_automation_id_fkey FOREIGN KEY (automation_id) REFERENCES chat_automations(id) ON DELETE CASCADE;
|
||||
ForeignKeyChatAutomationTriggersWebhookSecretKeyID ForeignKeyConstraint = "chat_automation_triggers_webhook_secret_key_id_fkey" // ALTER TABLE ONLY chat_automation_triggers ADD CONSTRAINT chat_automation_triggers_webhook_secret_key_id_fkey FOREIGN KEY (webhook_secret_key_id) REFERENCES dbcrypt_keys(active_key_digest);
|
||||
ForeignKeyChatAutomationsModelConfigID ForeignKeyConstraint = "chat_automations_model_config_id_fkey" // ALTER TABLE ONLY chat_automations ADD CONSTRAINT chat_automations_model_config_id_fkey FOREIGN KEY (model_config_id) REFERENCES chat_model_configs(id) ON DELETE SET NULL;
|
||||
ForeignKeyChatAutomationsOrganizationID ForeignKeyConstraint = "chat_automations_organization_id_fkey" // ALTER TABLE ONLY chat_automations ADD CONSTRAINT chat_automations_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE;
|
||||
ForeignKeyChatAutomationsOwnerID ForeignKeyConstraint = "chat_automations_owner_id_fkey" // ALTER TABLE ONLY chat_automations ADD CONSTRAINT chat_automations_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
ForeignKeyChatDiffStatusesChatID ForeignKeyConstraint = "chat_diff_statuses_chat_id_fkey" // ALTER TABLE ONLY chat_diff_statuses ADD CONSTRAINT chat_diff_statuses_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE;
|
||||
ForeignKeyChatFilesOrganizationID ForeignKeyConstraint = "chat_files_organization_id_fkey" // ALTER TABLE ONLY chat_files ADD CONSTRAINT chat_files_organization_id_fkey FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE;
|
||||
ForeignKeyChatFilesOwnerID ForeignKeyConstraint = "chat_files_owner_id_fkey" // ALTER TABLE ONLY chat_files ADD CONSTRAINT chat_files_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
@@ -29,9 +20,6 @@ const (
|
||||
ForeignKeyChatProvidersAPIKeyKeyID ForeignKeyConstraint = "chat_providers_api_key_key_id_fkey" // ALTER TABLE ONLY chat_providers ADD CONSTRAINT chat_providers_api_key_key_id_fkey FOREIGN KEY (api_key_key_id) REFERENCES dbcrypt_keys(active_key_digest);
|
||||
ForeignKeyChatProvidersCreatedBy ForeignKeyConstraint = "chat_providers_created_by_fkey" // ALTER TABLE ONLY chat_providers ADD CONSTRAINT chat_providers_created_by_fkey FOREIGN KEY (created_by) REFERENCES users(id);
|
||||
ForeignKeyChatQueuedMessagesChatID ForeignKeyConstraint = "chat_queued_messages_chat_id_fkey" // ALTER TABLE ONLY chat_queued_messages ADD CONSTRAINT chat_queued_messages_chat_id_fkey FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE;
|
||||
ForeignKeyChatsAgentID ForeignKeyConstraint = "chats_agent_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_agent_id_fkey FOREIGN KEY (agent_id) REFERENCES workspace_agents(id) ON DELETE SET NULL;
|
||||
ForeignKeyChatsAutomationID ForeignKeyConstraint = "chats_automation_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_automation_id_fkey FOREIGN KEY (automation_id) REFERENCES chat_automations(id) ON DELETE SET NULL;
|
||||
ForeignKeyChatsBuildID ForeignKeyConstraint = "chats_build_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_build_id_fkey FOREIGN KEY (build_id) REFERENCES workspace_builds(id) ON DELETE SET NULL;
|
||||
ForeignKeyChatsLastModelConfigID ForeignKeyConstraint = "chats_last_model_config_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_last_model_config_id_fkey FOREIGN KEY (last_model_config_id) REFERENCES chat_model_configs(id);
|
||||
ForeignKeyChatsOwnerID ForeignKeyConstraint = "chats_owner_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE;
|
||||
ForeignKeyChatsParentChatID ForeignKeyConstraint = "chats_parent_chat_id_fkey" // ALTER TABLE ONLY chats ADD CONSTRAINT chats_parent_chat_id_fkey FOREIGN KEY (parent_chat_id) REFERENCES chats(id) ON DELETE SET NULL;
|
||||
|
||||
@@ -27,7 +27,6 @@ func TestCustomQueriesSyncedRowScan(t *testing.T) {
|
||||
"GetWorkspaces": "GetAuthorizedWorkspaces",
|
||||
"GetUsers": "GetAuthorizedUsers",
|
||||
"GetChats": "GetAuthorizedChats",
|
||||
"GetChatAutomations": "GetAuthorizedChatAutomations",
|
||||
}
|
||||
|
||||
// Scan custom
|
||||
|
||||
@@ -15,9 +15,6 @@ const (
|
||||
LockIDReconcilePrebuilds
|
||||
LockIDReconcileSystemRoles
|
||||
LockIDBoundaryUsageStats
|
||||
// LockIDChatAutomationCron prevents concurrent cron trigger
|
||||
// evaluation across coderd replicas.
|
||||
LockIDChatAutomationCron
|
||||
)
|
||||
|
||||
// GenLockID generates a unique and consistent lock ID from a given string.
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
ALTER TABLE chats
|
||||
DROP COLUMN IF EXISTS build_id,
|
||||
DROP COLUMN IF EXISTS agent_id;
|
||||
@@ -1,3 +0,0 @@
|
||||
ALTER TABLE chats
|
||||
ADD COLUMN build_id UUID REFERENCES workspace_builds(id) ON DELETE SET NULL,
|
||||
ADD COLUMN agent_id UUID REFERENCES workspace_agents(id) ON DELETE SET NULL;
|
||||
@@ -1 +0,0 @@
|
||||
ALTER TABLE chats DROP COLUMN pin_order;
|
||||
@@ -1 +0,0 @@
|
||||
ALTER TABLE chats ADD COLUMN pin_order integer DEFAULT 0 NOT NULL;
|
||||
@@ -1 +0,0 @@
|
||||
ALTER TABLE mcp_server_configs DROP COLUMN model_intent;
|
||||
@@ -1,2 +0,0 @@
|
||||
ALTER TABLE mcp_server_configs
|
||||
ADD COLUMN model_intent BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -1 +0,0 @@
|
||||
ALTER TABLE chats DROP COLUMN last_read_message_id;
|
||||
@@ -1,9 +0,0 @@
|
||||
ALTER TABLE chats ADD COLUMN last_read_message_id BIGINT;
|
||||
|
||||
-- Backfill existing chats so they don't appear unread after deploy.
|
||||
-- The has_unread query uses COALESCE(last_read_message_id, 0), so
|
||||
-- leaving this NULL would mark every existing chat as unread.
|
||||
UPDATE chats SET last_read_message_id = (
|
||||
SELECT MAX(cm.id) FROM chat_messages cm
|
||||
WHERE cm.chat_id = chats.id AND cm.role = 'assistant' AND cm.deleted = false
|
||||
);
|
||||
@@ -1 +0,0 @@
|
||||
ALTER TABLE chats DROP COLUMN last_injected_context;
|
||||
@@ -1 +0,0 @@
|
||||
ALTER TABLE chats ADD COLUMN last_injected_context JSONB;
|
||||
@@ -1,4 +0,0 @@
|
||||
-- Remove 'agents-access' from all users who have it.
|
||||
UPDATE users
|
||||
SET rbac_roles = array_remove(rbac_roles, 'agents-access')
|
||||
WHERE 'agents-access' = ANY(rbac_roles);
|
||||
@@ -1,5 +0,0 @@
|
||||
-- Grant 'agents-access' to every user who has ever created a chat.
|
||||
UPDATE users
|
||||
SET rbac_roles = array_append(rbac_roles, 'agents-access')
|
||||
WHERE id IN (SELECT DISTINCT owner_id FROM chats)
|
||||
AND NOT ('agents-access' = ANY(rbac_roles));
|
||||
@@ -1,13 +0,0 @@
|
||||
ALTER TABLE chats DROP COLUMN IF EXISTS automation_id;
|
||||
|
||||
DROP TABLE IF EXISTS chat_automation_events;
|
||||
|
||||
DROP TABLE IF EXISTS chat_automation_triggers;
|
||||
|
||||
DROP TABLE IF EXISTS chat_automations;
|
||||
|
||||
DROP TYPE IF EXISTS chat_automation_event_status;
|
||||
|
||||
DROP TYPE IF EXISTS chat_automation_trigger_type;
|
||||
|
||||
DROP TYPE IF EXISTS chat_automation_status;
|
||||
@@ -1,238 +0,0 @@
|
||||
-- Chat automations bridge external events (webhooks, cron schedules) to
|
||||
-- Coder chats. A chat automation defines *what* to say, *which* model
|
||||
-- and tools to use, and *how fast* it is allowed to create or continue
|
||||
-- chats.
|
||||
|
||||
CREATE TYPE chat_automation_status AS ENUM ('disabled', 'preview', 'active');
|
||||
CREATE TYPE chat_automation_trigger_type AS ENUM ('webhook', 'cron');
|
||||
CREATE TYPE chat_automation_event_status AS ENUM ('filtered', 'preview', 'created', 'continued', 'rate_limited', 'error');
|
||||
|
||||
CREATE TABLE chat_automations (
|
||||
id uuid NOT NULL,
|
||||
-- The user on whose behalf chats are created. All RBAC checks and
|
||||
-- chat ownership are scoped to this user.
|
||||
owner_id uuid NOT NULL,
|
||||
-- Organization scope for RBAC. Combined with owner_id and name to
|
||||
-- form a unique constraint so automations are namespaced per user
|
||||
-- per org.
|
||||
organization_id uuid NOT NULL,
|
||||
-- Human-readable identifier. Unique within (owner_id, organization_id).
|
||||
name text NOT NULL,
|
||||
-- Optional long-form description shown in the UI.
|
||||
description text NOT NULL DEFAULT '',
|
||||
-- The user-role message injected into every chat this automation
|
||||
-- creates. This is the core prompt that tells the LLM what to do.
|
||||
instructions text NOT NULL DEFAULT '',
|
||||
-- Optional model configuration override. When NULL the deployment
|
||||
-- default is used. SET NULL on delete so automations survive config
|
||||
-- changes gracefully.
|
||||
model_config_id uuid,
|
||||
-- MCP servers to attach to chats created by this automation.
|
||||
-- Stored as an array of UUIDs rather than a join table because
|
||||
-- the set is small and always read/written atomically.
|
||||
mcp_server_ids uuid[] NOT NULL DEFAULT '{}',
|
||||
-- Tool allowlist. Empty means all tools available to the model
|
||||
-- config are permitted.
|
||||
allowed_tools text[] NOT NULL DEFAULT '{}',
|
||||
-- Lifecycle state:
|
||||
-- disabled — trigger events are silently dropped.
|
||||
-- preview — events are logged but no chat is created (dry-run).
|
||||
-- active — events create or continue chats.
|
||||
status chat_automation_status NOT NULL DEFAULT 'disabled',
|
||||
-- Maximum number of *new* chats this automation may create in a
|
||||
-- rolling one-hour window. Prevents runaway webhook storms from
|
||||
-- flooding the system. Approximate under concurrency; the
|
||||
-- check-then-insert is not serialized, so brief bursts may
|
||||
-- slightly exceed the cap.
|
||||
max_chat_creates_per_hour integer NOT NULL DEFAULT 10,
|
||||
-- Maximum total messages (creates + continues) this automation may
|
||||
-- send in a rolling one-hour window. A second, broader throttle
|
||||
-- that catches high-frequency continuation patterns. Same
|
||||
-- approximate-under-concurrency caveat as above.
|
||||
max_messages_per_hour integer NOT NULL DEFAULT 60,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (model_config_id) REFERENCES chat_model_configs(id) ON DELETE SET NULL,
|
||||
CONSTRAINT chat_automations_max_chat_creates_per_hour_check CHECK (max_chat_creates_per_hour > 0),
|
||||
CONSTRAINT chat_automations_max_messages_per_hour_check CHECK (max_messages_per_hour > 0)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_chat_automations_owner_id ON chat_automations (owner_id);
|
||||
CREATE INDEX idx_chat_automations_organization_id ON chat_automations (organization_id);
|
||||
|
||||
-- Enforces that automation names are unique per user per org so they
|
||||
-- can be referenced unambiguously in CLI/API calls.
|
||||
CREATE UNIQUE INDEX idx_chat_automations_owner_org_name ON chat_automations (owner_id, organization_id, name);
|
||||
|
||||
-- Triggers define *how* an automation is invoked. Each automation can
|
||||
-- have multiple triggers (e.g. one webhook + one cron schedule).
|
||||
-- Webhook and cron triggers share the same row shape with type-specific
|
||||
-- nullable columns to keep the schema simple.
|
||||
CREATE TABLE chat_automation_triggers (
|
||||
id uuid NOT NULL,
|
||||
-- Parent automation. CASCADE delete ensures orphan triggers are
|
||||
-- cleaned up when an automation is removed.
|
||||
automation_id uuid NOT NULL,
|
||||
-- Discriminator: 'webhook' or 'cron'. Determines which nullable
|
||||
-- columns are meaningful.
|
||||
type chat_automation_trigger_type NOT NULL,
|
||||
-- HMAC-SHA256 shared secret for webhook signature verification
|
||||
-- (X-Hub-Signature-256 header). NULL for cron triggers.
|
||||
webhook_secret text,
|
||||
-- Identifier of the dbcrypt key used to encrypt webhook_secret.
|
||||
-- NULL means the secret is not yet encrypted. When dbcrypt is
|
||||
-- enabled, this references the active key digest used for
|
||||
-- AES-256-GCM encryption.
|
||||
webhook_secret_key_id text REFERENCES dbcrypt_keys(active_key_digest),
|
||||
-- Standard 5-field cron expression (minute hour dom month dow),
|
||||
-- with optional CRON_TZ= prefix. NULL for webhook triggers.
|
||||
cron_schedule text,
|
||||
-- Timestamp of the last successful cron fire. The scheduler
|
||||
-- computes next = cron.Next(last_triggered_at) and fires when
|
||||
-- next <= now. NULL means the trigger has never fired; the
|
||||
-- scheduler falls back to created_at as the reference time.
|
||||
-- Not used for webhook triggers.
|
||||
last_triggered_at timestamp with time zone,
|
||||
-- gjson path→value filter conditions evaluated against the
|
||||
-- incoming webhook payload. All conditions must match for the
|
||||
-- trigger to fire. NULL or empty means "match everything".
|
||||
filter jsonb,
|
||||
-- Maps chat label keys to gjson paths. When a trigger fires,
|
||||
-- labels are resolved from the payload and used to find an
|
||||
-- existing chat to continue (by label match) or set on a
|
||||
-- newly created chat. This is how automations route events
|
||||
-- to the right conversation.
|
||||
label_paths jsonb,
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
FOREIGN KEY (automation_id) REFERENCES chat_automations(id) ON DELETE CASCADE,
|
||||
CONSTRAINT chat_automation_triggers_webhook_fields CHECK (
|
||||
type != 'webhook' OR (webhook_secret IS NOT NULL AND cron_schedule IS NULL AND last_triggered_at IS NULL)
|
||||
),
|
||||
CONSTRAINT chat_automation_triggers_cron_fields CHECK (
|
||||
type != 'cron' OR (cron_schedule IS NOT NULL AND webhook_secret IS NULL AND webhook_secret_key_id IS NULL)
|
||||
)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_chat_automation_triggers_automation_id ON chat_automation_triggers (automation_id);
|
||||
|
||||
-- Every trigger invocation produces an event row regardless of outcome.
|
||||
-- This table is the audit trail and the data source for rate-limit
|
||||
-- window counts. Rows are append-only and expected to be purged by a
|
||||
-- background job after a retention period.
|
||||
CREATE TABLE chat_automation_events (
|
||||
id uuid NOT NULL,
|
||||
-- The automation that owns this event.
|
||||
automation_id uuid NOT NULL,
|
||||
-- The trigger that produced this event. SET NULL on delete so
|
||||
-- historical events survive trigger removal.
|
||||
trigger_id uuid,
|
||||
-- When the event was received (webhook delivery time or cron
|
||||
-- evaluation time). Used for rate-limit window calculations and
|
||||
-- purge cutoffs.
|
||||
received_at timestamp with time zone NOT NULL,
|
||||
-- The raw payload that was evaluated. For webhooks this is the
|
||||
-- HTTP body; for cron triggers it is a synthetic JSON envelope
|
||||
-- with schedule metadata.
|
||||
payload jsonb NOT NULL,
|
||||
-- Whether the trigger's filter conditions matched. False means
|
||||
-- the event was dropped before any chat interaction.
|
||||
filter_matched boolean NOT NULL,
|
||||
-- Labels resolved from the payload via label_paths. Stored so
|
||||
-- the event log shows exactly which labels were computed.
|
||||
resolved_labels jsonb,
|
||||
-- ID of an existing chat that was found via label matching and
|
||||
-- continued with a new message.
|
||||
matched_chat_id uuid,
|
||||
-- ID of a newly created chat (mutually exclusive with
|
||||
-- matched_chat_id in practice).
|
||||
created_chat_id uuid,
|
||||
-- Outcome of the event:
|
||||
-- filtered — filter did not match, event dropped.
|
||||
-- preview — automation is in preview mode, no chat action.
|
||||
-- created — new chat was created.
|
||||
-- continued — existing chat was continued.
|
||||
-- rate_limited — rate limit prevented chat action.
|
||||
-- error — something went wrong (see error column).
|
||||
status chat_automation_event_status NOT NULL,
|
||||
-- Human-readable error description when status = 'error' or
|
||||
-- 'rate_limited'. NULL for successful outcomes.
|
||||
error text,
|
||||
PRIMARY KEY (id),
|
||||
FOREIGN KEY (automation_id) REFERENCES chat_automations(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (trigger_id) REFERENCES chat_automation_triggers(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (matched_chat_id) REFERENCES chats(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (created_chat_id) REFERENCES chats(id) ON DELETE SET NULL,
|
||||
CONSTRAINT chat_automation_events_chat_exclusivity CHECK (
|
||||
matched_chat_id IS NULL OR created_chat_id IS NULL
|
||||
)
|
||||
);
|
||||
|
||||
-- Composite index for listing events per automation in reverse
|
||||
-- chronological order (the primary UI query pattern).
|
||||
CREATE INDEX idx_chat_automation_events_automation_id_received_at ON chat_automation_events (automation_id, received_at DESC);
|
||||
|
||||
-- Standalone index on received_at for the purge job, which deletes
|
||||
-- events older than the retention period across all automations.
|
||||
CREATE INDEX idx_chat_automation_events_received_at ON chat_automation_events (received_at);
|
||||
|
||||
-- Partial index for rate-limit window count queries, which filter
|
||||
-- by automation_id and status IN ('created', 'continued').
|
||||
CREATE INDEX idx_chat_automation_events_rate_limit
|
||||
ON chat_automation_events (automation_id, received_at)
|
||||
WHERE status IN ('created', 'continued');
|
||||
|
||||
-- Link chats back to the automation that created them. SET NULL on
|
||||
-- delete so chats survive if the automation is removed. Indexed for
|
||||
-- lookup queries that list chats spawned by a given automation.
|
||||
ALTER TABLE chats ADD COLUMN automation_id uuid REFERENCES chat_automations(id) ON DELETE SET NULL;
|
||||
|
||||
CREATE INDEX idx_chats_automation_id ON chats (automation_id);
|
||||
|
||||
-- Enum type comments.
|
||||
COMMENT ON TYPE chat_automation_status IS 'Lifecycle state of a chat automation: disabled, preview, or active.';
|
||||
COMMENT ON TYPE chat_automation_trigger_type IS 'Discriminator for chat automation triggers: webhook or cron.';
|
||||
COMMENT ON TYPE chat_automation_event_status IS 'Outcome of a chat automation event: filtered, preview, created, continued, rate_limited, or error.';
|
||||
|
||||
-- Table comments.
|
||||
COMMENT ON TABLE chat_automations IS 'Chat automations bridge external events (webhooks, cron schedules) to Coder chats. A chat automation defines what to say, which model and tools to use, and how fast it is allowed to create or continue chats.';
|
||||
COMMENT ON TABLE chat_automation_triggers IS 'Triggers define how an automation is invoked. Each automation can have multiple triggers (e.g. one webhook + one cron schedule). Webhook and cron triggers share the same row shape with type-specific nullable columns to keep the schema simple.';
|
||||
COMMENT ON TABLE chat_automation_events IS 'Every trigger invocation produces an event row regardless of outcome. This table is the audit trail and the data source for rate-limit window counts. Rows are append-only and expected to be purged by a background job after a retention period.';
|
||||
|
||||
-- Column comments for chat_automations.
|
||||
COMMENT ON COLUMN chat_automations.owner_id IS 'The user on whose behalf chats are created. All RBAC checks and chat ownership are scoped to this user.';
|
||||
COMMENT ON COLUMN chat_automations.organization_id IS 'Organization scope for RBAC. Combined with owner_id and name to form a unique constraint so automations are namespaced per user per org.';
|
||||
COMMENT ON COLUMN chat_automations.instructions IS 'The user-role message injected into every chat this automation creates. This is the core prompt that tells the LLM what to do.';
|
||||
COMMENT ON COLUMN chat_automations.model_config_id IS 'Optional model configuration override. When NULL the deployment default is used. SET NULL on delete so automations survive config changes gracefully.';
|
||||
COMMENT ON COLUMN chat_automations.mcp_server_ids IS 'MCP servers to attach to chats created by this automation. Stored as an array of UUIDs rather than a join table because the set is small and always read/written atomically.';
|
||||
COMMENT ON COLUMN chat_automations.allowed_tools IS 'Tool allowlist. Empty means all tools available to the model config are permitted.';
|
||||
COMMENT ON COLUMN chat_automations.status IS 'Lifecycle state: disabled — trigger events are silently dropped; preview — events are logged but no chat is created (dry-run); active — events create or continue chats.';
|
||||
COMMENT ON COLUMN chat_automations.max_chat_creates_per_hour IS 'Maximum number of new chats this automation may create in a rolling one-hour window. Prevents runaway webhook storms from flooding the system.';
|
||||
COMMENT ON COLUMN chat_automations.max_messages_per_hour IS 'Maximum total messages (creates + continues) this automation may send in a rolling one-hour window. A second, broader throttle that catches high-frequency continuation patterns.';
|
||||
|
||||
-- Column comments for chat_automation_triggers.
|
||||
COMMENT ON COLUMN chat_automation_triggers.type IS 'Discriminator: webhook or cron. Determines which nullable columns are meaningful.';
|
||||
COMMENT ON COLUMN chat_automation_triggers.webhook_secret IS 'HMAC-SHA256 shared secret for webhook signature verification (X-Hub-Signature-256 header). NULL for cron triggers.';
|
||||
COMMENT ON COLUMN chat_automation_triggers.cron_schedule IS 'Standard 5-field cron expression (minute hour dom month dow), with optional CRON_TZ= prefix. NULL for webhook triggers.';
|
||||
COMMENT ON COLUMN chat_automation_triggers.filter IS 'gjson path-to-value filter conditions evaluated against the incoming webhook payload. All conditions must match for the trigger to fire. NULL or empty means match everything.';
|
||||
COMMENT ON COLUMN chat_automation_triggers.label_paths IS 'Maps chat label keys to gjson paths. When a trigger fires, labels are resolved from the payload and used to find an existing chat to continue (by label match) or set on a newly created chat.';
|
||||
COMMENT ON COLUMN chat_automation_triggers.last_triggered_at IS 'Timestamp of the last successful cron fire. The scheduler computes next = cron.Next(last_triggered_at) and fires when next <= now. NULL means the trigger has never fired. Not used for webhook triggers.';
|
||||
|
||||
-- Column comments for chat_automation_events.
|
||||
COMMENT ON COLUMN chat_automation_events.payload IS 'The raw payload that was evaluated. For webhooks this is the HTTP body; for cron triggers it is a synthetic JSON envelope with schedule metadata.';
|
||||
COMMENT ON COLUMN chat_automation_events.filter_matched IS 'Whether the trigger filter conditions matched. False means the event was dropped before any chat interaction.';
|
||||
COMMENT ON COLUMN chat_automation_events.resolved_labels IS 'Labels resolved from the payload via label_paths. Stored so the event log shows exactly which labels were computed.';
|
||||
COMMENT ON COLUMN chat_automation_events.matched_chat_id IS 'ID of an existing chat that was found via label matching and continued with a new message.';
|
||||
COMMENT ON COLUMN chat_automation_events.created_chat_id IS 'ID of a newly created chat (mutually exclusive with matched_chat_id in practice).';
|
||||
COMMENT ON COLUMN chat_automation_events.status IS 'Outcome of the event: filtered — filter did not match; preview — automation is in preview mode; created — new chat was created; continued — existing chat was continued; rate_limited — rate limit prevented chat action; error — something went wrong.';
|
||||
|
||||
-- Add API key scope values for the new chat_automation resource type.
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'chat_automation:create';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'chat_automation:read';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'chat_automation:update';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'chat_automation:delete';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'chat_automation:*';
|
||||
@@ -877,149 +877,3 @@ func TestMigration000387MigrateTaskWorkspaces(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, antCount, "antagonist workspaces (deleted and regular) should not be migrated")
|
||||
}
|
||||
|
||||
func TestMigration000457ChatAccessRole(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const migrationVersion = 457
|
||||
|
||||
sqlDB := testSQLDB(t)
|
||||
|
||||
// Migrate up to the migration before the one that grants
|
||||
// agents-access roles.
|
||||
next, err := migrations.Stepper(sqlDB)
|
||||
require.NoError(t, err)
|
||||
for {
|
||||
version, more, err := next()
|
||||
require.NoError(t, err)
|
||||
if !more {
|
||||
t.Fatalf("migration %d not found", migrationVersion)
|
||||
}
|
||||
if version == migrationVersion-1 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitSuperLong)
|
||||
|
||||
// Define test users.
|
||||
userWithChat := uuid.New() // Has a chat, no agents-access role.
|
||||
userAlreadyHasRole := uuid.New() // Has a chat and already has agents-access.
|
||||
userNoChat := uuid.New() // No chat at all.
|
||||
userWithChatAndRoles := uuid.New() // Has a chat and other existing roles.
|
||||
|
||||
now := time.Now().UTC().Truncate(time.Microsecond)
|
||||
|
||||
// We need a chat_provider and chat_model_config for the chats FK.
|
||||
providerID := uuid.New()
|
||||
modelConfigID := uuid.New()
|
||||
|
||||
tx, err := sqlDB.BeginTx(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
defer tx.Rollback()
|
||||
|
||||
fixtures := []struct {
|
||||
query string
|
||||
args []any
|
||||
}{
|
||||
// Insert test users with varying rbac_roles.
|
||||
{
|
||||
`INSERT INTO users (id, username, email, hashed_password, created_at, updated_at, status, rbac_roles, login_type)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
|
||||
[]any{userWithChat, "user-with-chat", "chat@test.com", []byte{}, now, now, "active", pq.StringArray{}, "password"},
|
||||
},
|
||||
{
|
||||
`INSERT INTO users (id, username, email, hashed_password, created_at, updated_at, status, rbac_roles, login_type)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
|
||||
[]any{userAlreadyHasRole, "user-already-has-role", "already@test.com", []byte{}, now, now, "active", pq.StringArray{"agents-access"}, "password"},
|
||||
},
|
||||
{
|
||||
`INSERT INTO users (id, username, email, hashed_password, created_at, updated_at, status, rbac_roles, login_type)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
|
||||
[]any{userNoChat, "user-no-chat", "nochat@test.com", []byte{}, now, now, "active", pq.StringArray{}, "password"},
|
||||
},
|
||||
{
|
||||
`INSERT INTO users (id, username, email, hashed_password, created_at, updated_at, status, rbac_roles, login_type)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
|
||||
[]any{userWithChatAndRoles, "user-with-roles", "roles@test.com", []byte{}, now, now, "active", pq.StringArray{"template-admin"}, "password"},
|
||||
},
|
||||
// Insert a chat provider and model config for the chats FK.
|
||||
{
|
||||
`INSERT INTO chat_providers (id, provider, display_name, api_key, enabled, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
||||
[]any{providerID, "openai", "OpenAI", "", true, now, now},
|
||||
},
|
||||
{
|
||||
`INSERT INTO chat_model_configs (id, provider, model, display_name, enabled, context_limit, compression_threshold, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
|
||||
[]any{modelConfigID, "openai", "gpt-4", "GPT 4", true, 100000, 70, now, now},
|
||||
},
|
||||
// Insert chats for users A, B, and D (not C).
|
||||
{
|
||||
`INSERT INTO chats (id, owner_id, last_model_config_id, title, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)`,
|
||||
[]any{uuid.New(), userWithChat, modelConfigID, "Chat A", now, now},
|
||||
},
|
||||
{
|
||||
`INSERT INTO chats (id, owner_id, last_model_config_id, title, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)`,
|
||||
[]any{uuid.New(), userAlreadyHasRole, modelConfigID, "Chat B", now, now},
|
||||
},
|
||||
{
|
||||
`INSERT INTO chats (id, owner_id, last_model_config_id, title, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)`,
|
||||
[]any{uuid.New(), userWithChatAndRoles, modelConfigID, "Chat D", now, now},
|
||||
},
|
||||
}
|
||||
|
||||
for i, f := range fixtures {
|
||||
_, err := tx.ExecContext(ctx, f.query, f.args...)
|
||||
require.NoError(t, err, "fixture %d", i)
|
||||
}
|
||||
require.NoError(t, tx.Commit())
|
||||
|
||||
// Run the migration.
|
||||
version, _, err := next()
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, migrationVersion, version)
|
||||
|
||||
// Helper to get rbac_roles for a user.
|
||||
getRoles := func(t *testing.T, userID uuid.UUID) []string {
|
||||
t.Helper()
|
||||
var roles pq.StringArray
|
||||
err := sqlDB.QueryRowContext(ctx,
|
||||
"SELECT rbac_roles FROM users WHERE id = $1", userID,
|
||||
).Scan(&roles)
|
||||
require.NoError(t, err)
|
||||
return roles
|
||||
}
|
||||
|
||||
// Verify: user with chat gets agents-access.
|
||||
roles := getRoles(t, userWithChat)
|
||||
require.Contains(t, roles, "agents-access",
|
||||
"user with chat should get agents-access")
|
||||
|
||||
// Verify: user who already had agents-access has no duplicate.
|
||||
roles = getRoles(t, userAlreadyHasRole)
|
||||
count := 0
|
||||
for _, r := range roles {
|
||||
if r == "agents-access" {
|
||||
count++
|
||||
}
|
||||
}
|
||||
require.Equal(t, 1, count,
|
||||
"user who already had agents-access should not get a duplicate")
|
||||
|
||||
// Verify: user without chat does NOT get agents-access.
|
||||
roles = getRoles(t, userNoChat)
|
||||
require.NotContains(t, roles, "agents-access",
|
||||
"user without chat should not get agents-access")
|
||||
|
||||
// Verify: user with chat and existing roles gets agents-access
|
||||
// appended while preserving existing roles.
|
||||
roles = getRoles(t, userWithChatAndRoles)
|
||||
require.Contains(t, roles, "agents-access",
|
||||
"user with chat and other roles should get agents-access")
|
||||
require.Contains(t, roles, "template-admin",
|
||||
"existing roles should be preserved")
|
||||
}
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
INSERT INTO chat_automations (
|
||||
id,
|
||||
owner_id,
|
||||
organization_id,
|
||||
name,
|
||||
description,
|
||||
instructions,
|
||||
model_config_id,
|
||||
mcp_server_ids,
|
||||
allowed_tools,
|
||||
status,
|
||||
max_chat_creates_per_hour,
|
||||
max_messages_per_hour,
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
SELECT
|
||||
'b3d0fd0e-8e1a-4f2c-9a3b-1234567890ab',
|
||||
u.id,
|
||||
o.id,
|
||||
'fixture-automation',
|
||||
'Fixture automation for migration testing.',
|
||||
'You are a helpful assistant.',
|
||||
NULL,
|
||||
'{}',
|
||||
'{}',
|
||||
'active',
|
||||
10,
|
||||
60,
|
||||
'2024-01-01 00:00:00+00',
|
||||
'2024-01-01 00:00:00+00'
|
||||
FROM users u
|
||||
CROSS JOIN organizations o
|
||||
ORDER BY u.created_at, u.id
|
||||
LIMIT 1;
|
||||
|
||||
INSERT INTO chat_automation_triggers (
|
||||
id,
|
||||
automation_id,
|
||||
type,
|
||||
webhook_secret,
|
||||
webhook_secret_key_id,
|
||||
cron_schedule,
|
||||
last_triggered_at,
|
||||
filter,
|
||||
label_paths,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (
|
||||
'c4e1fe1f-9f2b-4a3d-ab4c-234567890abc',
|
||||
'b3d0fd0e-8e1a-4f2c-9a3b-1234567890ab',
|
||||
'webhook',
|
||||
'whsec_fixture_secret',
|
||||
NULL,
|
||||
NULL,
|
||||
NULL,
|
||||
'{"action": "opened"}'::jsonb,
|
||||
'{"repo": "repository.full_name"}'::jsonb,
|
||||
'2024-01-01 00:00:00+00',
|
||||
'2024-01-01 00:00:00+00'
|
||||
);
|
||||
|
||||
INSERT INTO chat_automation_events (
|
||||
id,
|
||||
automation_id,
|
||||
trigger_id,
|
||||
received_at,
|
||||
payload,
|
||||
filter_matched,
|
||||
resolved_labels,
|
||||
matched_chat_id,
|
||||
created_chat_id,
|
||||
status,
|
||||
error
|
||||
) VALUES (
|
||||
'd5f20f20-a03c-4b4e-bc5d-345678901bcd',
|
||||
'b3d0fd0e-8e1a-4f2c-9a3b-1234567890ab',
|
||||
'c4e1fe1f-9f2b-4a3d-ab4c-234567890abc',
|
||||
'2024-01-01 00:00:00+00',
|
||||
'{"action": "opened", "repository": {"full_name": "coder/coder"}}'::jsonb,
|
||||
TRUE,
|
||||
'{"repo": "coder/coder"}'::jsonb,
|
||||
NULL,
|
||||
NULL,
|
||||
'preview',
|
||||
NULL
|
||||
);
|
||||
@@ -178,17 +178,6 @@ func (c Chat) RBACObject() rbac.Object {
|
||||
return rbac.ResourceChat.WithID(c.ID).WithOwner(c.OwnerID.String())
|
||||
}
|
||||
|
||||
func (r GetChatsRow) RBACObject() rbac.Object {
|
||||
return r.Chat.RBACObject()
|
||||
}
|
||||
|
||||
func (a ChatAutomation) RBACObject() rbac.Object {
|
||||
return rbac.ResourceChatAutomation.
|
||||
WithID(a.ID).
|
||||
WithOwner(a.OwnerID.String()).
|
||||
InOrg(a.OrganizationID)
|
||||
}
|
||||
|
||||
func (c ChatFile) RBACObject() rbac.Object {
|
||||
return rbac.ResourceChat.WithID(c.ID).WithOwner(c.OwnerID.String()).InOrg(c.OrganizationID)
|
||||
}
|
||||
|
||||
+20
-175
@@ -53,7 +53,6 @@ type customQuerier interface {
|
||||
connectionLogQuerier
|
||||
aibridgeQuerier
|
||||
chatQuerier
|
||||
chatAutomationQuerier
|
||||
}
|
||||
|
||||
type templateQuerier interface {
|
||||
@@ -742,10 +741,10 @@ func (q *sqlQuerier) CountAuthorizedConnectionLogs(ctx context.Context, arg Coun
|
||||
}
|
||||
|
||||
type chatQuerier interface {
|
||||
GetAuthorizedChats(ctx context.Context, arg GetChatsParams, prepared rbac.PreparedAuthorized) ([]GetChatsRow, error)
|
||||
GetAuthorizedChats(ctx context.Context, arg GetChatsParams, prepared rbac.PreparedAuthorized) ([]Chat, error)
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) GetAuthorizedChats(ctx context.Context, arg GetChatsParams, prepared rbac.PreparedAuthorized) ([]GetChatsRow, error) {
|
||||
func (q *sqlQuerier) GetAuthorizedChats(ctx context.Context, arg GetChatsParams, prepared rbac.PreparedAuthorized) ([]Chat, error) {
|
||||
authorizedFilter, err := prepared.CompileToSQL(ctx, rbac.ConfigChats())
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("compile authorized filter: %w", err)
|
||||
@@ -770,96 +769,28 @@ func (q *sqlQuerier) GetAuthorizedChats(ctx context.Context, arg GetChatsParams,
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetChatsRow
|
||||
var items []Chat
|
||||
for rows.Next() {
|
||||
var i GetChatsRow
|
||||
if err := rows.Scan(
|
||||
&i.Chat.ID,
|
||||
&i.Chat.OwnerID,
|
||||
&i.Chat.WorkspaceID,
|
||||
&i.Chat.Title,
|
||||
&i.Chat.Status,
|
||||
&i.Chat.WorkerID,
|
||||
&i.Chat.StartedAt,
|
||||
&i.Chat.HeartbeatAt,
|
||||
&i.Chat.CreatedAt,
|
||||
&i.Chat.UpdatedAt,
|
||||
&i.Chat.ParentChatID,
|
||||
&i.Chat.RootChatID,
|
||||
&i.Chat.LastModelConfigID,
|
||||
&i.Chat.Archived,
|
||||
&i.Chat.LastError,
|
||||
&i.Chat.Mode,
|
||||
pq.Array(&i.Chat.MCPServerIDs),
|
||||
&i.Chat.Labels,
|
||||
&i.Chat.BuildID,
|
||||
&i.Chat.AgentID,
|
||||
&i.Chat.PinOrder,
|
||||
&i.Chat.LastReadMessageID,
|
||||
&i.Chat.LastInjectedContext,
|
||||
&i.Chat.AutomationID,
|
||||
&i.HasUnread,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
type chatAutomationQuerier interface {
|
||||
GetAuthorizedChatAutomations(ctx context.Context, arg GetChatAutomationsParams, prepared rbac.PreparedAuthorized) ([]ChatAutomation, error)
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) GetAuthorizedChatAutomations(ctx context.Context, arg GetChatAutomationsParams, prepared rbac.PreparedAuthorized) ([]ChatAutomation, error) {
|
||||
authorizedFilter, err := prepared.CompileToSQL(ctx, regosql.ConvertConfig{
|
||||
VariableConverter: regosql.NoACLConverter(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("compile authorized filter: %w", err)
|
||||
}
|
||||
|
||||
filtered, err := insertAuthorizedFilter(getChatAutomations, fmt.Sprintf(" AND %s", authorizedFilter))
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("insert authorized filter: %w", err)
|
||||
}
|
||||
|
||||
// The name comment is for metric tracking
|
||||
query := fmt.Sprintf("-- name: GetAuthorizedChatAutomations :many\n%s", filtered)
|
||||
rows, err := q.db.QueryContext(ctx, query,
|
||||
arg.OwnerID,
|
||||
arg.OrganizationID,
|
||||
arg.OffsetOpt,
|
||||
arg.LimitOpt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ChatAutomation
|
||||
for rows.Next() {
|
||||
var i ChatAutomation
|
||||
var i Chat
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.OwnerID,
|
||||
&i.OrganizationID,
|
||||
&i.Name,
|
||||
&i.Description,
|
||||
&i.Instructions,
|
||||
&i.ModelConfigID,
|
||||
pq.Array(&i.MCPServerIDs),
|
||||
pq.Array(&i.AllowedTools),
|
||||
&i.WorkspaceID,
|
||||
&i.Title,
|
||||
&i.Status,
|
||||
&i.MaxChatCreatesPerHour,
|
||||
&i.MaxMessagesPerHour,
|
||||
&i.WorkerID,
|
||||
&i.StartedAt,
|
||||
&i.HeartbeatAt,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.ParentChatID,
|
||||
&i.RootChatID,
|
||||
&i.LastModelConfigID,
|
||||
&i.Archived,
|
||||
&i.LastError,
|
||||
&i.Mode,
|
||||
pq.Array(&i.MCPServerIDs),
|
||||
&i.Labels,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -878,10 +809,8 @@ type aibridgeQuerier interface {
|
||||
ListAuthorizedAIBridgeInterceptions(ctx context.Context, arg ListAIBridgeInterceptionsParams, prepared rbac.PreparedAuthorized) ([]ListAIBridgeInterceptionsRow, error)
|
||||
CountAuthorizedAIBridgeInterceptions(ctx context.Context, arg CountAIBridgeInterceptionsParams, prepared rbac.PreparedAuthorized) (int64, error)
|
||||
ListAuthorizedAIBridgeModels(ctx context.Context, arg ListAIBridgeModelsParams, prepared rbac.PreparedAuthorized) ([]string, error)
|
||||
ListAuthorizedAIBridgeClients(ctx context.Context, arg ListAIBridgeClientsParams, prepared rbac.PreparedAuthorized) ([]string, error)
|
||||
ListAuthorizedAIBridgeSessions(ctx context.Context, arg ListAIBridgeSessionsParams, prepared rbac.PreparedAuthorized) ([]ListAIBridgeSessionsRow, error)
|
||||
CountAuthorizedAIBridgeSessions(ctx context.Context, arg CountAIBridgeSessionsParams, prepared rbac.PreparedAuthorized) (int64, error)
|
||||
ListAuthorizedAIBridgeSessionThreads(ctx context.Context, arg ListAIBridgeSessionThreadsParams, prepared rbac.PreparedAuthorized) ([]ListAIBridgeSessionThreadsRow, error)
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) ListAuthorizedAIBridgeInterceptions(ctx context.Context, arg ListAIBridgeInterceptionsParams, prepared rbac.PreparedAuthorized) ([]ListAIBridgeInterceptionsRow, error) {
|
||||
@@ -1016,35 +945,6 @@ func (q *sqlQuerier) ListAuthorizedAIBridgeModels(ctx context.Context, arg ListA
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) ListAuthorizedAIBridgeClients(ctx context.Context, arg ListAIBridgeClientsParams, prepared rbac.PreparedAuthorized) ([]string, error) {
|
||||
authorizedFilter, err := prepared.CompileToSQL(ctx, regosql.ConvertConfig{
|
||||
VariableConverter: regosql.AIBridgeInterceptionConverter(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("compile authorized filter: %w", err)
|
||||
}
|
||||
filtered, err := insertAuthorizedFilter(listAIBridgeClients, fmt.Sprintf(" AND %s", authorizedFilter))
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("insert authorized filter: %w", err)
|
||||
}
|
||||
|
||||
query := fmt.Sprintf("-- name: ListAIBridgeClients :many\n%s", filtered)
|
||||
rows, err := q.db.QueryContext(ctx, query, arg.Client, arg.Offset, arg.Limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []string
|
||||
for rows.Next() {
|
||||
var client string
|
||||
if err := rows.Scan(&client); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, client)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) ListAuthorizedAIBridgeSessions(ctx context.Context, arg ListAIBridgeSessionsParams, prepared rbac.PreparedAuthorized) ([]ListAIBridgeSessionsRow, error) {
|
||||
authorizedFilter, err := prepared.CompileToSQL(ctx, regosql.ConvertConfig{
|
||||
VariableConverter: regosql.AIBridgeInterceptionConverter(),
|
||||
@@ -1060,6 +960,8 @@ func (q *sqlQuerier) ListAuthorizedAIBridgeSessions(ctx context.Context, arg Lis
|
||||
query := fmt.Sprintf("-- name: ListAuthorizedAIBridgeSessions :many\n%s", filtered)
|
||||
rows, err := q.db.QueryContext(ctx, query,
|
||||
arg.AfterSessionID,
|
||||
arg.Offset,
|
||||
arg.Limit,
|
||||
arg.StartedAfter,
|
||||
arg.StartedBefore,
|
||||
arg.InitiatorID,
|
||||
@@ -1067,8 +969,6 @@ func (q *sqlQuerier) ListAuthorizedAIBridgeSessions(ctx context.Context, arg Lis
|
||||
arg.Model,
|
||||
arg.Client,
|
||||
arg.SessionID,
|
||||
arg.Offset,
|
||||
arg.Limit,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1148,66 +1048,11 @@ func (q *sqlQuerier) CountAuthorizedAIBridgeSessions(ctx context.Context, arg Co
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) ListAuthorizedAIBridgeSessionThreads(ctx context.Context, arg ListAIBridgeSessionThreadsParams, prepared rbac.PreparedAuthorized) ([]ListAIBridgeSessionThreadsRow, error) {
|
||||
authorizedFilter, err := prepared.CompileToSQL(ctx, regosql.ConvertConfig{
|
||||
VariableConverter: regosql.AIBridgeInterceptionConverter(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("compile authorized filter: %w", err)
|
||||
}
|
||||
filtered, err := insertAuthorizedFilter(listAIBridgeSessionThreads, fmt.Sprintf(" AND %s", authorizedFilter))
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("insert authorized filter: %w", err)
|
||||
}
|
||||
|
||||
query := fmt.Sprintf("-- name: ListAuthorizedAIBridgeSessionThreads :many\n%s", filtered)
|
||||
rows, err := q.db.QueryContext(ctx, query,
|
||||
arg.SessionID,
|
||||
arg.AfterID,
|
||||
arg.BeforeID,
|
||||
arg.Limit,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ListAIBridgeSessionThreadsRow
|
||||
for rows.Next() {
|
||||
var i ListAIBridgeSessionThreadsRow
|
||||
if err := rows.Scan(
|
||||
&i.ThreadID,
|
||||
&i.AIBridgeInterception.ID,
|
||||
&i.AIBridgeInterception.InitiatorID,
|
||||
&i.AIBridgeInterception.Provider,
|
||||
&i.AIBridgeInterception.Model,
|
||||
&i.AIBridgeInterception.StartedAt,
|
||||
&i.AIBridgeInterception.Metadata,
|
||||
&i.AIBridgeInterception.EndedAt,
|
||||
&i.AIBridgeInterception.APIKeyID,
|
||||
&i.AIBridgeInterception.Client,
|
||||
&i.AIBridgeInterception.ThreadParentID,
|
||||
&i.AIBridgeInterception.ThreadRootID,
|
||||
&i.AIBridgeInterception.ClientSessionID,
|
||||
&i.AIBridgeInterception.SessionID,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func insertAuthorizedFilter(query string, replaceWith string) (string, error) {
|
||||
if !strings.Contains(query, authorizedQueryPlaceholder) {
|
||||
return "", xerrors.Errorf("query does not contain authorized replace string, this is not an authorized query")
|
||||
}
|
||||
filtered := strings.ReplaceAll(query, authorizedQueryPlaceholder, replaceWith)
|
||||
filtered := strings.Replace(query, authorizedQueryPlaceholder, replaceWith, 1)
|
||||
return filtered, nil
|
||||
}
|
||||
|
||||
|
||||
+19
-302
@@ -224,11 +224,6 @@ const (
|
||||
ApiKeyScopeChatUpdate APIKeyScope = "chat:update"
|
||||
ApiKeyScopeChatDelete APIKeyScope = "chat:delete"
|
||||
ApiKeyScopeChat APIKeyScope = "chat:*"
|
||||
ApiKeyScopeChatAutomationCreate APIKeyScope = "chat_automation:create"
|
||||
ApiKeyScopeChatAutomationRead APIKeyScope = "chat_automation:read"
|
||||
ApiKeyScopeChatAutomationUpdate APIKeyScope = "chat_automation:update"
|
||||
ApiKeyScopeChatAutomationDelete APIKeyScope = "chat_automation:delete"
|
||||
ApiKeyScopeChatAutomation APIKeyScope = "chat_automation:*"
|
||||
)
|
||||
|
||||
func (e *APIKeyScope) Scan(src interface{}) error {
|
||||
@@ -472,12 +467,7 @@ func (e APIKeyScope) Valid() bool {
|
||||
ApiKeyScopeChatRead,
|
||||
ApiKeyScopeChatUpdate,
|
||||
ApiKeyScopeChatDelete,
|
||||
ApiKeyScopeChat,
|
||||
ApiKeyScopeChatAutomationCreate,
|
||||
ApiKeyScopeChatAutomationRead,
|
||||
ApiKeyScopeChatAutomationUpdate,
|
||||
ApiKeyScopeChatAutomationDelete,
|
||||
ApiKeyScopeChatAutomation:
|
||||
ApiKeyScopeChat:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -690,11 +680,6 @@ func AllAPIKeyScopeValues() []APIKeyScope {
|
||||
ApiKeyScopeChatUpdate,
|
||||
ApiKeyScopeChatDelete,
|
||||
ApiKeyScopeChat,
|
||||
ApiKeyScopeChatAutomationCreate,
|
||||
ApiKeyScopeChatAutomationRead,
|
||||
ApiKeyScopeChatAutomationUpdate,
|
||||
ApiKeyScopeChatAutomationDelete,
|
||||
ApiKeyScopeChatAutomation,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1122,198 +1107,6 @@ func AllBuildReasonValues() []BuildReason {
|
||||
}
|
||||
}
|
||||
|
||||
// Outcome of a chat automation event: filtered, preview, created, continued, rate_limited, or error.
|
||||
type ChatAutomationEventStatus string
|
||||
|
||||
const (
|
||||
ChatAutomationEventStatusFiltered ChatAutomationEventStatus = "filtered"
|
||||
ChatAutomationEventStatusPreview ChatAutomationEventStatus = "preview"
|
||||
ChatAutomationEventStatusCreated ChatAutomationEventStatus = "created"
|
||||
ChatAutomationEventStatusContinued ChatAutomationEventStatus = "continued"
|
||||
ChatAutomationEventStatusRateLimited ChatAutomationEventStatus = "rate_limited"
|
||||
ChatAutomationEventStatusError ChatAutomationEventStatus = "error"
|
||||
)
|
||||
|
||||
func (e *ChatAutomationEventStatus) Scan(src interface{}) error {
|
||||
switch s := src.(type) {
|
||||
case []byte:
|
||||
*e = ChatAutomationEventStatus(s)
|
||||
case string:
|
||||
*e = ChatAutomationEventStatus(s)
|
||||
default:
|
||||
return fmt.Errorf("unsupported scan type for ChatAutomationEventStatus: %T", src)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type NullChatAutomationEventStatus struct {
|
||||
ChatAutomationEventStatus ChatAutomationEventStatus `json:"chat_automation_event_status"`
|
||||
Valid bool `json:"valid"` // Valid is true if ChatAutomationEventStatus is not NULL
|
||||
}
|
||||
|
||||
// Scan implements the Scanner interface.
|
||||
func (ns *NullChatAutomationEventStatus) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
ns.ChatAutomationEventStatus, ns.Valid = "", false
|
||||
return nil
|
||||
}
|
||||
ns.Valid = true
|
||||
return ns.ChatAutomationEventStatus.Scan(value)
|
||||
}
|
||||
|
||||
// Value implements the driver Valuer interface.
|
||||
func (ns NullChatAutomationEventStatus) Value() (driver.Value, error) {
|
||||
if !ns.Valid {
|
||||
return nil, nil
|
||||
}
|
||||
return string(ns.ChatAutomationEventStatus), nil
|
||||
}
|
||||
|
||||
func (e ChatAutomationEventStatus) Valid() bool {
|
||||
switch e {
|
||||
case ChatAutomationEventStatusFiltered,
|
||||
ChatAutomationEventStatusPreview,
|
||||
ChatAutomationEventStatusCreated,
|
||||
ChatAutomationEventStatusContinued,
|
||||
ChatAutomationEventStatusRateLimited,
|
||||
ChatAutomationEventStatusError:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func AllChatAutomationEventStatusValues() []ChatAutomationEventStatus {
|
||||
return []ChatAutomationEventStatus{
|
||||
ChatAutomationEventStatusFiltered,
|
||||
ChatAutomationEventStatusPreview,
|
||||
ChatAutomationEventStatusCreated,
|
||||
ChatAutomationEventStatusContinued,
|
||||
ChatAutomationEventStatusRateLimited,
|
||||
ChatAutomationEventStatusError,
|
||||
}
|
||||
}
|
||||
|
||||
// Lifecycle state of a chat automation: disabled, preview, or active.
|
||||
type ChatAutomationStatus string
|
||||
|
||||
const (
|
||||
ChatAutomationStatusDisabled ChatAutomationStatus = "disabled"
|
||||
ChatAutomationStatusPreview ChatAutomationStatus = "preview"
|
||||
ChatAutomationStatusActive ChatAutomationStatus = "active"
|
||||
)
|
||||
|
||||
func (e *ChatAutomationStatus) Scan(src interface{}) error {
|
||||
switch s := src.(type) {
|
||||
case []byte:
|
||||
*e = ChatAutomationStatus(s)
|
||||
case string:
|
||||
*e = ChatAutomationStatus(s)
|
||||
default:
|
||||
return fmt.Errorf("unsupported scan type for ChatAutomationStatus: %T", src)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type NullChatAutomationStatus struct {
|
||||
ChatAutomationStatus ChatAutomationStatus `json:"chat_automation_status"`
|
||||
Valid bool `json:"valid"` // Valid is true if ChatAutomationStatus is not NULL
|
||||
}
|
||||
|
||||
// Scan implements the Scanner interface.
|
||||
func (ns *NullChatAutomationStatus) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
ns.ChatAutomationStatus, ns.Valid = "", false
|
||||
return nil
|
||||
}
|
||||
ns.Valid = true
|
||||
return ns.ChatAutomationStatus.Scan(value)
|
||||
}
|
||||
|
||||
// Value implements the driver Valuer interface.
|
||||
func (ns NullChatAutomationStatus) Value() (driver.Value, error) {
|
||||
if !ns.Valid {
|
||||
return nil, nil
|
||||
}
|
||||
return string(ns.ChatAutomationStatus), nil
|
||||
}
|
||||
|
||||
func (e ChatAutomationStatus) Valid() bool {
|
||||
switch e {
|
||||
case ChatAutomationStatusDisabled,
|
||||
ChatAutomationStatusPreview,
|
||||
ChatAutomationStatusActive:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func AllChatAutomationStatusValues() []ChatAutomationStatus {
|
||||
return []ChatAutomationStatus{
|
||||
ChatAutomationStatusDisabled,
|
||||
ChatAutomationStatusPreview,
|
||||
ChatAutomationStatusActive,
|
||||
}
|
||||
}
|
||||
|
||||
// Discriminator for chat automation triggers: webhook or cron.
|
||||
type ChatAutomationTriggerType string
|
||||
|
||||
const (
|
||||
ChatAutomationTriggerTypeWebhook ChatAutomationTriggerType = "webhook"
|
||||
ChatAutomationTriggerTypeCron ChatAutomationTriggerType = "cron"
|
||||
)
|
||||
|
||||
func (e *ChatAutomationTriggerType) Scan(src interface{}) error {
|
||||
switch s := src.(type) {
|
||||
case []byte:
|
||||
*e = ChatAutomationTriggerType(s)
|
||||
case string:
|
||||
*e = ChatAutomationTriggerType(s)
|
||||
default:
|
||||
return fmt.Errorf("unsupported scan type for ChatAutomationTriggerType: %T", src)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type NullChatAutomationTriggerType struct {
|
||||
ChatAutomationTriggerType ChatAutomationTriggerType `json:"chat_automation_trigger_type"`
|
||||
Valid bool `json:"valid"` // Valid is true if ChatAutomationTriggerType is not NULL
|
||||
}
|
||||
|
||||
// Scan implements the Scanner interface.
|
||||
func (ns *NullChatAutomationTriggerType) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
ns.ChatAutomationTriggerType, ns.Valid = "", false
|
||||
return nil
|
||||
}
|
||||
ns.Valid = true
|
||||
return ns.ChatAutomationTriggerType.Scan(value)
|
||||
}
|
||||
|
||||
// Value implements the driver Valuer interface.
|
||||
func (ns NullChatAutomationTriggerType) Value() (driver.Value, error) {
|
||||
if !ns.Valid {
|
||||
return nil, nil
|
||||
}
|
||||
return string(ns.ChatAutomationTriggerType), nil
|
||||
}
|
||||
|
||||
func (e ChatAutomationTriggerType) Valid() bool {
|
||||
switch e {
|
||||
case ChatAutomationTriggerTypeWebhook,
|
||||
ChatAutomationTriggerTypeCron:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func AllChatAutomationTriggerTypeValues() []ChatAutomationTriggerType {
|
||||
return []ChatAutomationTriggerType{
|
||||
ChatAutomationTriggerTypeWebhook,
|
||||
ChatAutomationTriggerTypeCron,
|
||||
}
|
||||
}
|
||||
|
||||
type ChatMessageRole string
|
||||
|
||||
const (
|
||||
@@ -4360,99 +4153,24 @@ type BoundaryUsageStat struct {
|
||||
}
|
||||
|
||||
type Chat struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
|
||||
WorkspaceID uuid.NullUUID `db:"workspace_id" json:"workspace_id"`
|
||||
Title string `db:"title" json:"title"`
|
||||
Status ChatStatus `db:"status" json:"status"`
|
||||
WorkerID uuid.NullUUID `db:"worker_id" json:"worker_id"`
|
||||
StartedAt sql.NullTime `db:"started_at" json:"started_at"`
|
||||
HeartbeatAt sql.NullTime `db:"heartbeat_at" json:"heartbeat_at"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
ParentChatID uuid.NullUUID `db:"parent_chat_id" json:"parent_chat_id"`
|
||||
RootChatID uuid.NullUUID `db:"root_chat_id" json:"root_chat_id"`
|
||||
LastModelConfigID uuid.UUID `db:"last_model_config_id" json:"last_model_config_id"`
|
||||
Archived bool `db:"archived" json:"archived"`
|
||||
LastError sql.NullString `db:"last_error" json:"last_error"`
|
||||
Mode NullChatMode `db:"mode" json:"mode"`
|
||||
MCPServerIDs []uuid.UUID `db:"mcp_server_ids" json:"mcp_server_ids"`
|
||||
Labels StringMap `db:"labels" json:"labels"`
|
||||
BuildID uuid.NullUUID `db:"build_id" json:"build_id"`
|
||||
AgentID uuid.NullUUID `db:"agent_id" json:"agent_id"`
|
||||
PinOrder int32 `db:"pin_order" json:"pin_order"`
|
||||
LastReadMessageID sql.NullInt64 `db:"last_read_message_id" json:"last_read_message_id"`
|
||||
LastInjectedContext pqtype.NullRawMessage `db:"last_injected_context" json:"last_injected_context"`
|
||||
AutomationID uuid.NullUUID `db:"automation_id" json:"automation_id"`
|
||||
}
|
||||
|
||||
// Chat automations bridge external events (webhooks, cron schedules) to Coder chats. A chat automation defines what to say, which model and tools to use, and how fast it is allowed to create or continue chats.
|
||||
type ChatAutomation struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
// The user on whose behalf chats are created. All RBAC checks and chat ownership are scoped to this user.
|
||||
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
|
||||
// Organization scope for RBAC. Combined with owner_id and name to form a unique constraint so automations are namespaced per user per org.
|
||||
OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"`
|
||||
Name string `db:"name" json:"name"`
|
||||
Description string `db:"description" json:"description"`
|
||||
// The user-role message injected into every chat this automation creates. This is the core prompt that tells the LLM what to do.
|
||||
Instructions string `db:"instructions" json:"instructions"`
|
||||
// Optional model configuration override. When NULL the deployment default is used. SET NULL on delete so automations survive config changes gracefully.
|
||||
ModelConfigID uuid.NullUUID `db:"model_config_id" json:"model_config_id"`
|
||||
// MCP servers to attach to chats created by this automation. Stored as an array of UUIDs rather than a join table because the set is small and always read/written atomically.
|
||||
MCPServerIDs []uuid.UUID `db:"mcp_server_ids" json:"mcp_server_ids"`
|
||||
// Tool allowlist. Empty means all tools available to the model config are permitted.
|
||||
AllowedTools []string `db:"allowed_tools" json:"allowed_tools"`
|
||||
// Lifecycle state: disabled — trigger events are silently dropped; preview — events are logged but no chat is created (dry-run); active — events create or continue chats.
|
||||
Status ChatAutomationStatus `db:"status" json:"status"`
|
||||
// Maximum number of new chats this automation may create in a rolling one-hour window. Prevents runaway webhook storms from flooding the system.
|
||||
MaxChatCreatesPerHour int32 `db:"max_chat_creates_per_hour" json:"max_chat_creates_per_hour"`
|
||||
// Maximum total messages (creates + continues) this automation may send in a rolling one-hour window. A second, broader throttle that catches high-frequency continuation patterns.
|
||||
MaxMessagesPerHour int32 `db:"max_messages_per_hour" json:"max_messages_per_hour"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
// Every trigger invocation produces an event row regardless of outcome. This table is the audit trail and the data source for rate-limit window counts. Rows are append-only and expected to be purged by a background job after a retention period.
|
||||
type ChatAutomationEvent struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
AutomationID uuid.UUID `db:"automation_id" json:"automation_id"`
|
||||
TriggerID uuid.NullUUID `db:"trigger_id" json:"trigger_id"`
|
||||
ReceivedAt time.Time `db:"received_at" json:"received_at"`
|
||||
// The raw payload that was evaluated. For webhooks this is the HTTP body; for cron triggers it is a synthetic JSON envelope with schedule metadata.
|
||||
Payload json.RawMessage `db:"payload" json:"payload"`
|
||||
// Whether the trigger filter conditions matched. False means the event was dropped before any chat interaction.
|
||||
FilterMatched bool `db:"filter_matched" json:"filter_matched"`
|
||||
// Labels resolved from the payload via label_paths. Stored so the event log shows exactly which labels were computed.
|
||||
ResolvedLabels pqtype.NullRawMessage `db:"resolved_labels" json:"resolved_labels"`
|
||||
// ID of an existing chat that was found via label matching and continued with a new message.
|
||||
MatchedChatID uuid.NullUUID `db:"matched_chat_id" json:"matched_chat_id"`
|
||||
// ID of a newly created chat (mutually exclusive with matched_chat_id in practice).
|
||||
CreatedChatID uuid.NullUUID `db:"created_chat_id" json:"created_chat_id"`
|
||||
// Outcome of the event: filtered — filter did not match; preview — automation is in preview mode; created — new chat was created; continued — existing chat was continued; rate_limited — rate limit prevented chat action; error — something went wrong.
|
||||
Status ChatAutomationEventStatus `db:"status" json:"status"`
|
||||
Error sql.NullString `db:"error" json:"error"`
|
||||
}
|
||||
|
||||
// Triggers define how an automation is invoked. Each automation can have multiple triggers (e.g. one webhook + one cron schedule). Webhook and cron triggers share the same row shape with type-specific nullable columns to keep the schema simple.
|
||||
type ChatAutomationTrigger struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
AutomationID uuid.UUID `db:"automation_id" json:"automation_id"`
|
||||
// Discriminator: webhook or cron. Determines which nullable columns are meaningful.
|
||||
Type ChatAutomationTriggerType `db:"type" json:"type"`
|
||||
// HMAC-SHA256 shared secret for webhook signature verification (X-Hub-Signature-256 header). NULL for cron triggers.
|
||||
WebhookSecret sql.NullString `db:"webhook_secret" json:"webhook_secret"`
|
||||
WebhookSecretKeyID sql.NullString `db:"webhook_secret_key_id" json:"webhook_secret_key_id"`
|
||||
// Standard 5-field cron expression (minute hour dom month dow), with optional CRON_TZ= prefix. NULL for webhook triggers.
|
||||
CronSchedule sql.NullString `db:"cron_schedule" json:"cron_schedule"`
|
||||
// Timestamp of the last successful cron fire. The scheduler computes next = cron.Next(last_triggered_at) and fires when next <= now. NULL means the trigger has never fired. Not used for webhook triggers.
|
||||
LastTriggeredAt sql.NullTime `db:"last_triggered_at" json:"last_triggered_at"`
|
||||
// gjson path-to-value filter conditions evaluated against the incoming webhook payload. All conditions must match for the trigger to fire. NULL or empty means match everything.
|
||||
Filter pqtype.NullRawMessage `db:"filter" json:"filter"`
|
||||
// Maps chat label keys to gjson paths. When a trigger fires, labels are resolved from the payload and used to find an existing chat to continue (by label match) or set on a newly created chat.
|
||||
LabelPaths pqtype.NullRawMessage `db:"label_paths" json:"label_paths"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
|
||||
WorkspaceID uuid.NullUUID `db:"workspace_id" json:"workspace_id"`
|
||||
Title string `db:"title" json:"title"`
|
||||
Status ChatStatus `db:"status" json:"status"`
|
||||
WorkerID uuid.NullUUID `db:"worker_id" json:"worker_id"`
|
||||
StartedAt sql.NullTime `db:"started_at" json:"started_at"`
|
||||
HeartbeatAt sql.NullTime `db:"heartbeat_at" json:"heartbeat_at"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
ParentChatID uuid.NullUUID `db:"parent_chat_id" json:"parent_chat_id"`
|
||||
RootChatID uuid.NullUUID `db:"root_chat_id" json:"root_chat_id"`
|
||||
LastModelConfigID uuid.UUID `db:"last_model_config_id" json:"last_model_config_id"`
|
||||
Archived bool `db:"archived" json:"archived"`
|
||||
LastError sql.NullString `db:"last_error" json:"last_error"`
|
||||
Mode NullChatMode `db:"mode" json:"mode"`
|
||||
MCPServerIDs []uuid.UUID `db:"mcp_server_ids" json:"mcp_server_ids"`
|
||||
Labels StringMap `db:"labels" json:"labels"`
|
||||
}
|
||||
|
||||
type ChatDiffStatus struct {
|
||||
@@ -4767,7 +4485,6 @@ type MCPServerConfig struct {
|
||||
UpdatedBy uuid.NullUUID `db:"updated_by" json:"updated_by"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
ModelIntent bool `db:"model_intent" json:"model_intent"`
|
||||
}
|
||||
|
||||
type MCPServerUserToken struct {
|
||||
|
||||
@@ -54,7 +54,7 @@ type sqlcQuerier interface {
|
||||
ActivityBumpWorkspace(ctx context.Context, arg ActivityBumpWorkspaceParams) error
|
||||
// AllUserIDs returns all UserIDs regardless of user status or deletion.
|
||||
AllUserIDs(ctx context.Context, includeSystem bool) ([]uuid.UUID, error)
|
||||
ArchiveChatByID(ctx context.Context, id uuid.UUID) ([]Chat, error)
|
||||
ArchiveChatByID(ctx context.Context, id uuid.UUID) error
|
||||
// Archiving templates is a soft delete action, so is reversible.
|
||||
// Archiving prevents the version from being used and discovered
|
||||
// by listing.
|
||||
@@ -74,21 +74,10 @@ type sqlcQuerier interface {
|
||||
CleanTailnetCoordinators(ctx context.Context) error
|
||||
CleanTailnetLostPeers(ctx context.Context) error
|
||||
CleanTailnetTunnels(ctx context.Context) error
|
||||
CleanupDeletedMCPServerIDsFromChatAutomations(ctx context.Context) error
|
||||
CleanupDeletedMCPServerIDsFromChats(ctx context.Context) error
|
||||
CountAIBridgeInterceptions(ctx context.Context, arg CountAIBridgeInterceptionsParams) (int64, error)
|
||||
CountAIBridgeSessions(ctx context.Context, arg CountAIBridgeSessionsParams) (int64, error)
|
||||
CountAuditLogs(ctx context.Context, arg CountAuditLogsParams) (int64, error)
|
||||
// Counts new-chat events in the rate-limit window. This count is
|
||||
// approximate under concurrency: concurrent webhook handlers may
|
||||
// each read the same count before any of them insert, so brief
|
||||
// bursts can slightly exceed the configured cap.
|
||||
CountChatAutomationChatCreatesInWindow(ctx context.Context, arg CountChatAutomationChatCreatesInWindowParams) (int64, error)
|
||||
// Counts total message events (creates + continues) in the rate-limit
|
||||
// window. This count is approximate under concurrency: concurrent
|
||||
// webhook handlers may each read the same count before any of them
|
||||
// insert, so brief bursts can slightly exceed the configured cap.
|
||||
CountChatAutomationMessagesInWindow(ctx context.Context, arg CountChatAutomationMessagesInWindowParams) (int64, error)
|
||||
CountConnectionLogs(ctx context.Context, arg CountConnectionLogsParams) (int64, error)
|
||||
// Counts enabled, non-deleted model configs that lack both input and
|
||||
// output pricing in their JSONB options.cost configuration.
|
||||
@@ -111,8 +100,6 @@ type sqlcQuerier interface {
|
||||
// be recreated.
|
||||
DeleteAllWebpushSubscriptions(ctx context.Context) error
|
||||
DeleteApplicationConnectAPIKeysByUserID(ctx context.Context, userID uuid.UUID) error
|
||||
DeleteChatAutomationByID(ctx context.Context, id uuid.UUID) error
|
||||
DeleteChatAutomationTriggerByID(ctx context.Context, id uuid.UUID) error
|
||||
DeleteChatModelConfigByID(ctx context.Context, id uuid.UUID) error
|
||||
DeleteChatProviderByID(ctx context.Context, id uuid.UUID) error
|
||||
DeleteChatQueuedMessage(ctx context.Context, arg DeleteChatQueuedMessageParams) error
|
||||
@@ -210,10 +197,6 @@ type sqlcQuerier interface {
|
||||
GetAPIKeysByUserID(ctx context.Context, arg GetAPIKeysByUserIDParams) ([]APIKey, error)
|
||||
GetAPIKeysLastUsedAfter(ctx context.Context, lastUsed time.Time) ([]APIKey, error)
|
||||
GetActiveAISeatCount(ctx context.Context) (int64, error)
|
||||
// Returns all cron triggers whose parent automation is active or in
|
||||
// preview mode. The scheduler uses this to evaluate which triggers
|
||||
// are due.
|
||||
GetActiveChatAutomationCronTriggers(ctx context.Context) ([]GetActiveChatAutomationCronTriggersRow, error)
|
||||
GetActivePresetPrebuildSchedules(ctx context.Context) ([]TemplateVersionPresetPrebuildSchedule, error)
|
||||
GetActiveUserCount(ctx context.Context, includeSystem bool) (int64, error)
|
||||
GetActiveWorkspaceBuildsByTemplateID(ctx context.Context, templateID uuid.UUID) ([]WorkspaceBuild, error)
|
||||
@@ -240,11 +223,6 @@ type sqlcQuerier interface {
|
||||
// This function returns roles for authorization purposes. Implied member roles
|
||||
// are included.
|
||||
GetAuthorizationUserRoles(ctx context.Context, userID uuid.UUID) (GetAuthorizationUserRolesRow, error)
|
||||
GetChatAutomationByID(ctx context.Context, id uuid.UUID) (ChatAutomation, error)
|
||||
GetChatAutomationEventsByAutomationID(ctx context.Context, arg GetChatAutomationEventsByAutomationIDParams) ([]ChatAutomationEvent, error)
|
||||
GetChatAutomationTriggerByID(ctx context.Context, id uuid.UUID) (ChatAutomationTrigger, error)
|
||||
GetChatAutomationTriggersByAutomationID(ctx context.Context, automationID uuid.UUID) ([]ChatAutomationTrigger, error)
|
||||
GetChatAutomations(ctx context.Context, arg GetChatAutomationsParams) ([]ChatAutomation, error)
|
||||
GetChatByID(ctx context.Context, id uuid.UUID) (Chat, error)
|
||||
GetChatByIDForUpdate(ctx context.Context, id uuid.UUID) (Chat, error)
|
||||
// Per-root-chat cost breakdown for a single user within a date range.
|
||||
@@ -265,14 +243,8 @@ type sqlcQuerier interface {
|
||||
GetChatDiffStatusesByChatIDs(ctx context.Context, chatIds []uuid.UUID) ([]ChatDiffStatus, error)
|
||||
GetChatFileByID(ctx context.Context, id uuid.UUID) (ChatFile, error)
|
||||
GetChatFilesByIDs(ctx context.Context, ids []uuid.UUID) ([]ChatFile, error)
|
||||
// GetChatIncludeDefaultSystemPrompt preserves the legacy default
|
||||
// for deployments created before the explicit include-default toggle.
|
||||
// When the toggle is unset, a non-empty custom prompt implies false;
|
||||
// otherwise the setting defaults to true.
|
||||
GetChatIncludeDefaultSystemPrompt(ctx context.Context) (bool, error)
|
||||
GetChatMessageByID(ctx context.Context, id int64) (ChatMessage, error)
|
||||
GetChatMessagesByChatID(ctx context.Context, arg GetChatMessagesByChatIDParams) ([]ChatMessage, error)
|
||||
GetChatMessagesByChatIDAscPaginated(ctx context.Context, arg GetChatMessagesByChatIDAscPaginatedParams) ([]ChatMessage, error)
|
||||
GetChatMessagesByChatIDDescPaginated(ctx context.Context, arg GetChatMessagesByChatIDDescPaginatedParams) ([]ChatMessage, error)
|
||||
GetChatMessagesForPromptByChatID(ctx context.Context, chatID uuid.UUID) ([]ChatMessage, error)
|
||||
GetChatModelConfigByID(ctx context.Context, id uuid.UUID) (ChatModelConfig, error)
|
||||
@@ -282,12 +254,6 @@ type sqlcQuerier interface {
|
||||
GetChatProviders(ctx context.Context) ([]ChatProvider, error)
|
||||
GetChatQueuedMessages(ctx context.Context, chatID uuid.UUID) ([]ChatQueuedMessage, error)
|
||||
GetChatSystemPrompt(ctx context.Context) (string, error)
|
||||
// GetChatSystemPromptConfig returns both chat system prompt settings in a
|
||||
// single read to avoid torn reads between separate site-config lookups.
|
||||
// The include-default fallback preserves the legacy behavior where a
|
||||
// non-empty custom prompt implied opting out before the explicit toggle
|
||||
// existed.
|
||||
GetChatSystemPromptConfig(ctx context.Context) (GetChatSystemPromptConfigRow, error)
|
||||
// GetChatTemplateAllowlist returns the JSON-encoded template allowlist.
|
||||
// Returns an empty string when no allowlist has been configured (all templates allowed).
|
||||
GetChatTemplateAllowlist(ctx context.Context) (string, error)
|
||||
@@ -297,8 +263,7 @@ type sqlcQuerier interface {
|
||||
// Returns the global TTL for chat workspaces as a Go duration string.
|
||||
// Returns "0s" (disabled) when no value has been configured.
|
||||
GetChatWorkspaceTTL(ctx context.Context) (string, error)
|
||||
GetChats(ctx context.Context, arg GetChatsParams) ([]GetChatsRow, error)
|
||||
GetChatsByWorkspaceIDs(ctx context.Context, ids []uuid.UUID) ([]Chat, error)
|
||||
GetChats(ctx context.Context, arg GetChatsParams) ([]Chat, error)
|
||||
GetConnectionLogsOffset(ctx context.Context, arg GetConnectionLogsOffsetParams) ([]GetConnectionLogsOffsetRow, error)
|
||||
GetCryptoKeyByFeatureAndSequence(ctx context.Context, arg GetCryptoKeyByFeatureAndSequenceParams) (CryptoKey, error)
|
||||
GetCryptoKeys(ctx context.Context) ([]CryptoKey, error)
|
||||
@@ -583,10 +548,6 @@ type sqlcQuerier interface {
|
||||
// inclusive.
|
||||
GetTotalUsageDCManagedAgentsV1(ctx context.Context, arg GetTotalUsageDCManagedAgentsV1Params) (int64, error)
|
||||
GetUnexpiredLicenses(ctx context.Context) ([]License, error)
|
||||
// Returns user IDs from the provided list that are consuming an AI seat.
|
||||
// Filters to active, non-deleted, non-system users to match the canonical
|
||||
// seat count query (GetActiveAISeatCount).
|
||||
GetUserAISeatStates(ctx context.Context, userIds []uuid.UUID) ([]uuid.UUID, error)
|
||||
// GetUserActivityInsights returns the ranking with top active users.
|
||||
// The result can be filtered on template_ids, meaning only user data
|
||||
// from workspaces based on those templates will be included.
|
||||
@@ -718,9 +679,6 @@ type sqlcQuerier interface {
|
||||
InsertAllUsersGroup(ctx context.Context, organizationID uuid.UUID) (Group, error)
|
||||
InsertAuditLog(ctx context.Context, arg InsertAuditLogParams) (AuditLog, error)
|
||||
InsertChat(ctx context.Context, arg InsertChatParams) (Chat, error)
|
||||
InsertChatAutomation(ctx context.Context, arg InsertChatAutomationParams) (ChatAutomation, error)
|
||||
InsertChatAutomationEvent(ctx context.Context, arg InsertChatAutomationEventParams) (ChatAutomationEvent, error)
|
||||
InsertChatAutomationTrigger(ctx context.Context, arg InsertChatAutomationTriggerParams) (ChatAutomationTrigger, error)
|
||||
InsertChatFile(ctx context.Context, arg InsertChatFileParams) (InsertChatFileRow, error)
|
||||
InsertChatMessages(ctx context.Context, arg InsertChatMessagesParams) ([]ChatMessage, error)
|
||||
InsertChatModelConfig(ctx context.Context, arg InsertChatModelConfigParams) (ChatModelConfig, error)
|
||||
@@ -800,23 +758,14 @@ type sqlcQuerier interface {
|
||||
InsertWorkspaceProxy(ctx context.Context, arg InsertWorkspaceProxyParams) (WorkspaceProxy, error)
|
||||
InsertWorkspaceResource(ctx context.Context, arg InsertWorkspaceResourceParams) (WorkspaceResource, error)
|
||||
InsertWorkspaceResourceMetadata(ctx context.Context, arg InsertWorkspaceResourceMetadataParams) ([]WorkspaceResourceMetadatum, error)
|
||||
ListAIBridgeClients(ctx context.Context, arg ListAIBridgeClientsParams) ([]string, error)
|
||||
ListAIBridgeInterceptions(ctx context.Context, arg ListAIBridgeInterceptionsParams) ([]ListAIBridgeInterceptionsRow, error)
|
||||
// Finds all unique AI Bridge interception telemetry summaries combinations
|
||||
// (provider, model, client) in the given timeframe for telemetry reporting.
|
||||
ListAIBridgeInterceptionsTelemetrySummaries(ctx context.Context, arg ListAIBridgeInterceptionsTelemetrySummariesParams) ([]ListAIBridgeInterceptionsTelemetrySummariesRow, error)
|
||||
ListAIBridgeModelThoughtsByInterceptionIDs(ctx context.Context, interceptionIds []uuid.UUID) ([]AIBridgeModelThought, error)
|
||||
ListAIBridgeModels(ctx context.Context, arg ListAIBridgeModelsParams) ([]string, error)
|
||||
// Returns all interceptions belonging to paginated threads within a session.
|
||||
// Threads are paginated by (started_at, thread_id) cursor.
|
||||
ListAIBridgeSessionThreads(ctx context.Context, arg ListAIBridgeSessionThreadsParams) ([]ListAIBridgeSessionThreadsRow, error)
|
||||
// Returns paginated sessions with aggregated metadata, token counts, and
|
||||
// the most recent user prompt. A "session" is a logical grouping of
|
||||
// interceptions that share the same session_id (set by the client).
|
||||
//
|
||||
// Pagination-first strategy: identify the page of sessions cheaply via a
|
||||
// single GROUP BY scan, then do expensive lateral joins (tokens, prompts,
|
||||
// first-interception metadata) only for the ~page-size result set.
|
||||
ListAIBridgeSessions(ctx context.Context, arg ListAIBridgeSessionsParams) ([]ListAIBridgeSessionsRow, error)
|
||||
ListAIBridgeTokenUsagesByInterceptionIDs(ctx context.Context, interceptionIds []uuid.UUID) ([]AIBridgeTokenUsage, error)
|
||||
ListAIBridgeToolUsagesByInterceptionIDs(ctx context.Context, interceptionIds []uuid.UUID) ([]AIBridgeToolUsage, error)
|
||||
@@ -840,17 +789,7 @@ type sqlcQuerier interface {
|
||||
// - Use both to get a specific org member row
|
||||
OrganizationMembers(ctx context.Context, arg OrganizationMembersParams) ([]OrganizationMembersRow, error)
|
||||
PaginatedOrganizationMembers(ctx context.Context, arg PaginatedOrganizationMembersParams) ([]PaginatedOrganizationMembersRow, error)
|
||||
// Under READ COMMITTED, concurrent pin operations for the same
|
||||
// owner may momentarily produce duplicate pin_order values because
|
||||
// each CTE snapshot does not see the other's writes. The next
|
||||
// pin/unpin/reorder operation's ROW_NUMBER() self-heals the
|
||||
// sequence, so this is acceptable.
|
||||
PinChatByID(ctx context.Context, id uuid.UUID) error
|
||||
PopNextQueuedMessage(ctx context.Context, chatID uuid.UUID) (ChatQueuedMessage, error)
|
||||
// Deletes old chat automation events in bounded batches to avoid
|
||||
// long-running locks on high-volume tables. Callers should loop
|
||||
// until zero rows are returned.
|
||||
PurgeOldChatAutomationEvents(ctx context.Context, arg PurgeOldChatAutomationEventsParams) (int64, error)
|
||||
ReduceWorkspaceAgentShareLevelToAuthenticatedByTemplate(ctx context.Context, templateID uuid.UUID) error
|
||||
RegisterWorkspaceProxy(ctx context.Context, arg RegisterWorkspaceProxyParams) (WorkspaceProxy, error)
|
||||
RemoveUserFromGroups(ctx context.Context, arg RemoveUserFromGroupsParams) ([]uuid.UUID, error)
|
||||
@@ -873,41 +812,24 @@ type sqlcQuerier interface {
|
||||
// This must be called from within a transaction. The lock will be automatically
|
||||
// released when the transaction ends.
|
||||
TryAcquireLock(ctx context.Context, pgTryAdvisoryXactLock int64) (bool, error)
|
||||
UnarchiveChatByID(ctx context.Context, id uuid.UUID) ([]Chat, error)
|
||||
UnarchiveChatByID(ctx context.Context, id uuid.UUID) error
|
||||
// This will always work regardless of the current state of the template version.
|
||||
UnarchiveTemplateVersion(ctx context.Context, arg UnarchiveTemplateVersionParams) error
|
||||
UnfavoriteWorkspace(ctx context.Context, id uuid.UUID) error
|
||||
UnpinChatByID(ctx context.Context, id uuid.UUID) error
|
||||
UnsetDefaultChatModelConfigs(ctx context.Context) error
|
||||
UpdateAIBridgeInterceptionEnded(ctx context.Context, arg UpdateAIBridgeInterceptionEndedParams) (AIBridgeInterception, error)
|
||||
UpdateAPIKeyByID(ctx context.Context, arg UpdateAPIKeyByIDParams) error
|
||||
UpdateChatAutomation(ctx context.Context, arg UpdateChatAutomationParams) (ChatAutomation, error)
|
||||
UpdateChatAutomationTrigger(ctx context.Context, arg UpdateChatAutomationTriggerParams) (ChatAutomationTrigger, error)
|
||||
UpdateChatAutomationTriggerLastTriggeredAt(ctx context.Context, arg UpdateChatAutomationTriggerLastTriggeredAtParams) error
|
||||
UpdateChatAutomationTriggerWebhookSecret(ctx context.Context, arg UpdateChatAutomationTriggerWebhookSecretParams) (ChatAutomationTrigger, error)
|
||||
UpdateChatBuildAgentBinding(ctx context.Context, arg UpdateChatBuildAgentBindingParams) (Chat, error)
|
||||
UpdateChatByID(ctx context.Context, arg UpdateChatByIDParams) (Chat, error)
|
||||
// Bumps the heartbeat timestamp for a running chat so that other
|
||||
// replicas know the worker is still alive.
|
||||
UpdateChatHeartbeat(ctx context.Context, arg UpdateChatHeartbeatParams) (int64, error)
|
||||
UpdateChatLabelsByID(ctx context.Context, arg UpdateChatLabelsByIDParams) (Chat, error)
|
||||
// Updates the cached injected context parts (AGENTS.md +
|
||||
// skills) on the chat row. Called only when context changes
|
||||
// (first workspace attach or agent change). updated_at is
|
||||
// intentionally not touched to avoid reordering the chat list.
|
||||
UpdateChatLastInjectedContext(ctx context.Context, arg UpdateChatLastInjectedContextParams) (Chat, error)
|
||||
UpdateChatLastModelConfigByID(ctx context.Context, arg UpdateChatLastModelConfigByIDParams) (Chat, error)
|
||||
// Updates the last read message ID for a chat. This is used to track
|
||||
// which messages the owner has seen, enabling unread indicators.
|
||||
UpdateChatLastReadMessageID(ctx context.Context, arg UpdateChatLastReadMessageIDParams) error
|
||||
UpdateChatMCPServerIDs(ctx context.Context, arg UpdateChatMCPServerIDsParams) (Chat, error)
|
||||
UpdateChatMessageByID(ctx context.Context, arg UpdateChatMessageByIDParams) (ChatMessage, error)
|
||||
UpdateChatModelConfig(ctx context.Context, arg UpdateChatModelConfigParams) (ChatModelConfig, error)
|
||||
UpdateChatPinOrder(ctx context.Context, arg UpdateChatPinOrderParams) error
|
||||
UpdateChatProvider(ctx context.Context, arg UpdateChatProviderParams) (ChatProvider, error)
|
||||
UpdateChatStatus(ctx context.Context, arg UpdateChatStatusParams) (Chat, error)
|
||||
UpdateChatStatusPreserveUpdatedAt(ctx context.Context, arg UpdateChatStatusPreserveUpdatedAtParams) (Chat, error)
|
||||
UpdateChatWorkspaceBinding(ctx context.Context, arg UpdateChatWorkspaceBindingParams) (Chat, error)
|
||||
UpdateChatWorkspace(ctx context.Context, arg UpdateChatWorkspaceParams) (Chat, error)
|
||||
UpdateCryptoKeyDeletesAt(ctx context.Context, arg UpdateCryptoKeyDeletesAtParams) (CryptoKey, error)
|
||||
UpdateCustomRole(ctx context.Context, arg UpdateCustomRoleParams) (CustomRole, error)
|
||||
UpdateExternalAuthLink(ctx context.Context, arg UpdateExternalAuthLinkParams) (ExternalAuthLink, error)
|
||||
@@ -1014,7 +936,6 @@ type sqlcQuerier interface {
|
||||
UpsertChatDesktopEnabled(ctx context.Context, enableDesktop bool) error
|
||||
UpsertChatDiffStatus(ctx context.Context, arg UpsertChatDiffStatusParams) (ChatDiffStatus, error)
|
||||
UpsertChatDiffStatusReference(ctx context.Context, arg UpsertChatDiffStatusReferenceParams) (ChatDiffStatus, error)
|
||||
UpsertChatIncludeDefaultSystemPrompt(ctx context.Context, includeDefaultSystemPrompt bool) error
|
||||
UpsertChatSystemPrompt(ctx context.Context, value string) error
|
||||
UpsertChatTemplateAllowlist(ctx context.Context, templateAllowlist string) error
|
||||
UpsertChatUsageLimitConfig(ctx context.Context, arg UpsertChatUsageLimitConfigParams) (ChatUsageLimitConfig, error)
|
||||
|
||||
+12
-315
@@ -1251,12 +1251,8 @@ func TestGetAuthorizedChats(t *testing.T) {
|
||||
owner := dbgen.User(t, db, database.User{
|
||||
RBACRoles: []string{rbac.RoleOwner().String()},
|
||||
})
|
||||
member := dbgen.User(t, db, database.User{
|
||||
RBACRoles: pq.StringArray{rbac.RoleAgentsAccess().String()},
|
||||
})
|
||||
secondMember := dbgen.User(t, db, database.User{
|
||||
RBACRoles: pq.StringArray{rbac.RoleAgentsAccess().String()},
|
||||
})
|
||||
member := dbgen.User(t, db, database.User{})
|
||||
secondMember := dbgen.User(t, db, database.User{})
|
||||
|
||||
// Create FK dependencies: a chat provider and model config.
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
@@ -1315,7 +1311,7 @@ func TestGetAuthorizedChats(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Len(t, memberRows, 2)
|
||||
for _, row := range memberRows {
|
||||
require.Equal(t, member.ID, row.Chat.OwnerID, "member should only see own chats")
|
||||
require.Equal(t, member.ID, row.OwnerID, "member should only see own chats")
|
||||
}
|
||||
|
||||
// Owner should see at least the 5 pre-created chats (site-wide
|
||||
@@ -1385,7 +1381,7 @@ func TestGetAuthorizedChats(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Len(t, memberRows, 2)
|
||||
for _, row := range memberRows {
|
||||
require.Equal(t, member.ID, row.Chat.OwnerID, "member should only see own chats")
|
||||
require.Equal(t, member.ID, row.OwnerID, "member should only see own chats")
|
||||
}
|
||||
|
||||
// As owner: should see at least the 5 pre-created chats.
|
||||
@@ -1411,9 +1407,7 @@ func TestGetAuthorizedChats(t *testing.T) {
|
||||
|
||||
// Use a dedicated user for pagination to avoid interference
|
||||
// with the other parallel subtests.
|
||||
paginationUser := dbgen.User(t, db, database.User{
|
||||
RBACRoles: pq.StringArray{rbac.RoleAgentsAccess().String()},
|
||||
})
|
||||
paginationUser := dbgen.User(t, db, database.User{})
|
||||
for i := range 7 {
|
||||
_, err := db.InsertChat(ctx, database.InsertChatParams{
|
||||
OwnerID: paginationUser.ID,
|
||||
@@ -1435,13 +1429,13 @@ func TestGetAuthorizedChats(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Len(t, page1, 2)
|
||||
for _, row := range page1 {
|
||||
require.Equal(t, paginationUser.ID, row.Chat.OwnerID, "paginated results must belong to pagination user")
|
||||
require.Equal(t, paginationUser.ID, row.OwnerID, "paginated results must belong to pagination user")
|
||||
}
|
||||
|
||||
// Fetch remaining pages and collect all chat IDs.
|
||||
allIDs := make(map[uuid.UUID]struct{})
|
||||
for _, row := range page1 {
|
||||
allIDs[row.Chat.ID] = struct{}{}
|
||||
allIDs[row.ID] = struct{}{}
|
||||
}
|
||||
offset := int32(2)
|
||||
for {
|
||||
@@ -1451,8 +1445,8 @@ func TestGetAuthorizedChats(t *testing.T) {
|
||||
}, preparedMember)
|
||||
require.NoError(t, err)
|
||||
for _, row := range page {
|
||||
require.Equal(t, paginationUser.ID, row.Chat.OwnerID, "paginated results must belong to pagination user")
|
||||
allIDs[row.Chat.ID] = struct{}{}
|
||||
require.Equal(t, paginationUser.ID, row.OwnerID, "paginated results must belong to pagination user")
|
||||
allIDs[row.ID] = struct{}{}
|
||||
}
|
||||
if len(page) < 2 {
|
||||
break
|
||||
@@ -10493,186 +10487,6 @@ func TestGetPRInsights(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestChatPinOrderQueries(t *testing.T) {
|
||||
t.Parallel()
|
||||
if testing.Short() {
|
||||
t.SkipNow()
|
||||
}
|
||||
|
||||
setup := func(t *testing.T) (context.Context, database.Store, uuid.UUID, uuid.UUID) {
|
||||
t.Helper()
|
||||
|
||||
db, _ := dbtestutil.NewDB(t)
|
||||
owner := dbgen.User(t, db, database.User{})
|
||||
|
||||
// Use background context for fixture setup so the
|
||||
// timed test context doesn't tick during DB init.
|
||||
bg := context.Background()
|
||||
_, err := db.InsertChatProvider(bg, database.InsertChatProviderParams{
|
||||
Provider: "openai",
|
||||
DisplayName: "OpenAI",
|
||||
APIKey: "test-key",
|
||||
Enabled: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
modelCfg, err := db.InsertChatModelConfig(bg, database.InsertChatModelConfigParams{
|
||||
Provider: "openai",
|
||||
Model: "test-model",
|
||||
DisplayName: "Test Model",
|
||||
CreatedBy: uuid.NullUUID{UUID: owner.ID, Valid: true},
|
||||
UpdatedBy: uuid.NullUUID{UUID: owner.ID, Valid: true},
|
||||
Enabled: true,
|
||||
IsDefault: true,
|
||||
ContextLimit: 128000,
|
||||
CompressionThreshold: 80,
|
||||
Options: json.RawMessage(`{}`),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitMedium)
|
||||
return ctx, db, owner.ID, modelCfg.ID
|
||||
}
|
||||
|
||||
createChat := func(t *testing.T, ctx context.Context, db database.Store, ownerID, modelCfgID uuid.UUID, title string) database.Chat {
|
||||
t.Helper()
|
||||
|
||||
chat, err := db.InsertChat(ctx, database.InsertChatParams{
|
||||
OwnerID: ownerID,
|
||||
LastModelConfigID: modelCfgID,
|
||||
Title: title,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return chat
|
||||
}
|
||||
|
||||
requirePinOrders := func(t *testing.T, ctx context.Context, db database.Store, want map[uuid.UUID]int32) {
|
||||
t.Helper()
|
||||
|
||||
for chatID, wantPinOrder := range want {
|
||||
chat, err := db.GetChatByID(ctx, chatID)
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, wantPinOrder, chat.PinOrder)
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("PinChatByIDAppendsWithinOwner", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, db, ownerID, modelCfgID := setup(t)
|
||||
first := createChat(t, ctx, db, ownerID, modelCfgID, "first")
|
||||
second := createChat(t, ctx, db, ownerID, modelCfgID, "second")
|
||||
third := createChat(t, ctx, db, ownerID, modelCfgID, "third")
|
||||
|
||||
otherOwner := dbgen.User(t, db, database.User{})
|
||||
other := createChat(t, ctx, db, otherOwner.ID, modelCfgID, "other-owner")
|
||||
|
||||
require.NoError(t, db.PinChatByID(ctx, other.ID))
|
||||
require.NoError(t, db.PinChatByID(ctx, first.ID))
|
||||
require.NoError(t, db.PinChatByID(ctx, second.ID))
|
||||
require.NoError(t, db.PinChatByID(ctx, third.ID))
|
||||
|
||||
requirePinOrders(t, ctx, db, map[uuid.UUID]int32{
|
||||
first.ID: 1,
|
||||
second.ID: 2,
|
||||
third.ID: 3,
|
||||
other.ID: 1,
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("UpdateChatPinOrderShiftsNeighborsAndClamps", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, db, ownerID, modelCfgID := setup(t)
|
||||
first := createChat(t, ctx, db, ownerID, modelCfgID, "first")
|
||||
second := createChat(t, ctx, db, ownerID, modelCfgID, "second")
|
||||
third := createChat(t, ctx, db, ownerID, modelCfgID, "third")
|
||||
|
||||
for _, chat := range []database.Chat{first, second, third} {
|
||||
require.NoError(t, db.PinChatByID(ctx, chat.ID))
|
||||
}
|
||||
|
||||
require.NoError(t, db.UpdateChatPinOrder(ctx, database.UpdateChatPinOrderParams{
|
||||
ID: third.ID,
|
||||
PinOrder: 1,
|
||||
}))
|
||||
requirePinOrders(t, ctx, db, map[uuid.UUID]int32{
|
||||
first.ID: 2,
|
||||
second.ID: 3,
|
||||
third.ID: 1,
|
||||
})
|
||||
|
||||
require.NoError(t, db.UpdateChatPinOrder(ctx, database.UpdateChatPinOrderParams{
|
||||
ID: third.ID,
|
||||
PinOrder: 99,
|
||||
}))
|
||||
requirePinOrders(t, ctx, db, map[uuid.UUID]int32{
|
||||
first.ID: 1,
|
||||
second.ID: 2,
|
||||
third.ID: 3,
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("UnpinChatByIDCompactsPinnedChats", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, db, ownerID, modelCfgID := setup(t)
|
||||
first := createChat(t, ctx, db, ownerID, modelCfgID, "first")
|
||||
second := createChat(t, ctx, db, ownerID, modelCfgID, "second")
|
||||
third := createChat(t, ctx, db, ownerID, modelCfgID, "third")
|
||||
|
||||
for _, chat := range []database.Chat{first, second, third} {
|
||||
require.NoError(t, db.PinChatByID(ctx, chat.ID))
|
||||
}
|
||||
|
||||
require.NoError(t, db.UnpinChatByID(ctx, second.ID))
|
||||
requirePinOrders(t, ctx, db, map[uuid.UUID]int32{
|
||||
first.ID: 1,
|
||||
second.ID: 0,
|
||||
third.ID: 2,
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("ArchiveClearsPinAndExcludesFromRanking", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx, db, ownerID, modelCfgID := setup(t)
|
||||
first := createChat(t, ctx, db, ownerID, modelCfgID, "first")
|
||||
second := createChat(t, ctx, db, ownerID, modelCfgID, "second")
|
||||
third := createChat(t, ctx, db, ownerID, modelCfgID, "third")
|
||||
|
||||
for _, chat := range []database.Chat{first, second, third} {
|
||||
require.NoError(t, db.PinChatByID(ctx, chat.ID))
|
||||
}
|
||||
|
||||
// Archive the middle pin.
|
||||
_, err := db.ArchiveChatByID(ctx, second.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Archived chat should have pin_order cleared. Remaining
|
||||
// pins keep their original positions; the next mutation
|
||||
// compacts via ROW_NUMBER().
|
||||
requirePinOrders(t, ctx, db, map[uuid.UUID]int32{
|
||||
first.ID: 1,
|
||||
second.ID: 0,
|
||||
third.ID: 3,
|
||||
})
|
||||
|
||||
// Reorder among remaining active pins — archived chat
|
||||
// should not interfere with position calculation.
|
||||
require.NoError(t, db.UpdateChatPinOrder(ctx, database.UpdateChatPinOrderParams{
|
||||
ID: third.ID,
|
||||
PinOrder: 1,
|
||||
}))
|
||||
// After reorder, ROW_NUMBER() compacts the sequence.
|
||||
requirePinOrders(t, ctx, db, map[uuid.UUID]int32{
|
||||
first.ID: 2,
|
||||
second.ID: 0,
|
||||
third.ID: 1,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestChatLabels(t *testing.T) {
|
||||
t.Parallel()
|
||||
if testing.Short() {
|
||||
@@ -10856,7 +10670,7 @@ func TestChatLabels(t *testing.T) {
|
||||
|
||||
titles := make([]string, 0, len(results))
|
||||
for _, c := range results {
|
||||
titles = append(titles, c.Chat.Title)
|
||||
titles = append(titles, c.Title)
|
||||
}
|
||||
require.Contains(t, titles, "filter-a")
|
||||
require.Contains(t, titles, "filter-b")
|
||||
@@ -10874,7 +10688,8 @@ func TestChatLabels(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, results, 1)
|
||||
require.Equal(t, "filter-a", results[0].Chat.Title)
|
||||
require.Equal(t, "filter-a", results[0].Title)
|
||||
|
||||
// No filter — should return all chats for this owner.
|
||||
allChats, err := db.GetChats(ctx, database.GetChatsParams{
|
||||
OwnerID: owner.ID,
|
||||
@@ -10883,121 +10698,3 @@ func TestChatLabels(t *testing.T) {
|
||||
require.GreaterOrEqual(t, len(allChats), 3)
|
||||
})
|
||||
}
|
||||
|
||||
func TestChatHasUnread(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
store, _ := dbtestutil.NewDB(t)
|
||||
ctx := context.Background()
|
||||
|
||||
dbgen.Organization(t, store, database.Organization{})
|
||||
user := dbgen.User(t, store, database.User{})
|
||||
|
||||
_, err := store.InsertChatProvider(ctx, database.InsertChatProviderParams{
|
||||
Provider: "openai",
|
||||
DisplayName: "OpenAI",
|
||||
APIKey: "test-key",
|
||||
Enabled: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
modelCfg, err := store.InsertChatModelConfig(ctx, database.InsertChatModelConfigParams{
|
||||
Provider: "openai",
|
||||
Model: "test-model-" + uuid.NewString(),
|
||||
DisplayName: "Test Model",
|
||||
CreatedBy: uuid.NullUUID{UUID: user.ID, Valid: true},
|
||||
UpdatedBy: uuid.NullUUID{UUID: user.ID, Valid: true},
|
||||
Enabled: true,
|
||||
IsDefault: true,
|
||||
ContextLimit: 128000,
|
||||
CompressionThreshold: 80,
|
||||
Options: json.RawMessage(`{}`),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
chat, err := store.InsertChat(ctx, database.InsertChatParams{
|
||||
OwnerID: user.ID,
|
||||
LastModelConfigID: modelCfg.ID,
|
||||
Title: "test-chat-" + uuid.NewString(),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
getHasUnread := func() bool {
|
||||
rows, err := store.GetChats(ctx, database.GetChatsParams{
|
||||
OwnerID: user.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
for _, row := range rows {
|
||||
if row.Chat.ID == chat.ID {
|
||||
return row.HasUnread
|
||||
}
|
||||
}
|
||||
t.Fatal("chat not found in GetChats result")
|
||||
return false
|
||||
}
|
||||
|
||||
// New chat with no messages: not unread.
|
||||
require.False(t, getHasUnread(), "new chat with no messages should not be unread")
|
||||
|
||||
// Helper to insert a single chat message.
|
||||
insertMsg := func(role database.ChatMessageRole, text string) {
|
||||
t.Helper()
|
||||
_, err := store.InsertChatMessages(ctx, database.InsertChatMessagesParams{
|
||||
ChatID: chat.ID,
|
||||
CreatedBy: []uuid.UUID{user.ID},
|
||||
ModelConfigID: []uuid.UUID{modelCfg.ID},
|
||||
Role: []database.ChatMessageRole{role},
|
||||
Content: []string{fmt.Sprintf(`[{"type":"text","text":%q}]`, text)},
|
||||
ContentVersion: []int16{0},
|
||||
Visibility: []database.ChatMessageVisibility{database.ChatMessageVisibilityBoth},
|
||||
InputTokens: []int64{0},
|
||||
OutputTokens: []int64{0},
|
||||
TotalTokens: []int64{0},
|
||||
ReasoningTokens: []int64{0},
|
||||
CacheCreationTokens: []int64{0},
|
||||
CacheReadTokens: []int64{0},
|
||||
ContextLimit: []int64{0},
|
||||
Compressed: []bool{false},
|
||||
TotalCostMicros: []int64{0},
|
||||
RuntimeMs: []int64{0},
|
||||
ProviderResponseID: []string{""},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// Insert an assistant message: becomes unread.
|
||||
insertMsg(database.ChatMessageRoleAssistant, "hello")
|
||||
require.True(t, getHasUnread(), "chat with unread assistant message should be unread")
|
||||
|
||||
// Mark as read: no longer unread.
|
||||
lastMsg, err := store.GetLastChatMessageByRole(ctx, database.GetLastChatMessageByRoleParams{
|
||||
ChatID: chat.ID,
|
||||
Role: database.ChatMessageRoleAssistant,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
err = store.UpdateChatLastReadMessageID(ctx, database.UpdateChatLastReadMessageIDParams{
|
||||
ID: chat.ID,
|
||||
LastReadMessageID: lastMsg.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.False(t, getHasUnread(), "chat should not be unread after marking as read")
|
||||
|
||||
// Insert another assistant message: becomes unread again.
|
||||
insertMsg(database.ChatMessageRoleAssistant, "new message")
|
||||
require.True(t, getHasUnread(), "new assistant message after read should be unread")
|
||||
|
||||
// Mark as read again, then verify user messages don't
|
||||
// trigger unread.
|
||||
lastMsg, err = store.GetLastChatMessageByRole(ctx, database.GetLastChatMessageByRoleParams{
|
||||
ChatID: chat.ID,
|
||||
Role: database.ChatMessageRoleAssistant,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
err = store.UpdateChatLastReadMessageID(ctx, database.UpdateChatLastReadMessageIDParams{
|
||||
ID: chat.ID,
|
||||
LastReadMessageID: lastMsg.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
insertMsg(database.ChatMessageRoleUser, "user msg")
|
||||
require.False(t, getHasUnread(), "user messages should not trigger unread")
|
||||
}
|
||||
|
||||
+128
-2026
File diff suppressed because it is too large
Load Diff
@@ -454,91 +454,95 @@ WHERE
|
||||
-- Returns paginated sessions with aggregated metadata, token counts, and
|
||||
-- the most recent user prompt. A "session" is a logical grouping of
|
||||
-- interceptions that share the same session_id (set by the client).
|
||||
--
|
||||
-- Pagination-first strategy: identify the page of sessions cheaply via a
|
||||
-- single GROUP BY scan, then do expensive lateral joins (tokens, prompts,
|
||||
-- first-interception metadata) only for the ~page-size result set.
|
||||
WITH cursor_pos AS (
|
||||
-- Resolve the cursor's started_at once, outside the HAVING clause,
|
||||
-- so the planner cannot accidentally re-evaluate it per group.
|
||||
SELECT MIN(aibridge_interceptions.started_at) AS started_at
|
||||
FROM aibridge_interceptions
|
||||
WHERE aibridge_interceptions.session_id = @after_session_id AND aibridge_interceptions.ended_at IS NOT NULL
|
||||
),
|
||||
session_page AS (
|
||||
-- Paginate at the session level first; only cheap aggregates here.
|
||||
WITH filtered_interceptions AS (
|
||||
SELECT
|
||||
ai.session_id,
|
||||
ai.initiator_id,
|
||||
MIN(ai.started_at) AS started_at,
|
||||
MAX(ai.ended_at) AS ended_at,
|
||||
COUNT(*) FILTER (WHERE ai.thread_root_id IS NULL) AS threads
|
||||
aibridge_interceptions.*
|
||||
FROM
|
||||
aibridge_interceptions ai
|
||||
aibridge_interceptions
|
||||
WHERE
|
||||
-- Remove inflight interceptions (ones which lack an ended_at value).
|
||||
ai.ended_at IS NOT NULL
|
||||
aibridge_interceptions.ended_at IS NOT NULL
|
||||
-- Filter by time frame
|
||||
AND CASE
|
||||
WHEN @started_after::timestamptz != '0001-01-01 00:00:00+00'::timestamptz THEN ai.started_at >= @started_after::timestamptz
|
||||
WHEN @started_after::timestamptz != '0001-01-01 00:00:00+00'::timestamptz THEN aibridge_interceptions.started_at >= @started_after::timestamptz
|
||||
ELSE true
|
||||
END
|
||||
AND CASE
|
||||
WHEN @started_before::timestamptz != '0001-01-01 00:00:00+00'::timestamptz THEN ai.started_at <= @started_before::timestamptz
|
||||
WHEN @started_before::timestamptz != '0001-01-01 00:00:00+00'::timestamptz THEN aibridge_interceptions.started_at <= @started_before::timestamptz
|
||||
ELSE true
|
||||
END
|
||||
-- Filter initiator_id
|
||||
AND CASE
|
||||
WHEN @initiator_id::uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN ai.initiator_id = @initiator_id::uuid
|
||||
WHEN @initiator_id::uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN aibridge_interceptions.initiator_id = @initiator_id::uuid
|
||||
ELSE true
|
||||
END
|
||||
-- Filter provider
|
||||
AND CASE
|
||||
WHEN @provider::text != '' THEN ai.provider = @provider::text
|
||||
WHEN @provider::text != '' THEN aibridge_interceptions.provider = @provider::text
|
||||
ELSE true
|
||||
END
|
||||
-- Filter model
|
||||
AND CASE
|
||||
WHEN @model::text != '' THEN ai.model = @model::text
|
||||
WHEN @model::text != '' THEN aibridge_interceptions.model = @model::text
|
||||
ELSE true
|
||||
END
|
||||
-- Filter client
|
||||
AND CASE
|
||||
WHEN @client::text != '' THEN COALESCE(ai.client, 'Unknown') = @client::text
|
||||
WHEN @client::text != '' THEN COALESCE(aibridge_interceptions.client, 'Unknown') = @client::text
|
||||
ELSE true
|
||||
END
|
||||
-- Filter session_id
|
||||
AND CASE
|
||||
WHEN @session_id::text != '' THEN ai.session_id = @session_id::text
|
||||
WHEN @session_id::text != '' THEN aibridge_interceptions.session_id = @session_id::text
|
||||
ELSE true
|
||||
END
|
||||
-- Authorize Filter clause will be injected below in ListAuthorizedAIBridgeSessions
|
||||
-- @authorize_filter
|
||||
),
|
||||
session_tokens AS (
|
||||
-- Aggregate token usage across all interceptions in each session.
|
||||
-- Group by (session_id, initiator_id) to avoid merging sessions from
|
||||
-- different users who happen to share the same client_session_id.
|
||||
SELECT
|
||||
fi.session_id,
|
||||
fi.initiator_id,
|
||||
COALESCE(SUM(tu.input_tokens), 0)::bigint AS input_tokens,
|
||||
COALESCE(SUM(tu.output_tokens), 0)::bigint AS output_tokens
|
||||
-- TODO: add extra token types once https://github.com/coder/aibridge/issues/150 lands.
|
||||
FROM
|
||||
filtered_interceptions fi
|
||||
LEFT JOIN
|
||||
aibridge_token_usages tu ON fi.id = tu.interception_id
|
||||
GROUP BY
|
||||
ai.session_id, ai.initiator_id
|
||||
HAVING
|
||||
-- Cursor pagination: uses a composite (started_at, session_id)
|
||||
-- cursor to support keyset pagination. The less-than comparison
|
||||
-- matches the DESC sort order so rows after the cursor come
|
||||
-- later in results. The cursor value comes from cursor_pos to
|
||||
-- guarantee single evaluation.
|
||||
CASE
|
||||
WHEN @after_session_id::text != '' THEN (
|
||||
(MIN(ai.started_at), ai.session_id) < (
|
||||
(SELECT started_at FROM cursor_pos),
|
||||
@after_session_id::text
|
||||
)
|
||||
)
|
||||
ELSE true
|
||||
END
|
||||
ORDER BY
|
||||
MIN(ai.started_at) DESC,
|
||||
ai.session_id DESC
|
||||
LIMIT COALESCE(NULLIF(@limit_::integer, 0), 100)
|
||||
OFFSET @offset_
|
||||
fi.session_id, fi.initiator_id
|
||||
),
|
||||
session_root AS (
|
||||
-- Build one summary row per session. Group by (session_id, initiator_id)
|
||||
-- to avoid merging sessions from different users who happen to share the
|
||||
-- same client_session_id. The ARRAY_AGG with ORDER BY picks values from
|
||||
-- the chronologically first interception for fields that should represent
|
||||
-- the session as a whole (client, metadata). Threads are counted as
|
||||
-- distinct root interception IDs: an interception with a NULL
|
||||
-- thread_root_id is itself a thread root.
|
||||
SELECT
|
||||
fi.session_id,
|
||||
fi.initiator_id,
|
||||
(ARRAY_AGG(fi.client ORDER BY fi.started_at, fi.id))[1] AS client,
|
||||
(ARRAY_AGG(fi.metadata ORDER BY fi.started_at, fi.id))[1] AS metadata,
|
||||
ARRAY_AGG(DISTINCT fi.provider ORDER BY fi.provider) AS providers,
|
||||
ARRAY_AGG(DISTINCT fi.model ORDER BY fi.model) AS models,
|
||||
MIN(fi.started_at) AS started_at,
|
||||
MAX(fi.ended_at) AS ended_at,
|
||||
COUNT(DISTINCT COALESCE(fi.thread_root_id, fi.id)) AS threads,
|
||||
-- Collect IDs for lateral prompt lookup.
|
||||
ARRAY_AGG(fi.id) AS interception_ids
|
||||
FROM
|
||||
filtered_interceptions fi
|
||||
GROUP BY
|
||||
fi.session_id, fi.initiator_id
|
||||
)
|
||||
SELECT
|
||||
sp.session_id,
|
||||
sr.session_id,
|
||||
visible_users.id AS user_id,
|
||||
visible_users.username AS user_username,
|
||||
visible_users.name AS user_name,
|
||||
@@ -547,114 +551,47 @@ SELECT
|
||||
sr.models::text[] AS models,
|
||||
COALESCE(sr.client, '')::varchar(64) AS client,
|
||||
sr.metadata::jsonb AS metadata,
|
||||
sp.started_at::timestamptz AS started_at,
|
||||
sp.ended_at::timestamptz AS ended_at,
|
||||
sp.threads,
|
||||
sr.started_at::timestamptz AS started_at,
|
||||
sr.ended_at::timestamptz AS ended_at,
|
||||
sr.threads,
|
||||
COALESCE(st.input_tokens, 0)::bigint AS input_tokens,
|
||||
COALESCE(st.output_tokens, 0)::bigint AS output_tokens,
|
||||
COALESCE(slp.prompt, '') AS last_prompt
|
||||
FROM
|
||||
session_page sp
|
||||
session_root sr
|
||||
JOIN
|
||||
visible_users ON visible_users.id = sp.initiator_id
|
||||
visible_users ON visible_users.id = sr.initiator_id
|
||||
LEFT JOIN
|
||||
session_tokens st ON st.session_id = sr.session_id AND st.initiator_id = sr.initiator_id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
(ARRAY_AGG(ai.client ORDER BY ai.started_at, ai.id))[1] AS client,
|
||||
(ARRAY_AGG(ai.metadata ORDER BY ai.started_at, ai.id))[1] AS metadata,
|
||||
ARRAY_AGG(DISTINCT ai.provider ORDER BY ai.provider) AS providers,
|
||||
ARRAY_AGG(DISTINCT ai.model ORDER BY ai.model) AS models,
|
||||
ARRAY_AGG(ai.id) AS interception_ids
|
||||
FROM aibridge_interceptions ai
|
||||
WHERE ai.session_id = sp.session_id
|
||||
AND ai.initiator_id = sp.initiator_id
|
||||
AND ai.ended_at IS NOT NULL
|
||||
) sr ON true
|
||||
LEFT JOIN LATERAL (
|
||||
-- Aggregate tokens only for this session's interceptions.
|
||||
SELECT
|
||||
COALESCE(SUM(tu.input_tokens), 0)::bigint AS input_tokens,
|
||||
COALESCE(SUM(tu.output_tokens), 0)::bigint AS output_tokens
|
||||
FROM aibridge_token_usages tu
|
||||
WHERE tu.interception_id = ANY(sr.interception_ids)
|
||||
) st ON true
|
||||
LEFT JOIN LATERAL (
|
||||
-- Fetch only the most recent user prompt across all interceptions
|
||||
-- in the session.
|
||||
-- Lateral join to efficiently fetch only the most recent user prompt
|
||||
-- across all interceptions in the session, avoiding a full aggregation.
|
||||
SELECT up.prompt
|
||||
FROM aibridge_user_prompts up
|
||||
WHERE up.interception_id = ANY(sr.interception_ids)
|
||||
ORDER BY up.created_at DESC, up.id DESC
|
||||
LIMIT 1
|
||||
) slp ON true
|
||||
ORDER BY
|
||||
sp.started_at DESC,
|
||||
sp.session_id DESC
|
||||
;
|
||||
|
||||
-- name: ListAIBridgeSessionThreads :many
|
||||
-- Returns all interceptions belonging to paginated threads within a session.
|
||||
-- Threads are paginated by (started_at, thread_id) cursor.
|
||||
WITH paginated_threads AS (
|
||||
SELECT
|
||||
-- Find thread root interceptions (thread_root_id IS NULL), apply cursor
|
||||
-- pagination, and return the page.
|
||||
aibridge_interceptions.id AS thread_id,
|
||||
aibridge_interceptions.started_at
|
||||
FROM
|
||||
aibridge_interceptions
|
||||
WHERE
|
||||
aibridge_interceptions.session_id = @session_id::text
|
||||
AND aibridge_interceptions.ended_at IS NOT NULL
|
||||
AND aibridge_interceptions.thread_root_id IS NULL
|
||||
-- Pagination cursor.
|
||||
AND (@after_id::uuid = '00000000-0000-0000-0000-000000000000'::uuid OR
|
||||
(aibridge_interceptions.started_at, aibridge_interceptions.id) > (
|
||||
(SELECT started_at FROM aibridge_interceptions ai2 WHERE ai2.id = @after_id),
|
||||
@after_id::uuid
|
||||
WHERE
|
||||
-- Cursor pagination: uses a composite (started_at, session_id) cursor
|
||||
-- to support keyset pagination. The less-than comparison matches the
|
||||
-- DESC sort order so that rows after the cursor come later in results.
|
||||
CASE
|
||||
WHEN @after_session_id::text != '' THEN (
|
||||
(sr.started_at, sr.session_id) < (
|
||||
(SELECT started_at FROM session_root WHERE session_id = @after_session_id),
|
||||
@after_session_id::text
|
||||
)
|
||||
)
|
||||
AND (@before_id::uuid = '00000000-0000-0000-0000-000000000000'::uuid OR
|
||||
(aibridge_interceptions.started_at, aibridge_interceptions.id) < (
|
||||
(SELECT started_at FROM aibridge_interceptions ai2 WHERE ai2.id = @before_id),
|
||||
@before_id::uuid
|
||||
)
|
||||
)
|
||||
-- @authorize_filter
|
||||
ORDER BY
|
||||
aibridge_interceptions.started_at ASC,
|
||||
aibridge_interceptions.id ASC
|
||||
LIMIT COALESCE(NULLIF(@limit_::integer, 0), 50)
|
||||
)
|
||||
SELECT
|
||||
COALESCE(aibridge_interceptions.thread_root_id, aibridge_interceptions.id) AS thread_id,
|
||||
sqlc.embed(aibridge_interceptions)
|
||||
FROM
|
||||
aibridge_interceptions
|
||||
JOIN
|
||||
paginated_threads pt
|
||||
ON pt.thread_id = COALESCE(aibridge_interceptions.thread_root_id, aibridge_interceptions.id)
|
||||
WHERE
|
||||
aibridge_interceptions.session_id = @session_id::text
|
||||
AND aibridge_interceptions.ended_at IS NOT NULL
|
||||
-- @authorize_filter
|
||||
ELSE true
|
||||
END
|
||||
ORDER BY
|
||||
-- Ensure threads and their associated interceptions (agentic loops) are sorted chronologically.
|
||||
pt.started_at ASC,
|
||||
pt.thread_id ASC,
|
||||
aibridge_interceptions.started_at ASC,
|
||||
aibridge_interceptions.id ASC
|
||||
sr.started_at DESC,
|
||||
sr.session_id DESC
|
||||
LIMIT COALESCE(NULLIF(@limit_::integer, 0), 100)
|
||||
OFFSET @offset_
|
||||
;
|
||||
|
||||
-- name: ListAIBridgeModelThoughtsByInterceptionIDs :many
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
aibridge_model_thoughts
|
||||
WHERE
|
||||
interception_id = ANY(@interception_ids::uuid[])
|
||||
ORDER BY
|
||||
created_at ASC;
|
||||
|
||||
-- name: ListAIBridgeModels :many
|
||||
SELECT
|
||||
model
|
||||
@@ -679,27 +616,3 @@ ORDER BY
|
||||
LIMIT COALESCE(NULLIF(@limit_::integer, 0), 100)
|
||||
OFFSET @offset_
|
||||
;
|
||||
|
||||
|
||||
-- name: ListAIBridgeClients :many
|
||||
SELECT
|
||||
COALESCE(client, 'Unknown') AS client
|
||||
FROM
|
||||
aibridge_interceptions
|
||||
WHERE
|
||||
ended_at IS NOT NULL
|
||||
-- Filter client (prefix match to allow B-tree index usage).
|
||||
AND CASE
|
||||
WHEN @client::text != '' THEN COALESCE(aibridge_interceptions.client, 'Unknown') LIKE @client::text || '%'
|
||||
ELSE true
|
||||
END
|
||||
-- We use an `@authorize_filter` as we are attempting to list clients
|
||||
-- that are relevant to the user and what they are allowed to see.
|
||||
-- Authorize Filter clause will be injected below in
|
||||
-- ListAIBridgeClientsAuthorized.
|
||||
-- @authorize_filter
|
||||
GROUP BY
|
||||
client
|
||||
LIMIT COALESCE(NULLIF(@limit_::integer, 0), 100)
|
||||
OFFSET @offset_
|
||||
;
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
-- name: GetUserAISeatStates :many
|
||||
-- Returns user IDs from the provided list that are consuming an AI seat.
|
||||
-- Filters to active, non-deleted, non-system users to match the canonical
|
||||
-- seat count query (GetActiveAISeatCount).
|
||||
SELECT
|
||||
ais.user_id
|
||||
FROM
|
||||
ai_seat_state ais
|
||||
JOIN
|
||||
users u
|
||||
ON
|
||||
ais.user_id = u.id
|
||||
WHERE
|
||||
ais.user_id = ANY(@user_ids::uuid[])
|
||||
AND u.status = 'active'::user_status
|
||||
AND u.deleted = false
|
||||
AND u.is_system = false;
|
||||
@@ -1,80 +0,0 @@
|
||||
-- name: InsertChatAutomationEvent :one
|
||||
INSERT INTO chat_automation_events (
|
||||
id,
|
||||
automation_id,
|
||||
trigger_id,
|
||||
received_at,
|
||||
payload,
|
||||
filter_matched,
|
||||
resolved_labels,
|
||||
matched_chat_id,
|
||||
created_chat_id,
|
||||
status,
|
||||
error
|
||||
) VALUES (
|
||||
@id::uuid,
|
||||
@automation_id::uuid,
|
||||
sqlc.narg('trigger_id')::uuid,
|
||||
@received_at::timestamptz,
|
||||
@payload::jsonb,
|
||||
@filter_matched::boolean,
|
||||
sqlc.narg('resolved_labels')::jsonb,
|
||||
sqlc.narg('matched_chat_id')::uuid,
|
||||
sqlc.narg('created_chat_id')::uuid,
|
||||
@status::chat_automation_event_status,
|
||||
sqlc.narg('error')::text
|
||||
) RETURNING *;
|
||||
|
||||
-- name: GetChatAutomationEventsByAutomationID :many
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
chat_automation_events
|
||||
WHERE
|
||||
automation_id = @automation_id::uuid
|
||||
AND CASE
|
||||
WHEN sqlc.narg('status_filter')::chat_automation_event_status IS NOT NULL THEN status = sqlc.narg('status_filter')::chat_automation_event_status
|
||||
ELSE true
|
||||
END
|
||||
ORDER BY
|
||||
received_at DESC
|
||||
OFFSET @offset_opt
|
||||
LIMIT
|
||||
COALESCE(NULLIF(@limit_opt :: int, 0), 50);
|
||||
|
||||
-- name: CountChatAutomationChatCreatesInWindow :one
|
||||
-- Counts new-chat events in the rate-limit window. This count is
|
||||
-- approximate under concurrency: concurrent webhook handlers may
|
||||
-- each read the same count before any of them insert, so brief
|
||||
-- bursts can slightly exceed the configured cap.
|
||||
SELECT COUNT(*)
|
||||
FROM chat_automation_events
|
||||
WHERE automation_id = @automation_id::uuid
|
||||
AND status = 'created'
|
||||
AND received_at > @window_start::timestamptz;
|
||||
|
||||
-- name: CountChatAutomationMessagesInWindow :one
|
||||
-- Counts total message events (creates + continues) in the rate-limit
|
||||
-- window. This count is approximate under concurrency: concurrent
|
||||
-- webhook handlers may each read the same count before any of them
|
||||
-- insert, so brief bursts can slightly exceed the configured cap.
|
||||
SELECT COUNT(*)
|
||||
FROM chat_automation_events
|
||||
WHERE automation_id = @automation_id::uuid
|
||||
AND status IN ('created', 'continued')
|
||||
AND received_at > @window_start::timestamptz;
|
||||
|
||||
-- name: PurgeOldChatAutomationEvents :execrows
|
||||
-- Deletes old chat automation events in bounded batches to avoid
|
||||
-- long-running locks on high-volume tables. Callers should loop
|
||||
-- until zero rows are returned.
|
||||
WITH old_events AS (
|
||||
SELECT id
|
||||
FROM chat_automation_events
|
||||
WHERE received_at < @before::timestamptz
|
||||
ORDER BY received_at ASC
|
||||
LIMIT @limit_count
|
||||
)
|
||||
DELETE FROM chat_automation_events
|
||||
USING old_events
|
||||
WHERE chat_automation_events.id = old_events.id;
|
||||
@@ -1,85 +0,0 @@
|
||||
-- name: InsertChatAutomation :one
|
||||
INSERT INTO chat_automations (
|
||||
id,
|
||||
owner_id,
|
||||
organization_id,
|
||||
name,
|
||||
description,
|
||||
instructions,
|
||||
model_config_id,
|
||||
mcp_server_ids,
|
||||
allowed_tools,
|
||||
status,
|
||||
max_chat_creates_per_hour,
|
||||
max_messages_per_hour,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (
|
||||
@id::uuid,
|
||||
@owner_id::uuid,
|
||||
@organization_id::uuid,
|
||||
@name::text,
|
||||
@description::text,
|
||||
@instructions::text,
|
||||
sqlc.narg('model_config_id')::uuid,
|
||||
COALESCE(@mcp_server_ids::uuid[], '{}'::uuid[]),
|
||||
COALESCE(@allowed_tools::text[], '{}'::text[]),
|
||||
@status::chat_automation_status,
|
||||
@max_chat_creates_per_hour::integer,
|
||||
@max_messages_per_hour::integer,
|
||||
@created_at::timestamptz,
|
||||
@updated_at::timestamptz
|
||||
) RETURNING *;
|
||||
|
||||
-- name: GetChatAutomationByID :one
|
||||
SELECT * FROM chat_automations WHERE id = @id::uuid;
|
||||
|
||||
-- name: GetChatAutomations :many
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
chat_automations
|
||||
WHERE
|
||||
CASE
|
||||
WHEN @owner_id :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN chat_automations.owner_id = @owner_id
|
||||
ELSE true
|
||||
END
|
||||
AND CASE
|
||||
WHEN @organization_id :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN chat_automations.organization_id = @organization_id
|
||||
ELSE true
|
||||
END
|
||||
-- Authorize Filter clause will be injected below in GetAuthorizedChatAutomations
|
||||
-- @authorize_filter
|
||||
ORDER BY
|
||||
created_at DESC, id DESC
|
||||
OFFSET @offset_opt
|
||||
LIMIT
|
||||
COALESCE(NULLIF(@limit_opt :: int, 0), 50);
|
||||
|
||||
-- name: UpdateChatAutomation :one
|
||||
UPDATE chat_automations SET
|
||||
name = @name::text,
|
||||
description = @description::text,
|
||||
instructions = @instructions::text,
|
||||
model_config_id = sqlc.narg('model_config_id')::uuid,
|
||||
mcp_server_ids = COALESCE(@mcp_server_ids::uuid[], '{}'::uuid[]),
|
||||
allowed_tools = COALESCE(@allowed_tools::text[], '{}'::text[]),
|
||||
status = @status::chat_automation_status,
|
||||
max_chat_creates_per_hour = @max_chat_creates_per_hour::integer,
|
||||
max_messages_per_hour = @max_messages_per_hour::integer,
|
||||
updated_at = @updated_at::timestamptz
|
||||
WHERE id = @id::uuid
|
||||
RETURNING *;
|
||||
|
||||
-- name: DeleteChatAutomationByID :exec
|
||||
DELETE FROM chat_automations WHERE id = @id::uuid;
|
||||
|
||||
-- name: CleanupDeletedMCPServerIDsFromChatAutomations :exec
|
||||
UPDATE chat_automations
|
||||
SET mcp_server_ids = (
|
||||
SELECT COALESCE(array_agg(sid), '{}')
|
||||
FROM unnest(chat_automations.mcp_server_ids) AS sid
|
||||
WHERE sid IN (SELECT id FROM mcp_server_configs)
|
||||
)
|
||||
WHERE mcp_server_ids != '{}'
|
||||
AND NOT (mcp_server_ids <@ COALESCE((SELECT array_agg(id) FROM mcp_server_configs), '{}'));
|
||||
@@ -1,87 +0,0 @@
|
||||
-- name: InsertChatAutomationTrigger :one
|
||||
INSERT INTO chat_automation_triggers (
|
||||
id,
|
||||
automation_id,
|
||||
type,
|
||||
webhook_secret,
|
||||
webhook_secret_key_id,
|
||||
cron_schedule,
|
||||
filter,
|
||||
label_paths,
|
||||
created_at,
|
||||
updated_at
|
||||
) VALUES (
|
||||
@id::uuid,
|
||||
@automation_id::uuid,
|
||||
@type::chat_automation_trigger_type,
|
||||
sqlc.narg('webhook_secret')::text,
|
||||
sqlc.narg('webhook_secret_key_id')::text,
|
||||
sqlc.narg('cron_schedule')::text,
|
||||
sqlc.narg('filter')::jsonb,
|
||||
sqlc.narg('label_paths')::jsonb,
|
||||
@created_at::timestamptz,
|
||||
@updated_at::timestamptz
|
||||
) RETURNING *;
|
||||
|
||||
-- name: GetChatAutomationTriggerByID :one
|
||||
SELECT * FROM chat_automation_triggers WHERE id = @id::uuid;
|
||||
|
||||
-- name: GetChatAutomationTriggersByAutomationID :many
|
||||
SELECT * FROM chat_automation_triggers
|
||||
WHERE automation_id = @automation_id::uuid
|
||||
ORDER BY created_at ASC;
|
||||
|
||||
-- name: UpdateChatAutomationTrigger :one
|
||||
UPDATE chat_automation_triggers SET
|
||||
cron_schedule = COALESCE(sqlc.narg('cron_schedule'), cron_schedule),
|
||||
filter = COALESCE(sqlc.narg('filter'), filter),
|
||||
label_paths = COALESCE(sqlc.narg('label_paths'), label_paths),
|
||||
updated_at = @updated_at::timestamptz
|
||||
WHERE id = @id::uuid
|
||||
RETURNING *;
|
||||
|
||||
-- name: UpdateChatAutomationTriggerWebhookSecret :one
|
||||
UPDATE chat_automation_triggers SET
|
||||
webhook_secret = sqlc.narg('webhook_secret')::text,
|
||||
webhook_secret_key_id = sqlc.narg('webhook_secret_key_id')::text,
|
||||
updated_at = @updated_at::timestamptz
|
||||
WHERE id = @id::uuid
|
||||
RETURNING *;
|
||||
|
||||
-- name: DeleteChatAutomationTriggerByID :exec
|
||||
DELETE FROM chat_automation_triggers WHERE id = @id::uuid;
|
||||
|
||||
-- name: GetActiveChatAutomationCronTriggers :many
|
||||
-- Returns all cron triggers whose parent automation is active or in
|
||||
-- preview mode. The scheduler uses this to evaluate which triggers
|
||||
-- are due.
|
||||
SELECT
|
||||
t.id,
|
||||
t.automation_id,
|
||||
t.type,
|
||||
t.cron_schedule,
|
||||
t.filter,
|
||||
t.label_paths,
|
||||
t.last_triggered_at,
|
||||
t.created_at,
|
||||
t.updated_at,
|
||||
a.status AS automation_status,
|
||||
a.owner_id AS automation_owner_id,
|
||||
a.instructions AS automation_instructions,
|
||||
a.name AS automation_name,
|
||||
a.organization_id AS automation_organization_id,
|
||||
a.model_config_id AS automation_model_config_id,
|
||||
a.mcp_server_ids AS automation_mcp_server_ids,
|
||||
a.allowed_tools AS automation_allowed_tools,
|
||||
a.max_chat_creates_per_hour AS automation_max_chat_creates_per_hour,
|
||||
a.max_messages_per_hour AS automation_max_messages_per_hour
|
||||
FROM chat_automation_triggers t
|
||||
JOIN chat_automations a ON a.id = t.automation_id
|
||||
WHERE t.type = 'cron'
|
||||
AND t.cron_schedule IS NOT NULL
|
||||
AND a.status IN ('active', 'preview');
|
||||
|
||||
-- name: UpdateChatAutomationTriggerLastTriggeredAt :exec
|
||||
UPDATE chat_automation_triggers
|
||||
SET last_triggered_at = @last_triggered_at::timestamptz
|
||||
WHERE id = @id::uuid;
|
||||
@@ -1,192 +1,9 @@
|
||||
-- name: ArchiveChatByID :many
|
||||
WITH chats AS (
|
||||
UPDATE chats
|
||||
SET archived = true, pin_order = 0, updated_at = NOW()
|
||||
WHERE id = @id::uuid OR root_chat_id = @id::uuid
|
||||
RETURNING *
|
||||
)
|
||||
SELECT *
|
||||
FROM chats
|
||||
ORDER BY (id = @id::uuid) DESC, created_at ASC, id ASC;
|
||||
-- name: ArchiveChatByID :exec
|
||||
UPDATE chats SET archived = true, updated_at = NOW()
|
||||
WHERE id = @id OR root_chat_id = @id;
|
||||
|
||||
-- name: UnarchiveChatByID :many
|
||||
WITH chats AS (
|
||||
UPDATE chats
|
||||
SET archived = false, updated_at = NOW()
|
||||
WHERE id = @id::uuid OR root_chat_id = @id::uuid
|
||||
RETURNING *
|
||||
)
|
||||
SELECT *
|
||||
FROM chats
|
||||
ORDER BY (id = @id::uuid) DESC, created_at ASC, id ASC;
|
||||
|
||||
-- name: PinChatByID :exec
|
||||
WITH target_chat AS (
|
||||
SELECT
|
||||
id,
|
||||
owner_id
|
||||
FROM
|
||||
chats
|
||||
WHERE
|
||||
id = @id::uuid
|
||||
),
|
||||
-- Under READ COMMITTED, concurrent pin operations for the same
|
||||
-- owner may momentarily produce duplicate pin_order values because
|
||||
-- each CTE snapshot does not see the other's writes. The next
|
||||
-- pin/unpin/reorder operation's ROW_NUMBER() self-heals the
|
||||
-- sequence, so this is acceptable.
|
||||
ranked AS (
|
||||
SELECT
|
||||
c.id,
|
||||
ROW_NUMBER() OVER (ORDER BY c.pin_order ASC, c.id ASC) :: integer AS next_pin_order
|
||||
FROM
|
||||
chats c
|
||||
JOIN
|
||||
target_chat ON c.owner_id = target_chat.owner_id
|
||||
WHERE
|
||||
c.pin_order > 0
|
||||
AND c.archived = FALSE
|
||||
AND c.id <> target_chat.id
|
||||
),
|
||||
updates AS (
|
||||
SELECT
|
||||
ranked.id,
|
||||
ranked.next_pin_order AS pin_order
|
||||
FROM
|
||||
ranked
|
||||
UNION ALL
|
||||
SELECT
|
||||
target_chat.id,
|
||||
COALESCE((
|
||||
SELECT
|
||||
MAX(ranked.next_pin_order)
|
||||
FROM
|
||||
ranked
|
||||
), 0) + 1 AS pin_order
|
||||
FROM
|
||||
target_chat
|
||||
)
|
||||
UPDATE
|
||||
chats c
|
||||
SET
|
||||
pin_order = updates.pin_order
|
||||
FROM
|
||||
updates
|
||||
WHERE
|
||||
c.id = updates.id;
|
||||
|
||||
-- name: UnpinChatByID :exec
|
||||
WITH target_chat AS (
|
||||
SELECT
|
||||
id,
|
||||
owner_id
|
||||
FROM
|
||||
chats
|
||||
WHERE
|
||||
id = @id::uuid
|
||||
),
|
||||
ranked AS (
|
||||
SELECT
|
||||
c.id,
|
||||
ROW_NUMBER() OVER (ORDER BY c.pin_order ASC, c.id ASC) :: integer AS current_position
|
||||
FROM
|
||||
chats c
|
||||
JOIN
|
||||
target_chat ON c.owner_id = target_chat.owner_id
|
||||
WHERE
|
||||
c.pin_order > 0
|
||||
AND c.archived = FALSE
|
||||
),
|
||||
target AS (
|
||||
SELECT
|
||||
ranked.id,
|
||||
ranked.current_position
|
||||
FROM
|
||||
ranked
|
||||
WHERE
|
||||
ranked.id = @id::uuid
|
||||
),
|
||||
updates AS (
|
||||
SELECT
|
||||
ranked.id,
|
||||
CASE
|
||||
WHEN ranked.id = target.id THEN 0
|
||||
WHEN ranked.current_position > target.current_position THEN ranked.current_position - 1
|
||||
ELSE ranked.current_position
|
||||
END AS pin_order
|
||||
FROM
|
||||
ranked
|
||||
CROSS JOIN
|
||||
target
|
||||
)
|
||||
UPDATE
|
||||
chats c
|
||||
SET
|
||||
pin_order = updates.pin_order
|
||||
FROM
|
||||
updates
|
||||
WHERE
|
||||
c.id = updates.id;
|
||||
|
||||
-- name: UpdateChatPinOrder :exec
|
||||
WITH target_chat AS (
|
||||
SELECT
|
||||
id,
|
||||
owner_id
|
||||
FROM
|
||||
chats
|
||||
WHERE
|
||||
id = @id::uuid
|
||||
),
|
||||
ranked AS (
|
||||
SELECT
|
||||
c.id,
|
||||
ROW_NUMBER() OVER (ORDER BY c.pin_order ASC, c.id ASC) :: integer AS current_position,
|
||||
COUNT(*) OVER () :: integer AS pinned_count
|
||||
FROM
|
||||
chats c
|
||||
JOIN
|
||||
target_chat ON c.owner_id = target_chat.owner_id
|
||||
WHERE
|
||||
c.pin_order > 0
|
||||
AND c.archived = FALSE
|
||||
),
|
||||
target AS (
|
||||
SELECT
|
||||
ranked.id,
|
||||
ranked.current_position,
|
||||
LEAST(GREATEST(@pin_order::integer, 1), ranked.pinned_count) AS desired_position
|
||||
FROM
|
||||
ranked
|
||||
WHERE
|
||||
ranked.id = @id::uuid
|
||||
),
|
||||
updates AS (
|
||||
SELECT
|
||||
ranked.id,
|
||||
CASE
|
||||
WHEN ranked.id = target.id THEN target.desired_position
|
||||
WHEN target.desired_position < target.current_position
|
||||
AND ranked.current_position >= target.desired_position
|
||||
AND ranked.current_position < target.current_position THEN ranked.current_position + 1
|
||||
WHEN target.desired_position > target.current_position
|
||||
AND ranked.current_position > target.current_position
|
||||
AND ranked.current_position <= target.desired_position THEN ranked.current_position - 1
|
||||
ELSE ranked.current_position
|
||||
END AS pin_order
|
||||
FROM
|
||||
ranked
|
||||
CROSS JOIN
|
||||
target
|
||||
)
|
||||
UPDATE
|
||||
chats c
|
||||
SET
|
||||
pin_order = updates.pin_order
|
||||
FROM
|
||||
updates
|
||||
WHERE
|
||||
c.id = updates.id;
|
||||
-- name: UnarchiveChatByID :exec
|
||||
UPDATE chats SET archived = false, updated_at = NOW() WHERE id = @id::uuid;
|
||||
|
||||
-- name: SoftDeleteChatMessagesAfterID :exec
|
||||
UPDATE
|
||||
@@ -235,21 +52,6 @@ WHERE
|
||||
ORDER BY
|
||||
created_at ASC;
|
||||
|
||||
-- name: GetChatMessagesByChatIDAscPaginated :many
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
chat_messages
|
||||
WHERE
|
||||
chat_id = @chat_id::uuid
|
||||
AND id > @after_id::bigint
|
||||
AND visibility IN ('user', 'both')
|
||||
AND deleted = false
|
||||
ORDER BY
|
||||
id ASC
|
||||
LIMIT
|
||||
COALESCE(NULLIF(@limit_val::int, 0), 50);
|
||||
|
||||
-- name: GetChatMessagesByChatIDDescPaginated :many
|
||||
SELECT
|
||||
*
|
||||
@@ -328,14 +130,7 @@ ORDER BY
|
||||
|
||||
-- name: GetChats :many
|
||||
SELECT
|
||||
sqlc.embed(chats),
|
||||
EXISTS (
|
||||
SELECT 1 FROM chat_messages cm
|
||||
WHERE cm.chat_id = chats.id
|
||||
AND cm.role = 'assistant'
|
||||
AND cm.deleted = false
|
||||
AND cm.id > COALESCE(chats.last_read_message_id, 0)
|
||||
) AS has_unread
|
||||
*
|
||||
FROM
|
||||
chats
|
||||
WHERE
|
||||
@@ -385,8 +180,6 @@ LIMIT
|
||||
INSERT INTO chats (
|
||||
owner_id,
|
||||
workspace_id,
|
||||
build_id,
|
||||
agent_id,
|
||||
parent_chat_id,
|
||||
root_chat_id,
|
||||
last_model_config_id,
|
||||
@@ -397,8 +190,6 @@ INSERT INTO chats (
|
||||
) VALUES (
|
||||
@owner_id::uuid,
|
||||
sqlc.narg('workspace_id')::uuid,
|
||||
sqlc.narg('build_id')::uuid,
|
||||
sqlc.narg('agent_id')::uuid,
|
||||
sqlc.narg('parent_chat_id')::uuid,
|
||||
sqlc.narg('root_chat_id')::uuid,
|
||||
@last_model_config_id::uuid,
|
||||
@@ -503,17 +294,6 @@ WHERE
|
||||
RETURNING
|
||||
*;
|
||||
|
||||
-- name: UpdateChatLastModelConfigByID :one
|
||||
UPDATE
|
||||
chats
|
||||
SET
|
||||
-- NOTE: updated_at is intentionally NOT touched here to avoid changing list ordering.
|
||||
last_model_config_id = @last_model_config_id::uuid
|
||||
WHERE
|
||||
id = @id::uuid
|
||||
RETURNING
|
||||
*;
|
||||
|
||||
-- name: UpdateChatLabelsByID :one
|
||||
UPDATE
|
||||
chats
|
||||
@@ -525,34 +305,16 @@ WHERE
|
||||
RETURNING
|
||||
*;
|
||||
|
||||
-- name: UpdateChatWorkspaceBinding :one
|
||||
UPDATE chats SET
|
||||
-- name: UpdateChatWorkspace :one
|
||||
UPDATE
|
||||
chats
|
||||
SET
|
||||
workspace_id = sqlc.narg('workspace_id')::uuid,
|
||||
build_id = sqlc.narg('build_id')::uuid,
|
||||
agent_id = sqlc.narg('agent_id')::uuid,
|
||||
updated_at = NOW()
|
||||
WHERE id = @id::uuid
|
||||
RETURNING *;
|
||||
|
||||
-- name: UpdateChatBuildAgentBinding :one
|
||||
UPDATE chats SET
|
||||
build_id = sqlc.narg('build_id')::uuid,
|
||||
agent_id = sqlc.narg('agent_id')::uuid,
|
||||
updated_at = NOW()
|
||||
WHERE
|
||||
id = @id::uuid
|
||||
RETURNING *;
|
||||
|
||||
-- name: UpdateChatLastInjectedContext :one
|
||||
-- Updates the cached injected context parts (AGENTS.md +
|
||||
-- skills) on the chat row. Called only when context changes
|
||||
-- (first workspace attach or agent change). updated_at is
|
||||
-- intentionally not touched to avoid reordering the chat list.
|
||||
UPDATE chats SET
|
||||
last_injected_context = sqlc.narg('last_injected_context')::jsonb
|
||||
WHERE
|
||||
id = @id::uuid
|
||||
RETURNING *;
|
||||
RETURNING
|
||||
*;
|
||||
|
||||
-- name: UpdateChatMCPServerIDs :one
|
||||
UPDATE
|
||||
@@ -609,21 +371,6 @@ WHERE
|
||||
RETURNING
|
||||
*;
|
||||
|
||||
-- name: UpdateChatStatusPreserveUpdatedAt :one
|
||||
UPDATE
|
||||
chats
|
||||
SET
|
||||
status = @status::chat_status,
|
||||
worker_id = sqlc.narg('worker_id')::uuid,
|
||||
started_at = sqlc.narg('started_at')::timestamptz,
|
||||
heartbeat_at = sqlc.narg('heartbeat_at')::timestamptz,
|
||||
last_error = sqlc.narg('last_error')::text,
|
||||
updated_at = @updated_at::timestamptz
|
||||
WHERE
|
||||
id = @id::uuid
|
||||
RETURNING
|
||||
*;
|
||||
|
||||
-- name: GetStaleChats :many
|
||||
-- Find chats that appear stuck (running but heartbeat has expired).
|
||||
-- Used for recovery after coderd crashes or long hangs.
|
||||
@@ -1131,13 +878,6 @@ JOIN group_members_expanded gme ON gme.group_id = g.id
|
||||
WHERE gme.user_id = @user_id::uuid
|
||||
AND g.chat_spend_limit_micros IS NOT NULL;
|
||||
|
||||
-- name: GetChatsByWorkspaceIDs :many
|
||||
SELECT *
|
||||
FROM chats
|
||||
WHERE archived = false
|
||||
AND workspace_id = ANY(@ids::uuid[])
|
||||
ORDER BY workspace_id, updated_at DESC;
|
||||
|
||||
-- name: ResolveUserChatSpendLimit :one
|
||||
-- Resolves the effective spend limit for a user using the hierarchy:
|
||||
-- 1. Individual user override (highest priority)
|
||||
@@ -1165,10 +905,3 @@ LEFT JOIN LATERAL (
|
||||
) gl ON TRUE
|
||||
WHERE u.id = @user_id::uuid
|
||||
LIMIT 1;
|
||||
|
||||
-- name: UpdateChatLastReadMessageID :exec
|
||||
-- Updates the last read message ID for a chat. This is used to track
|
||||
-- which messages the owner has seen, enabling unread indicators.
|
||||
UPDATE chats
|
||||
SET last_read_message_id = @last_read_message_id::bigint
|
||||
WHERE id = @id::uuid;
|
||||
|
||||
@@ -77,7 +77,6 @@ INSERT INTO mcp_server_configs (
|
||||
tool_deny_list,
|
||||
availability,
|
||||
enabled,
|
||||
model_intent,
|
||||
created_by,
|
||||
updated_by
|
||||
) VALUES (
|
||||
@@ -103,7 +102,6 @@ INSERT INTO mcp_server_configs (
|
||||
@tool_deny_list::text[],
|
||||
@availability::text,
|
||||
@enabled::boolean,
|
||||
@model_intent::boolean,
|
||||
@created_by::uuid,
|
||||
@updated_by::uuid
|
||||
)
|
||||
@@ -136,7 +134,6 @@ SET
|
||||
tool_deny_list = @tool_deny_list::text[],
|
||||
availability = @availability::text,
|
||||
enabled = @enabled::boolean,
|
||||
model_intent = @model_intent::boolean,
|
||||
updated_by = @updated_by::uuid,
|
||||
updated_at = NOW()
|
||||
WHERE
|
||||
|
||||
@@ -137,24 +137,6 @@ SELECT
|
||||
SELECT
|
||||
COALESCE((SELECT value FROM site_configs WHERE key = 'agents_chat_system_prompt'), '') :: text AS chat_system_prompt;
|
||||
|
||||
-- GetChatSystemPromptConfig returns both chat system prompt settings in a
|
||||
-- single read to avoid torn reads between separate site-config lookups.
|
||||
-- The include-default fallback preserves the legacy behavior where a
|
||||
-- non-empty custom prompt implied opting out before the explicit toggle
|
||||
-- existed.
|
||||
-- name: GetChatSystemPromptConfig :one
|
||||
SELECT
|
||||
COALESCE((SELECT value FROM site_configs WHERE key = 'agents_chat_system_prompt'), '') :: text AS chat_system_prompt,
|
||||
COALESCE(
|
||||
(SELECT value = 'true' FROM site_configs WHERE key = 'agents_chat_include_default_system_prompt'),
|
||||
NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM site_configs
|
||||
WHERE key = 'agents_chat_system_prompt'
|
||||
AND value != ''
|
||||
)
|
||||
) :: boolean AS include_default_system_prompt;
|
||||
|
||||
-- name: UpsertChatSystemPrompt :exec
|
||||
INSERT INTO site_configs (key, value) VALUES ('agents_chat_system_prompt', $1)
|
||||
ON CONFLICT (key) DO UPDATE SET value = $1 WHERE site_configs.key = 'agents_chat_system_prompt';
|
||||
@@ -185,38 +167,6 @@ WHERE site_configs.key = 'agents_desktop_enabled';
|
||||
SELECT
|
||||
COALESCE((SELECT value FROM site_configs WHERE key = 'agents_template_allowlist'), '') :: text AS template_allowlist;
|
||||
|
||||
-- GetChatIncludeDefaultSystemPrompt preserves the legacy default
|
||||
-- for deployments created before the explicit include-default toggle.
|
||||
-- When the toggle is unset, a non-empty custom prompt implies false;
|
||||
-- otherwise the setting defaults to true.
|
||||
-- name: GetChatIncludeDefaultSystemPrompt :one
|
||||
SELECT
|
||||
COALESCE(
|
||||
(SELECT value = 'true' FROM site_configs WHERE key = 'agents_chat_include_default_system_prompt'),
|
||||
NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM site_configs
|
||||
WHERE key = 'agents_chat_system_prompt'
|
||||
AND value != ''
|
||||
)
|
||||
) :: boolean AS include_default_system_prompt;
|
||||
|
||||
-- name: UpsertChatIncludeDefaultSystemPrompt :exec
|
||||
INSERT INTO site_configs (key, value)
|
||||
VALUES (
|
||||
'agents_chat_include_default_system_prompt',
|
||||
CASE
|
||||
WHEN sqlc.arg(include_default_system_prompt)::bool THEN 'true'
|
||||
ELSE 'false'
|
||||
END
|
||||
)
|
||||
ON CONFLICT (key) DO UPDATE
|
||||
SET value = CASE
|
||||
WHEN sqlc.arg(include_default_system_prompt)::bool THEN 'true'
|
||||
ELSE 'false'
|
||||
END
|
||||
WHERE site_configs.key = 'agents_chat_include_default_system_prompt';
|
||||
|
||||
-- name: GetChatWorkspaceTTL :one
|
||||
-- Returns the global TTL for chat workspaces as a Go duration string.
|
||||
-- Returns "0s" (disabled) when no value has been configured.
|
||||
|
||||
@@ -247,8 +247,6 @@ sql:
|
||||
mcp_server_tool_snapshots: MCPServerToolSnapshots
|
||||
mcp_server_config_id: MCPServerConfigID
|
||||
mcp_server_ids: MCPServerIDs
|
||||
automation_mcp_server_ids: AutomationMCPServerIDs
|
||||
webhook_secret_key_id: WebhookSecretKeyID
|
||||
icon_url: IconURL
|
||||
oauth2_client_id: OAuth2ClientID
|
||||
oauth2_client_secret: OAuth2ClientSecret
|
||||
|
||||
@@ -15,9 +15,6 @@ const (
|
||||
UniqueAPIKeysPkey UniqueConstraint = "api_keys_pkey" // ALTER TABLE ONLY api_keys ADD CONSTRAINT api_keys_pkey PRIMARY KEY (id);
|
||||
UniqueAuditLogsPkey UniqueConstraint = "audit_logs_pkey" // ALTER TABLE ONLY audit_logs ADD CONSTRAINT audit_logs_pkey PRIMARY KEY (id);
|
||||
UniqueBoundaryUsageStatsPkey UniqueConstraint = "boundary_usage_stats_pkey" // ALTER TABLE ONLY boundary_usage_stats ADD CONSTRAINT boundary_usage_stats_pkey PRIMARY KEY (replica_id);
|
||||
UniqueChatAutomationEventsPkey UniqueConstraint = "chat_automation_events_pkey" // ALTER TABLE ONLY chat_automation_events ADD CONSTRAINT chat_automation_events_pkey PRIMARY KEY (id);
|
||||
UniqueChatAutomationTriggersPkey UniqueConstraint = "chat_automation_triggers_pkey" // ALTER TABLE ONLY chat_automation_triggers ADD CONSTRAINT chat_automation_triggers_pkey PRIMARY KEY (id);
|
||||
UniqueChatAutomationsPkey UniqueConstraint = "chat_automations_pkey" // ALTER TABLE ONLY chat_automations ADD CONSTRAINT chat_automations_pkey PRIMARY KEY (id);
|
||||
UniqueChatDiffStatusesPkey UniqueConstraint = "chat_diff_statuses_pkey" // ALTER TABLE ONLY chat_diff_statuses ADD CONSTRAINT chat_diff_statuses_pkey PRIMARY KEY (chat_id);
|
||||
UniqueChatFilesPkey UniqueConstraint = "chat_files_pkey" // ALTER TABLE ONLY chat_files ADD CONSTRAINT chat_files_pkey PRIMARY KEY (id);
|
||||
UniqueChatMessagesPkey UniqueConstraint = "chat_messages_pkey" // ALTER TABLE ONLY chat_messages ADD CONSTRAINT chat_messages_pkey PRIMARY KEY (id);
|
||||
@@ -128,7 +125,6 @@ const (
|
||||
UniqueWorkspaceResourcesPkey UniqueConstraint = "workspace_resources_pkey" // ALTER TABLE ONLY workspace_resources ADD CONSTRAINT workspace_resources_pkey PRIMARY KEY (id);
|
||||
UniqueWorkspacesPkey UniqueConstraint = "workspaces_pkey" // ALTER TABLE ONLY workspaces ADD CONSTRAINT workspaces_pkey PRIMARY KEY (id);
|
||||
UniqueIndexAPIKeyName UniqueConstraint = "idx_api_key_name" // CREATE UNIQUE INDEX idx_api_key_name ON api_keys USING btree (user_id, token_name) WHERE (login_type = 'token'::login_type);
|
||||
UniqueIndexChatAutomationsOwnerOrgName UniqueConstraint = "idx_chat_automations_owner_org_name" // CREATE UNIQUE INDEX idx_chat_automations_owner_org_name ON chat_automations USING btree (owner_id, organization_id, name);
|
||||
UniqueIndexChatModelConfigsSingleDefault UniqueConstraint = "idx_chat_model_configs_single_default" // CREATE UNIQUE INDEX idx_chat_model_configs_single_default ON chat_model_configs USING btree ((1)) WHERE ((is_default = true) AND (deleted = false));
|
||||
UniqueIndexConnectionLogsConnectionIDWorkspaceIDAgentName UniqueConstraint = "idx_connection_logs_connection_id_workspace_id_agent_name" // CREATE UNIQUE INDEX idx_connection_logs_connection_id_workspace_id_agent_name ON connection_logs USING btree (connection_id, workspace_id, agent_name);
|
||||
UniqueIndexCustomRolesNameLowerOrganizationID UniqueConstraint = "idx_custom_roles_name_lower_organization_id" // CREATE UNIQUE INDEX idx_custom_roles_name_lower_organization_id ON custom_roles USING btree (lower(name), COALESCE(organization_id, '00000000-0000-0000-0000-000000000000'::uuid));
|
||||
|
||||
+101
-342
@@ -33,7 +33,6 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/db2sdk"
|
||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
dbpubsub "github.com/coder/coder/v2/coderd/database/pubsub"
|
||||
"github.com/coder/coder/v2/coderd/externalauth"
|
||||
"github.com/coder/coder/v2/coderd/externalauth/gitprovider"
|
||||
"github.com/coder/coder/v2/coderd/httpapi"
|
||||
@@ -111,28 +110,6 @@ func maybeWriteLimitErr(ctx context.Context, rw http.ResponseWriter, err error)
|
||||
return false
|
||||
}
|
||||
|
||||
func publishChatConfigEvent(logger slog.Logger, ps dbpubsub.Pubsub, kind pubsub.ChatConfigEventKind, entityID uuid.UUID) {
|
||||
payload, err := json.Marshal(pubsub.ChatConfigEvent{
|
||||
Kind: kind,
|
||||
EntityID: entityID,
|
||||
})
|
||||
if err != nil {
|
||||
logger.Error(context.Background(), "failed to marshal chat config event",
|
||||
slog.F("kind", kind),
|
||||
slog.F("entity_id", entityID),
|
||||
slog.Error(err),
|
||||
)
|
||||
return
|
||||
}
|
||||
if err := ps.Publish(pubsub.ChatConfigEventChannel, payload); err != nil {
|
||||
logger.Error(context.Background(), "failed to publish chat config event",
|
||||
slog.F("kind", kind),
|
||||
slog.F("entity_id", entityID),
|
||||
slog.Error(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// EXPERIMENTAL: this endpoint is experimental and is subject to change.
|
||||
func (api *API) watchChats(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
@@ -196,88 +173,6 @@ func (api *API) watchChats(rw http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// EXPERIMENTAL: chatsByWorkspace returns a mapping of workspace ID to
|
||||
// the latest non-archived chat ID for each requested workspace.
|
||||
// The query returns all matching chats and RBAC post-filters them;
|
||||
// the handler then picks the latest per workspace in Go. This avoids
|
||||
// the DISTINCT ON + post-filter bug where the sole candidate is
|
||||
// silently dropped when the caller can't read it.
|
||||
//
|
||||
// TODO:
|
||||
// 1. move aggregation to a SQL view with proper in-query authz so we
|
||||
// can return a single row per workspace without this two-pass approach.
|
||||
// 2. Restore the below router annotation and un-skip docs gen
|
||||
// <at>Router /experimental/chats/by-workspace [post]
|
||||
//
|
||||
// @Summary Get latest chats by workspace IDs
|
||||
// @ID get-latest-chats-by-workspace-ids
|
||||
// @Security CoderSessionToken
|
||||
// @Tags Chats
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200
|
||||
// @x-apidocgen {"skip": true}
|
||||
func (api *API) chatsByWorkspace(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
idsParam := r.URL.Query().Get("workspace_ids")
|
||||
if idsParam == "" {
|
||||
httpapi.Write(ctx, rw, http.StatusOK, map[uuid.UUID]uuid.UUID{})
|
||||
return
|
||||
}
|
||||
|
||||
raw := strings.Split(idsParam, ",")
|
||||
|
||||
// maxWorkspaceIDs is coupled to DEFAULT_RECORDS_PER_PAGE (25) in
|
||||
// site/src/components/PaginationWidget/utils.ts.
|
||||
// If the page size changes, this limit should too.
|
||||
const maxWorkspaceIDs = 25
|
||||
if len(raw) > maxWorkspaceIDs {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: fmt.Sprintf("Too many workspace IDs, maximum is %d.", maxWorkspaceIDs),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
workspaceIDs := make([]uuid.UUID, 0, len(raw))
|
||||
for _, s := range raw {
|
||||
id, err := uuid.Parse(strings.TrimSpace(s))
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: fmt.Sprintf("Invalid workspace ID %q: %s", s, err),
|
||||
})
|
||||
return
|
||||
}
|
||||
workspaceIDs = append(workspaceIDs, id)
|
||||
}
|
||||
|
||||
chats, err := api.Database.GetChatsByWorkspaceIDs(ctx, workspaceIDs)
|
||||
if httpapi.Is404Error(err) {
|
||||
httpapi.ResourceNotFound(rw)
|
||||
return
|
||||
} else if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Failed to get chats by workspace.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// The SQL orders by (workspace_id, updated_at DESC), so the first
|
||||
// chat seen per workspace after RBAC filtering is the latest
|
||||
// readable one.
|
||||
result := make(map[uuid.UUID]uuid.UUID, len(chats))
|
||||
for _, chat := range chats {
|
||||
if chat.WorkspaceID.Valid {
|
||||
if _, exists := result[chat.WorkspaceID.UUID]; !exists {
|
||||
result[chat.WorkspaceID.UUID] = chat.ID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, result)
|
||||
}
|
||||
|
||||
// EXPERIMENTAL: this endpoint is experimental and is subject to change.
|
||||
func (api *API) listChats(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
@@ -336,7 +231,7 @@ func (api *API) listChats(rw http.ResponseWriter, r *http.Request) {
|
||||
LimitOpt: int32(paginationParams.Limit),
|
||||
}
|
||||
|
||||
chatRows, err := api.Database.GetChats(ctx, params)
|
||||
chats, err := api.Database.GetChats(ctx, params)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Failed to list chats.",
|
||||
@@ -345,13 +240,7 @@ func (api *API) listChats(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Extract the Chat objects for diff status lookup.
|
||||
dbChats := make([]database.Chat, len(chatRows))
|
||||
for i, row := range chatRows {
|
||||
dbChats[i] = row.Chat
|
||||
}
|
||||
|
||||
diffStatusesByChatID, err := api.getChatDiffStatusesByChatID(ctx, dbChats)
|
||||
diffStatusesByChatID, err := api.getChatDiffStatusesByChatID(ctx, chats)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Failed to list chats.",
|
||||
@@ -360,7 +249,7 @@ func (api *API) listChats(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, db2sdk.ChatRows(chatRows, diffStatusesByChatID))
|
||||
httpapi.Write(ctx, rw, http.StatusOK, convertChats(chats, diffStatusesByChatID))
|
||||
}
|
||||
|
||||
func (api *API) getChatDiffStatusesByChatID(
|
||||
@@ -393,11 +282,6 @@ func (api *API) postChats(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
apiKey := httpmw.APIKey(r)
|
||||
|
||||
if !api.Authorize(r, policy.ActionCreate, rbac.ResourceChat.WithOwner(apiKey.UserID.String())) {
|
||||
httpapi.Forbidden(rw)
|
||||
return
|
||||
}
|
||||
|
||||
var req codersdk.CreateChatRequest
|
||||
if !httpapi.Read(ctx, rw, r, &req) {
|
||||
return
|
||||
@@ -503,10 +387,6 @@ func (api *API) postChats(rw http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
return
|
||||
}
|
||||
if dbauthz.IsNotAuthorizedError(err) {
|
||||
httpapi.Forbidden(rw)
|
||||
return
|
||||
}
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Failed to create chat.",
|
||||
Detail: err.Error(),
|
||||
@@ -514,7 +394,7 @@ func (api *API) postChats(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusCreated, db2sdk.Chat(chat, nil))
|
||||
httpapi.Write(ctx, rw, http.StatusCreated, convertChat(chat, nil))
|
||||
}
|
||||
|
||||
// EXPERIMENTAL: this endpoint is experimental and is subject to change.
|
||||
@@ -625,10 +505,6 @@ func (api *API) chatCostSummary(rw http.ResponseWriter, r *http.Request) {
|
||||
EndDate: endDate,
|
||||
})
|
||||
if err != nil {
|
||||
if dbauthz.IsNotAuthorizedError(err) {
|
||||
httpapi.Forbidden(rw)
|
||||
return
|
||||
}
|
||||
httpapi.InternalServerError(rw, err)
|
||||
return
|
||||
}
|
||||
@@ -639,10 +515,6 @@ func (api *API) chatCostSummary(rw http.ResponseWriter, r *http.Request) {
|
||||
EndDate: endDate,
|
||||
})
|
||||
if err != nil {
|
||||
if dbauthz.IsNotAuthorizedError(err) {
|
||||
httpapi.Forbidden(rw)
|
||||
return
|
||||
}
|
||||
httpapi.InternalServerError(rw, err)
|
||||
return
|
||||
}
|
||||
@@ -653,10 +525,6 @@ func (api *API) chatCostSummary(rw http.ResponseWriter, r *http.Request) {
|
||||
EndDate: endDate,
|
||||
})
|
||||
if err != nil {
|
||||
if dbauthz.IsNotAuthorizedError(err) {
|
||||
httpapi.Forbidden(rw)
|
||||
return
|
||||
}
|
||||
httpapi.InternalServerError(rw, err)
|
||||
return
|
||||
}
|
||||
@@ -1251,7 +1119,7 @@ func (api *API) getChat(rw http.ResponseWriter, r *http.Request) {
|
||||
slog.Error(err),
|
||||
)
|
||||
}
|
||||
httpapi.Write(ctx, rw, http.StatusOK, db2sdk.Chat(chat, diffStatus))
|
||||
httpapi.Write(ctx, rw, http.StatusOK, convertChat(chat, diffStatus))
|
||||
}
|
||||
|
||||
// EXPERIMENTAL: this endpoint is experimental and is subject to change.
|
||||
@@ -1582,8 +1450,8 @@ func (api *API) watchChatDesktop(rw http.ResponseWriter, r *http.Request) {
|
||||
logger.Debug(ctx, "desktop Bicopy finished")
|
||||
}
|
||||
|
||||
// patchChat updates a chat resource. Supports updating labels,
|
||||
// archiving, pinning, and pinned-chat ordering.
|
||||
// patchChat updates a chat resource. Supports updating labels and
|
||||
// toggling the archived state.
|
||||
func (api *API) patchChat(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
chat := httpmw.ChatParam(r)
|
||||
@@ -1641,20 +1509,20 @@ func (api *API) patchChat(rw http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
var err error
|
||||
// Use chatDaemon when available so it can interrupt active
|
||||
// processing before broadcasting archive state. Fall back to
|
||||
// direct DB when no daemon is running.
|
||||
// Use chatDaemon when available so it can notify active
|
||||
// subscribers. Fall back to direct DB for the simple
|
||||
// archive flag — no streaming state is involved.
|
||||
if archived {
|
||||
if api.chatDaemon != nil {
|
||||
err = api.chatDaemon.ArchiveChat(ctx, chat)
|
||||
} else {
|
||||
_, err = api.Database.ArchiveChatByID(ctx, chat.ID)
|
||||
err = api.Database.ArchiveChatByID(ctx, chat.ID)
|
||||
}
|
||||
} else {
|
||||
if api.chatDaemon != nil {
|
||||
err = api.chatDaemon.UnarchiveChat(ctx, chat)
|
||||
} else {
|
||||
_, err = api.Database.UnarchiveChatByID(ctx, chat.ID)
|
||||
err = api.Database.UnarchiveChatByID(ctx, chat.ID)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
@@ -1670,54 +1538,6 @@ func (api *API) patchChat(rw http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
if req.PinOrder != nil {
|
||||
pinOrder := *req.PinOrder
|
||||
if pinOrder < 0 {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Pin order must be non-negative.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if pinOrder > 0 && chat.Archived {
|
||||
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Cannot pin an archived chat.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// The behavior depends on current pin state:
|
||||
// - pinOrder == 0: unpin.
|
||||
// - pinOrder > 0 && already pinned: reorder (shift
|
||||
// neighbors, clamp to [1, count]).
|
||||
// - pinOrder > 0 && not pinned: append to end. The
|
||||
// requested value is intentionally ignored because
|
||||
// PinChatByID also bumps updated_at to keep the
|
||||
// chat visible in the paginated sidebar.
|
||||
var err error
|
||||
errMsg := "Failed to pin chat."
|
||||
switch {
|
||||
case pinOrder == 0:
|
||||
errMsg = "Failed to unpin chat."
|
||||
err = api.Database.UnpinChatByID(ctx, chat.ID)
|
||||
case chat.PinOrder > 0:
|
||||
errMsg = "Failed to reorder pinned chat."
|
||||
err = api.Database.UpdateChatPinOrder(ctx, database.UpdateChatPinOrderParams{
|
||||
ID: chat.ID,
|
||||
PinOrder: pinOrder,
|
||||
})
|
||||
default:
|
||||
err = api.Database.PinChatByID(ctx, chat.ID)
|
||||
}
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: errMsg,
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
rw.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
@@ -1974,39 +1794,6 @@ func (api *API) promoteChatQueuedMessage(rw http.ResponseWriter, r *http.Request
|
||||
httpapi.Write(ctx, rw, http.StatusOK, convertChatMessage(promoteResult.PromotedMessage))
|
||||
}
|
||||
|
||||
// markChatAsRead updates the last read message ID for a chat to the
|
||||
// latest message, so subsequent unread checks treat all current
|
||||
// messages as seen. This is called on stream connect and disconnect
|
||||
// to avoid per-message API calls during active streaming.
|
||||
func (api *API) markChatAsRead(ctx context.Context, chatID uuid.UUID) {
|
||||
lastMsg, err := api.Database.GetLastChatMessageByRole(ctx, database.GetLastChatMessageByRoleParams{
|
||||
ChatID: chatID,
|
||||
Role: database.ChatMessageRoleAssistant,
|
||||
})
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
// No assistant messages yet, nothing to mark as read.
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
api.Logger.Warn(ctx, "failed to get last assistant message for read marker",
|
||||
slog.F("chat_id", chatID),
|
||||
slog.Error(err),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
err = api.Database.UpdateChatLastReadMessageID(ctx, database.UpdateChatLastReadMessageIDParams{
|
||||
ID: chatID,
|
||||
LastReadMessageID: lastMsg.ID,
|
||||
})
|
||||
if err != nil {
|
||||
api.Logger.Warn(ctx, "failed to update chat last read message ID",
|
||||
slog.F("chat_id", chatID),
|
||||
slog.Error(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// EXPERIMENTAL: this endpoint is experimental and is subject to change.
|
||||
func (api *API) streamChat(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
@@ -2063,12 +1850,6 @@ func (api *API) streamChat(rw http.ResponseWriter, r *http.Request) {
|
||||
}()
|
||||
defer cancel()
|
||||
|
||||
// Mark the chat as read when the stream connects and again
|
||||
// when it disconnects so we avoid per-message API calls while
|
||||
// messages are actively streaming.
|
||||
api.markChatAsRead(ctx, chatID)
|
||||
defer api.markChatAsRead(context.WithoutCancel(ctx), chatID)
|
||||
|
||||
sendChatStreamBatch := func(batch []codersdk.ChatStreamEvent) error {
|
||||
if len(batch) == 0 {
|
||||
return nil
|
||||
@@ -2168,51 +1949,7 @@ func (api *API) interruptChat(rw http.ResponseWriter, r *http.Request) {
|
||||
chat = updatedChat
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, db2sdk.Chat(chat, nil))
|
||||
}
|
||||
|
||||
// EXPERIMENTAL: this endpoint is experimental and is subject to change.
|
||||
//
|
||||
//nolint:revive // HTTP handler writes to ResponseWriter.
|
||||
func (api *API) regenerateChatTitle(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
chat := httpmw.ChatParam(r)
|
||||
|
||||
if !api.Authorize(r, policy.ActionUpdate, chat.RBACObject()) {
|
||||
httpapi.ResourceNotFound(rw)
|
||||
return
|
||||
}
|
||||
if api.chatDaemon == nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Chat processor is unavailable.",
|
||||
Detail: "Chat processor is not configured.",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
updatedChat, err := api.chatDaemon.RegenerateChatTitle(ctx, chat)
|
||||
if err != nil {
|
||||
if errors.Is(err, chatd.ErrManualTitleRegenerationInProgress) {
|
||||
httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{
|
||||
Message: "Title regeneration already in progress for this chat.",
|
||||
})
|
||||
return
|
||||
}
|
||||
if maybeWriteLimitErr(ctx, rw, err) {
|
||||
return
|
||||
}
|
||||
if httpapi.Is404Error(err) {
|
||||
httpapi.ResourceNotFound(rw)
|
||||
return
|
||||
}
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Failed to regenerate chat title.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, db2sdk.Chat(updatedChat, nil))
|
||||
httpapi.Write(ctx, rw, http.StatusOK, convertChat(chat, nil))
|
||||
}
|
||||
|
||||
// EXPERIMENTAL: this endpoint is experimental and is subject to change.
|
||||
@@ -2943,35 +2680,25 @@ func detectChatFileType(data []byte) string {
|
||||
//nolint:revive // get-return: revive assumes get* must be a getter, but this is an HTTP handler.
|
||||
func (api *API) getChatSystemPrompt(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
if !api.Authorize(r, policy.ActionUpdate, rbac.ResourceDeploymentConfig) {
|
||||
httpapi.ResourceNotFound(rw)
|
||||
return
|
||||
}
|
||||
config, err := api.Database.GetChatSystemPromptConfig(ctx)
|
||||
prompt, err := api.Database.GetChatSystemPrompt(ctx)
|
||||
if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error fetching chat system prompt configuration.",
|
||||
Message: "Internal error fetching chat system prompt.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
httpapi.Write(ctx, rw, http.StatusOK, codersdk.ChatSystemPromptResponse{
|
||||
SystemPrompt: config.ChatSystemPrompt,
|
||||
IncludeDefaultSystemPrompt: config.IncludeDefaultSystemPrompt,
|
||||
DefaultSystemPrompt: chatd.DefaultSystemPrompt,
|
||||
httpapi.Write(ctx, rw, http.StatusOK, codersdk.ChatSystemPrompt{
|
||||
SystemPrompt: prompt,
|
||||
})
|
||||
}
|
||||
|
||||
func (api *API) putChatSystemPrompt(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
if !api.Authorize(r, policy.ActionUpdate, rbac.ResourceDeploymentConfig) {
|
||||
httpapi.Forbidden(rw)
|
||||
return
|
||||
}
|
||||
// Cap the raw request body to prevent excessive memory use from
|
||||
// payloads padded with invisible characters that sanitize away.
|
||||
r.Body = http.MaxBytesReader(rw, r.Body, int64(2*maxSystemPromptLenBytes))
|
||||
var req codersdk.UpdateChatSystemPromptRequest
|
||||
var req codersdk.ChatSystemPrompt
|
||||
if !httpapi.Read(ctx, rw, r, &req) {
|
||||
return
|
||||
}
|
||||
@@ -2985,23 +2712,13 @@ func (api *API) putChatSystemPrompt(rw http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
return
|
||||
}
|
||||
err := api.Database.InTx(func(tx database.Store) error {
|
||||
if err := tx.UpsertChatSystemPrompt(ctx, sanitizedPrompt); err != nil {
|
||||
return err
|
||||
}
|
||||
// Only update the include-default flag when the caller explicitly
|
||||
// provides it. Omitting the field preserves whatever is currently
|
||||
// stored (or the schema-level default for new deployments),
|
||||
// avoiding a backward-compatibility regression for older clients
|
||||
// that only send system_prompt.
|
||||
if req.IncludeDefaultSystemPrompt != nil {
|
||||
return tx.UpsertChatIncludeDefaultSystemPrompt(ctx, *req.IncludeDefaultSystemPrompt)
|
||||
}
|
||||
return nil
|
||||
}, nil)
|
||||
if err != nil {
|
||||
err := api.Database.UpsertChatSystemPrompt(ctx, sanitizedPrompt)
|
||||
if httpapi.Is404Error(err) { // also catches authz error
|
||||
httpapi.ResourceNotFound(rw)
|
||||
return
|
||||
} else if err != nil {
|
||||
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error updating chat system prompt configuration.",
|
||||
Message: "Internal error updating chat system prompt.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
@@ -3335,8 +3052,6 @@ func (api *API) putUserChatCustomPrompt(rw http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
publishChatConfigEvent(api.Logger, api.Pubsub, pubsub.ChatConfigEventUserPrompt, apiKey.UserID)
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, codersdk.UserChatCustomPrompt{
|
||||
CustomPrompt: updatedConfig.Value,
|
||||
})
|
||||
@@ -3507,32 +3222,21 @@ func (api *API) deleteUserChatCompactionThreshold(rw http.ResponseWriter, r *htt
|
||||
}
|
||||
|
||||
func (api *API) resolvedChatSystemPrompt(ctx context.Context) string {
|
||||
config, err := api.Database.GetChatSystemPromptConfig(ctx)
|
||||
custom, err := api.Database.GetChatSystemPrompt(ctx)
|
||||
if err != nil {
|
||||
// We intentionally fail open here. When the prompt configuration
|
||||
// cannot be read, returning the built-in default keeps the chat
|
||||
// grounded instead of sending no system guidance at all.
|
||||
api.Logger.Error(ctx, "failed to fetch chat system prompt configuration, using default", slog.Error(err))
|
||||
// Log but don't fail chat creation — fall back to the
|
||||
// built-in default so the user isn't blocked.
|
||||
api.Logger.Error(ctx, "failed to fetch custom chat system prompt, using default", slog.Error(err))
|
||||
return chatd.DefaultSystemPrompt
|
||||
}
|
||||
|
||||
sanitizedCustom := chatd.SanitizePromptText(config.ChatSystemPrompt)
|
||||
if sanitizedCustom == "" && strings.TrimSpace(config.ChatSystemPrompt) != "" {
|
||||
api.Logger.Warn(ctx, "custom system prompt became empty after sanitization, omitting custom portion")
|
||||
sanitized := chatd.SanitizePromptText(custom)
|
||||
if sanitized == "" && strings.TrimSpace(custom) != "" {
|
||||
api.Logger.Warn(ctx, "custom system prompt became empty after sanitization, using default")
|
||||
}
|
||||
|
||||
var parts []string
|
||||
if config.IncludeDefaultSystemPrompt {
|
||||
parts = append(parts, chatd.DefaultSystemPrompt)
|
||||
if sanitized != "" {
|
||||
return sanitized
|
||||
}
|
||||
if sanitizedCustom != "" {
|
||||
parts = append(parts, sanitizedCustom)
|
||||
}
|
||||
result := strings.Join(parts, "\n\n")
|
||||
if result == "" {
|
||||
api.Logger.Warn(ctx, "resolved system prompt is empty, no system prompt will be injected into chats")
|
||||
}
|
||||
return result
|
||||
return chatd.DefaultSystemPrompt
|
||||
}
|
||||
|
||||
func (api *API) postChatFile(rw http.ResponseWriter, r *http.Request) {
|
||||
@@ -3854,6 +3558,73 @@ func truncateRunes(value string, maxLen int) string {
|
||||
return string(runes[:maxLen])
|
||||
}
|
||||
|
||||
func convertChat(c database.Chat, diffStatus *database.ChatDiffStatus) codersdk.Chat {
|
||||
mcpServerIDs := c.MCPServerIDs
|
||||
if mcpServerIDs == nil {
|
||||
mcpServerIDs = []uuid.UUID{}
|
||||
}
|
||||
labels := map[string]string(c.Labels)
|
||||
if labels == nil {
|
||||
labels = map[string]string{}
|
||||
}
|
||||
chat := codersdk.Chat{
|
||||
ID: c.ID,
|
||||
OwnerID: c.OwnerID,
|
||||
LastModelConfigID: c.LastModelConfigID,
|
||||
Title: c.Title,
|
||||
Status: codersdk.ChatStatus(c.Status),
|
||||
Archived: c.Archived,
|
||||
CreatedAt: c.CreatedAt,
|
||||
UpdatedAt: c.UpdatedAt,
|
||||
MCPServerIDs: mcpServerIDs,
|
||||
Labels: labels,
|
||||
}
|
||||
if c.LastError.Valid {
|
||||
chat.LastError = &c.LastError.String
|
||||
}
|
||||
if c.ParentChatID.Valid {
|
||||
parentChatID := c.ParentChatID.UUID
|
||||
chat.ParentChatID = &parentChatID
|
||||
}
|
||||
switch {
|
||||
case c.RootChatID.Valid:
|
||||
rootChatID := c.RootChatID.UUID
|
||||
chat.RootChatID = &rootChatID
|
||||
case c.ParentChatID.Valid:
|
||||
rootChatID := c.ParentChatID.UUID
|
||||
chat.RootChatID = &rootChatID
|
||||
default:
|
||||
rootChatID := c.ID
|
||||
chat.RootChatID = &rootChatID
|
||||
}
|
||||
if c.WorkspaceID.Valid {
|
||||
chat.WorkspaceID = &c.WorkspaceID.UUID
|
||||
}
|
||||
if diffStatus != nil {
|
||||
convertedDiffStatus := db2sdk.ChatDiffStatus(c.ID, diffStatus)
|
||||
chat.DiffStatus = &convertedDiffStatus
|
||||
}
|
||||
return chat
|
||||
}
|
||||
|
||||
func convertChats(chats []database.Chat, diffStatusesByChatID map[uuid.UUID]database.ChatDiffStatus) []codersdk.Chat {
|
||||
result := make([]codersdk.Chat, len(chats))
|
||||
for i, c := range chats {
|
||||
diffStatus, ok := diffStatusesByChatID[c.ID]
|
||||
if ok {
|
||||
result[i] = convertChat(c, &diffStatus)
|
||||
continue
|
||||
}
|
||||
|
||||
result[i] = convertChat(c, nil)
|
||||
if diffStatusesByChatID != nil {
|
||||
emptyDiffStatus := db2sdk.ChatDiffStatus(c.ID, nil)
|
||||
result[i].DiffStatus = &emptyDiffStatus
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func convertChatCostModelBreakdown(model database.GetChatCostPerModelRow) codersdk.ChatCostModelBreakdown {
|
||||
displayName := strings.TrimSpace(model.DisplayName)
|
||||
if displayName == "" {
|
||||
@@ -4109,8 +3880,6 @@ func (api *API) createChatProvider(rw http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
publishChatConfigEvent(api.Logger, api.Pubsub, pubsub.ChatConfigEventProviders, uuid.Nil)
|
||||
|
||||
httpapi.Write(
|
||||
ctx,
|
||||
rw,
|
||||
@@ -4197,8 +3966,6 @@ func (api *API) updateChatProvider(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
publishChatConfigEvent(api.Logger, api.Pubsub, pubsub.ChatConfigEventProviders, uuid.Nil)
|
||||
|
||||
httpapi.Write(
|
||||
ctx,
|
||||
rw,
|
||||
@@ -4253,8 +4020,6 @@ func (api *API) deleteChatProvider(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
publishChatConfigEvent(api.Logger, api.Pubsub, pubsub.ChatConfigEventProviders, uuid.Nil)
|
||||
|
||||
rw.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
@@ -4434,8 +4199,6 @@ func (api *API) createChatModelConfig(rw http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
publishChatConfigEvent(api.Logger, api.Pubsub, pubsub.ChatConfigEventModelConfig, inserted.ID)
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusCreated, convertChatModelConfig(inserted))
|
||||
}
|
||||
|
||||
@@ -4607,8 +4370,6 @@ func (api *API) updateChatModelConfig(rw http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
publishChatConfigEvent(api.Logger, api.Pubsub, pubsub.ChatConfigEventModelConfig, updated.ID)
|
||||
|
||||
httpapi.Write(ctx, rw, http.StatusOK, convertChatModelConfig(updated))
|
||||
}
|
||||
|
||||
@@ -4649,8 +4410,6 @@ func (api *API) deleteChatModelConfig(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
publishChatConfigEvent(api.Logger, api.Pubsub, pubsub.ChatConfigEventModelConfig, modelConfigID)
|
||||
|
||||
rw.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
|
||||
+36
-1444
File diff suppressed because it is too large
Load Diff
@@ -71,8 +71,8 @@ func (r *ProvisionerDaemonsReport) Run(ctx context.Context, opts *ProvisionerDae
|
||||
return
|
||||
}
|
||||
|
||||
// nolint: gocritic // Read-only access to provisioner daemons for health check
|
||||
daemons, err := opts.Store.GetProvisionerDaemons(dbauthz.AsSystemReadProvisionerDaemons(ctx))
|
||||
// nolint: gocritic // need an actor to fetch provisioner daemons
|
||||
daemons, err := opts.Store.GetProvisionerDaemons(dbauthz.AsSystemRestricted(ctx))
|
||||
if err != nil {
|
||||
r.Severity = health.SeverityError
|
||||
r.Error = ptr.Ref("error fetching provisioner daemons: " + err.Error())
|
||||
|
||||
@@ -438,7 +438,7 @@ func OneWayWebSocketEventSender(log slog.Logger) func(rw http.ResponseWriter, r
|
||||
}
|
||||
go HeartbeatClose(ctx, log, cancel, socket)
|
||||
|
||||
eventC := make(chan codersdk.ServerSentEvent, 64)
|
||||
eventC := make(chan codersdk.ServerSentEvent)
|
||||
socketErrC := make(chan websocket.CloseError, 1)
|
||||
closed := make(chan struct{})
|
||||
go func() {
|
||||
@@ -488,16 +488,6 @@ func OneWayWebSocketEventSender(log slog.Logger) func(rw http.ResponseWriter, r
|
||||
}()
|
||||
|
||||
sendEvent := func(event codersdk.ServerSentEvent) error {
|
||||
// Prioritize context cancellation over sending to the
|
||||
// buffered channel. Without this check, both cases in
|
||||
// the select below can fire simultaneously when the
|
||||
// context is already done and the channel has capacity,
|
||||
// making the result nondeterministic.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
select {
|
||||
case eventC <- event:
|
||||
case <-ctx.Done():
|
||||
|
||||
@@ -699,8 +699,8 @@ func ExtractAPIKey(rw http.ResponseWriter, r *http.Request, cfg ExtractAPIKeyCon
|
||||
// is being used with the correct audience/resource server (RFC 8707).
|
||||
func validateOAuth2ProviderAppTokenAudience(ctx context.Context, db database.Store, key database.APIKey, accessURL *url.URL, r *http.Request) error {
|
||||
// Get the OAuth2 provider app token to check its audience
|
||||
//nolint:gocritic // OAuth2 system context — audience validation for provider app tokens
|
||||
token, err := db.GetOAuth2ProviderAppTokenByAPIKeyID(dbauthz.AsSystemOAuth2(ctx), key.ID)
|
||||
//nolint:gocritic // System needs to access token for audience validation
|
||||
token, err := db.GetOAuth2ProviderAppTokenByAPIKeyID(dbauthz.AsSystemRestricted(ctx), key.ID)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("failed to get OAuth2 token: %w", err)
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@ func CSRF(cookieCfg codersdk.HTTPCookieConfig) func(next http.Handler) http.Hand
|
||||
|
||||
// CSRF only affects requests that automatically attach credentials via a cookie.
|
||||
// If no cookie is present, then there is no risk of CSRF.
|
||||
//nolint:govet
|
||||
sessCookie, err := r.Cookie(codersdk.SessionTokenCookie)
|
||||
if xerrors.Is(err, http.ErrNoCookie) {
|
||||
return true
|
||||
|
||||
+10
-95
@@ -25,7 +25,6 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/httpmw"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
"github.com/coder/coder/v2/coderd/rbac/policy"
|
||||
"github.com/coder/coder/v2/coderd/x/chatd/mcpclient"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
@@ -60,8 +59,7 @@ func (api *API) listMCPServerConfigs(rw http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Look up the calling user's OAuth2 tokens so we can populate
|
||||
// auth_connected per server. Attempt to refresh expired tokens
|
||||
// so the status is accurate and the token is ready for use.
|
||||
// auth_connected per server.
|
||||
//nolint:gocritic // Need to check user tokens across all servers.
|
||||
userTokens, err := api.Database.GetMCPServerUserTokensByUserID(dbauthz.AsSystemRestricted(ctx), apiKey.UserID)
|
||||
if err != nil {
|
||||
@@ -71,20 +69,9 @@ func (api *API) listMCPServerConfigs(rw http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Build a config lookup for the refresh helper.
|
||||
configByID := make(map[uuid.UUID]database.MCPServerConfig, len(configs))
|
||||
for _, c := range configs {
|
||||
configByID[c.ID] = c
|
||||
}
|
||||
|
||||
tokenMap := make(map[uuid.UUID]bool, len(userTokens))
|
||||
for _, tok := range userTokens {
|
||||
cfg, ok := configByID[tok.MCPServerConfigID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
tokenMap[tok.MCPServerConfigID] = api.refreshMCPUserToken(ctx, cfg, tok, apiKey.UserID)
|
||||
for _, t := range userTokens {
|
||||
tokenMap[t.MCPServerConfigID] = true
|
||||
}
|
||||
|
||||
resp := make([]codersdk.MCPServerConfig, 0, len(configs))
|
||||
@@ -170,7 +157,6 @@ func (api *API) createMCPServerConfig(rw http.ResponseWriter, r *http.Request) {
|
||||
ToolDenyList: coalesceStringSlice(trimStringSlice(req.ToolDenyList)),
|
||||
Availability: strings.TrimSpace(req.Availability),
|
||||
Enabled: req.Enabled,
|
||||
ModelIntent: req.ModelIntent,
|
||||
CreatedBy: apiKey.UserID,
|
||||
UpdatedBy: apiKey.UserID,
|
||||
})
|
||||
@@ -257,7 +243,6 @@ func (api *API) createMCPServerConfig(rw http.ResponseWriter, r *http.Request) {
|
||||
ToolDenyList: inserted.ToolDenyList,
|
||||
Availability: inserted.Availability,
|
||||
Enabled: inserted.Enabled,
|
||||
ModelIntent: inserted.ModelIntent,
|
||||
UpdatedBy: apiKey.UserID,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -325,7 +310,6 @@ func (api *API) createMCPServerConfig(rw http.ResponseWriter, r *http.Request) {
|
||||
ToolDenyList: coalesceStringSlice(trimStringSlice(req.ToolDenyList)),
|
||||
Availability: strings.TrimSpace(req.Availability),
|
||||
Enabled: req.Enabled,
|
||||
ModelIntent: req.ModelIntent,
|
||||
CreatedBy: apiKey.UserID,
|
||||
UpdatedBy: apiKey.UserID,
|
||||
})
|
||||
@@ -402,8 +386,7 @@ func (api *API) getMCPServerConfig(rw http.ResponseWriter, r *http.Request) {
|
||||
sdkConfig = convertMCPServerConfigRedacted(config)
|
||||
}
|
||||
|
||||
// Populate AuthConnected for the calling user. Attempt to
|
||||
// refresh the token so the status is accurate.
|
||||
// Populate AuthConnected for the calling user.
|
||||
if config.AuthType == "oauth2" {
|
||||
//nolint:gocritic // Need to check user token for this server.
|
||||
userTokens, err := api.Database.GetMCPServerUserTokensByUserID(dbauthz.AsSystemRestricted(ctx), apiKey.UserID)
|
||||
@@ -414,9 +397,9 @@ func (api *API) getMCPServerConfig(rw http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
return
|
||||
}
|
||||
for _, tok := range userTokens {
|
||||
if tok.MCPServerConfigID == config.ID {
|
||||
sdkConfig.AuthConnected = api.refreshMCPUserToken(ctx, config, tok, apiKey.UserID)
|
||||
for _, t := range userTokens {
|
||||
if t.MCPServerConfigID == config.ID {
|
||||
sdkConfig.AuthConnected = true
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -575,11 +558,6 @@ func (api *API) updateMCPServerConfig(rw http.ResponseWriter, r *http.Request) {
|
||||
enabled = *req.Enabled
|
||||
}
|
||||
|
||||
modelIntent := existing.ModelIntent
|
||||
if req.ModelIntent != nil {
|
||||
modelIntent = *req.ModelIntent
|
||||
}
|
||||
|
||||
// When auth_type changes, clear fields belonging to the
|
||||
// previous auth type so stale secrets don't persist.
|
||||
if authType != existing.AuthType {
|
||||
@@ -647,7 +625,6 @@ func (api *API) updateMCPServerConfig(rw http.ResponseWriter, r *http.Request) {
|
||||
ToolDenyList: toolDenyList,
|
||||
Availability: availability,
|
||||
Enabled: enabled,
|
||||
ModelIntent: modelIntent,
|
||||
UpdatedBy: apiKey.UserID,
|
||||
ID: existing.ID,
|
||||
})
|
||||
@@ -1025,67 +1002,6 @@ func (api *API) mcpServerOAuth2Disconnect(rw http.ResponseWriter, r *http.Reques
|
||||
|
||||
// parseMCPServerConfigID extracts the MCP server config UUID from the
|
||||
// "mcpServer" path parameter.
|
||||
// refreshMCPUserToken attempts to refresh an expired OAuth2 token
|
||||
// for the given MCP server config. Returns true when the token is
|
||||
// valid (either still fresh or successfully refreshed), false when
|
||||
// the token is expired and cannot be refreshed.
|
||||
func (api *API) refreshMCPUserToken(
|
||||
ctx context.Context,
|
||||
cfg database.MCPServerConfig,
|
||||
tok database.MCPServerUserToken,
|
||||
userID uuid.UUID,
|
||||
) bool {
|
||||
if cfg.AuthType != "oauth2" {
|
||||
return true
|
||||
}
|
||||
if tok.RefreshToken == "" {
|
||||
// No refresh token — consider connected only if not
|
||||
// expired (or no expiry set).
|
||||
return !tok.Expiry.Valid || tok.Expiry.Time.After(time.Now())
|
||||
}
|
||||
|
||||
result, err := mcpclient.RefreshOAuth2Token(ctx, cfg, tok)
|
||||
if err != nil {
|
||||
api.Logger.Warn(ctx, "failed to refresh MCP oauth2 token",
|
||||
slog.F("server_slug", cfg.Slug),
|
||||
slog.Error(err),
|
||||
)
|
||||
// Refresh failed — token is dead.
|
||||
return false
|
||||
}
|
||||
|
||||
if result.Refreshed {
|
||||
var expiry sql.NullTime
|
||||
if !result.Expiry.IsZero() {
|
||||
expiry = sql.NullTime{Time: result.Expiry, Valid: true}
|
||||
}
|
||||
|
||||
//nolint:gocritic // Need system-level write access to
|
||||
// persist the refreshed OAuth2 token.
|
||||
_, err = api.Database.UpsertMCPServerUserToken(
|
||||
dbauthz.AsSystemRestricted(ctx),
|
||||
database.UpsertMCPServerUserTokenParams{
|
||||
MCPServerConfigID: tok.MCPServerConfigID,
|
||||
UserID: userID,
|
||||
AccessToken: result.AccessToken,
|
||||
AccessTokenKeyID: sql.NullString{},
|
||||
RefreshToken: result.RefreshToken,
|
||||
RefreshTokenKeyID: sql.NullString{},
|
||||
TokenType: result.TokenType,
|
||||
Expiry: expiry,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
api.Logger.Warn(ctx, "failed to persist refreshed MCP oauth2 token",
|
||||
slog.F("server_slug", cfg.Slug),
|
||||
slog.Error(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func parseMCPServerConfigID(rw http.ResponseWriter, r *http.Request) (uuid.UUID, bool) {
|
||||
mcpServerID, err := uuid.Parse(chi.URLParam(r, "mcpServer"))
|
||||
if err != nil {
|
||||
@@ -1129,10 +1045,9 @@ func convertMCPServerConfig(config database.MCPServerConfig) codersdk.MCPServerC
|
||||
|
||||
Availability: config.Availability,
|
||||
|
||||
Enabled: config.Enabled,
|
||||
ModelIntent: config.ModelIntent,
|
||||
CreatedAt: config.CreatedAt,
|
||||
UpdatedAt: config.UpdatedAt,
|
||||
Enabled: config.Enabled,
|
||||
CreatedAt: config.CreatedAt,
|
||||
UpdatedAt: config.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-62
@@ -2,7 +2,6 @@ package coderd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
@@ -180,17 +179,7 @@ func (api *API) organizationMember(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
var aiSeatSet map[uuid.UUID]struct{}
|
||||
if api.Entitlements.Enabled(codersdk.FeatureAIGovernanceUserLimit) {
|
||||
//nolint:gocritic // AI seat state is a system-level read gated by entitlement.
|
||||
aiSeatSet, err = getAISeatSetByUserIDs(dbauthz.AsSystemRestricted(ctx), api.Database, []uuid.UUID{member.UserID})
|
||||
if err != nil {
|
||||
httpapi.InternalServerError(rw, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := convertOrganizationMembersWithUserData(ctx, api.Database, rows, aiSeatSet)
|
||||
resp, err := convertOrganizationMembersWithUserData(ctx, api.Database, rows)
|
||||
if err != nil {
|
||||
httpapi.InternalServerError(rw, err)
|
||||
return
|
||||
@@ -238,21 +227,7 @@ func (api *API) listMembers(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
userIDs := make([]uuid.UUID, 0, len(members))
|
||||
for _, member := range members {
|
||||
userIDs = append(userIDs, member.OrganizationMember.UserID)
|
||||
}
|
||||
var aiSeatSet map[uuid.UUID]struct{}
|
||||
if api.Entitlements.Enabled(codersdk.FeatureAIGovernanceUserLimit) {
|
||||
//nolint:gocritic // AI seat state is a system-level read gated by entitlement.
|
||||
aiSeatSet, err = getAISeatSetByUserIDs(dbauthz.AsSystemRestricted(ctx), api.Database, userIDs)
|
||||
if err != nil {
|
||||
httpapi.InternalServerError(rw, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := convertOrganizationMembersWithUserData(ctx, api.Database, members, aiSeatSet)
|
||||
resp, err := convertOrganizationMembersWithUserData(ctx, api.Database, members)
|
||||
if err != nil {
|
||||
httpapi.InternalServerError(rw, err)
|
||||
return
|
||||
@@ -349,21 +324,7 @@ func (api *API) paginatedMembers(rw http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
userIDs := make([]uuid.UUID, 0, len(memberRows))
|
||||
for _, member := range memberRows {
|
||||
userIDs = append(userIDs, member.OrganizationMember.UserID)
|
||||
}
|
||||
var aiSeatSet map[uuid.UUID]struct{}
|
||||
if api.Entitlements.Enabled(codersdk.FeatureAIGovernanceUserLimit) {
|
||||
//nolint:gocritic // AI seat state is a system-level read gated by entitlement.
|
||||
aiSeatSet, err = getAISeatSetByUserIDs(dbauthz.AsSystemRestricted(ctx), api.Database, userIDs)
|
||||
if err != nil {
|
||||
httpapi.InternalServerError(rw, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
members, err := convertOrganizationMembersWithUserData(ctx, api.Database, memberRows, aiSeatSet)
|
||||
members, err := convertOrganizationMembersWithUserData(ctx, api.Database, memberRows)
|
||||
if err != nil {
|
||||
httpapi.InternalServerError(rw, err)
|
||||
return
|
||||
@@ -376,23 +337,6 @@ func (api *API) paginatedMembers(rw http.ResponseWriter, r *http.Request) {
|
||||
httpapi.Write(ctx, rw, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
func getAISeatSetByUserIDs(ctx context.Context, db database.Store, userIDs []uuid.UUID) (map[uuid.UUID]struct{}, error) {
|
||||
aiSeatUserIDs, err := db.GetUserAISeatStates(ctx, userIDs)
|
||||
if xerrors.Is(err, sql.ErrNoRows) {
|
||||
err = nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
aiSeatSet := make(map[uuid.UUID]struct{}, len(aiSeatUserIDs))
|
||||
for _, uid := range aiSeatUserIDs {
|
||||
aiSeatSet[uid] = struct{}{}
|
||||
}
|
||||
|
||||
return aiSeatSet, nil
|
||||
}
|
||||
|
||||
// @Summary Assign role to organization member
|
||||
// @ID assign-role-to-organization-member
|
||||
// @Security CoderSessionToken
|
||||
@@ -564,7 +508,7 @@ func convertOrganizationMembers(ctx context.Context, db database.Store, mems []d
|
||||
return converted, nil
|
||||
}
|
||||
|
||||
func convertOrganizationMembersWithUserData(ctx context.Context, db database.Store, rows []database.OrganizationMembersRow, aiSeatSet map[uuid.UUID]struct{}) ([]codersdk.OrganizationMemberWithUserData, error) {
|
||||
func convertOrganizationMembersWithUserData(ctx context.Context, db database.Store, rows []database.OrganizationMembersRow) ([]codersdk.OrganizationMemberWithUserData, error) {
|
||||
members := make([]database.OrganizationMember, 0)
|
||||
for _, row := range rows {
|
||||
members = append(members, row.OrganizationMember)
|
||||
@@ -580,14 +524,12 @@ func convertOrganizationMembersWithUserData(ctx context.Context, db database.Sto
|
||||
|
||||
converted := make([]codersdk.OrganizationMemberWithUserData, 0)
|
||||
for i := range convertedMembers {
|
||||
_, hasAISeat := aiSeatSet[rows[i].OrganizationMember.UserID]
|
||||
converted = append(converted, codersdk.OrganizationMemberWithUserData{
|
||||
Username: rows[i].Username,
|
||||
AvatarURL: rows[i].AvatarURL,
|
||||
Name: rows[i].Name,
|
||||
Email: rows[i].Email,
|
||||
GlobalRoles: db2sdk.SlimRolesFromNames(rows[i].GlobalRoles),
|
||||
HasAISeat: hasAISeat,
|
||||
LastSeenAt: rows[i].LastSeenAt,
|
||||
Status: codersdk.UserStatus(rows[i].Status),
|
||||
IsServiceAccount: rows[i].IsServiceAccount,
|
||||
|
||||
@@ -73,8 +73,8 @@ func CreateDynamicClientRegistration(db database.Store, accessURL *url.URL, audi
|
||||
// Store in database - use system context since this is a public endpoint
|
||||
now := dbtime.Now()
|
||||
clientName := req.GenerateClientName()
|
||||
//nolint:gocritic // OAuth2 system context — dynamic registration is a public endpoint
|
||||
app, err := db.InsertOAuth2ProviderApp(dbauthz.AsSystemOAuth2(ctx), database.InsertOAuth2ProviderAppParams{
|
||||
//nolint:gocritic // Dynamic client registration is a public endpoint, system access required
|
||||
app, err := db.InsertOAuth2ProviderApp(dbauthz.AsSystemRestricted(ctx), database.InsertOAuth2ProviderAppParams{
|
||||
ID: clientID,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
@@ -121,8 +121,8 @@ func CreateDynamicClientRegistration(db database.Store, accessURL *url.URL, audi
|
||||
return
|
||||
}
|
||||
|
||||
//nolint:gocritic // OAuth2 system context — dynamic registration is a public endpoint
|
||||
_, err = db.InsertOAuth2ProviderAppSecret(dbauthz.AsSystemOAuth2(ctx), database.InsertOAuth2ProviderAppSecretParams{
|
||||
//nolint:gocritic // Dynamic client registration is a public endpoint, system access required
|
||||
_, err = db.InsertOAuth2ProviderAppSecret(dbauthz.AsSystemRestricted(ctx), database.InsertOAuth2ProviderAppSecretParams{
|
||||
ID: uuid.New(),
|
||||
CreatedAt: now,
|
||||
SecretPrefix: []byte(parsedSecret.Prefix),
|
||||
@@ -183,8 +183,8 @@ func GetClientConfiguration(db database.Store) http.HandlerFunc {
|
||||
}
|
||||
|
||||
// Get app by client ID
|
||||
//nolint:gocritic // OAuth2 system context — RFC 7592 client configuration endpoint
|
||||
app, err := db.GetOAuth2ProviderAppByClientID(dbauthz.AsSystemOAuth2(ctx), clientID)
|
||||
//nolint:gocritic // RFC 7592 endpoints need system access to retrieve dynamically registered clients
|
||||
app, err := db.GetOAuth2ProviderAppByClientID(dbauthz.AsSystemRestricted(ctx), clientID)
|
||||
if err != nil {
|
||||
if xerrors.Is(err, sql.ErrNoRows) {
|
||||
writeOAuth2RegistrationError(ctx, rw, http.StatusUnauthorized,
|
||||
@@ -269,8 +269,8 @@ func UpdateClientConfiguration(db database.Store, auditor *audit.Auditor, logger
|
||||
req = req.ApplyDefaults()
|
||||
|
||||
// Get existing app to verify it exists and is dynamically registered
|
||||
//nolint:gocritic // OAuth2 system context — RFC 7592 client configuration endpoint
|
||||
existingApp, err := db.GetOAuth2ProviderAppByClientID(dbauthz.AsSystemOAuth2(ctx), clientID)
|
||||
//nolint:gocritic // RFC 7592 endpoints need system access to retrieve dynamically registered clients
|
||||
existingApp, err := db.GetOAuth2ProviderAppByClientID(dbauthz.AsSystemRestricted(ctx), clientID)
|
||||
if err == nil {
|
||||
aReq.Old = existingApp
|
||||
}
|
||||
@@ -294,8 +294,8 @@ func UpdateClientConfiguration(db database.Store, auditor *audit.Auditor, logger
|
||||
|
||||
// Update app in database
|
||||
now := dbtime.Now()
|
||||
//nolint:gocritic // OAuth2 system context — RFC 7592 client configuration endpoint
|
||||
updatedApp, err := db.UpdateOAuth2ProviderAppByClientID(dbauthz.AsSystemOAuth2(ctx), database.UpdateOAuth2ProviderAppByClientIDParams{
|
||||
//nolint:gocritic // RFC 7592 endpoints need system access to update dynamically registered clients
|
||||
updatedApp, err := db.UpdateOAuth2ProviderAppByClientID(dbauthz.AsSystemRestricted(ctx), database.UpdateOAuth2ProviderAppByClientIDParams{
|
||||
ID: clientID,
|
||||
UpdatedAt: now,
|
||||
Name: req.GenerateClientName(),
|
||||
@@ -377,8 +377,8 @@ func DeleteClientConfiguration(db database.Store, auditor *audit.Auditor, logger
|
||||
}
|
||||
|
||||
// Get existing app to verify it exists and is dynamically registered
|
||||
//nolint:gocritic // OAuth2 system context — RFC 7592 client configuration endpoint
|
||||
existingApp, err := db.GetOAuth2ProviderAppByClientID(dbauthz.AsSystemOAuth2(ctx), clientID)
|
||||
//nolint:gocritic // RFC 7592 endpoints need system access to retrieve dynamically registered clients
|
||||
existingApp, err := db.GetOAuth2ProviderAppByClientID(dbauthz.AsSystemRestricted(ctx), clientID)
|
||||
if err == nil {
|
||||
aReq.Old = existingApp
|
||||
}
|
||||
@@ -401,8 +401,8 @@ func DeleteClientConfiguration(db database.Store, auditor *audit.Auditor, logger
|
||||
}
|
||||
|
||||
// Delete the client and all associated data (tokens, secrets, etc.)
|
||||
//nolint:gocritic // OAuth2 system context — RFC 7592 client configuration endpoint
|
||||
err = db.DeleteOAuth2ProviderAppByClientID(dbauthz.AsSystemOAuth2(ctx), clientID)
|
||||
//nolint:gocritic // RFC 7592 endpoints need system access to delete dynamically registered clients
|
||||
err = db.DeleteOAuth2ProviderAppByClientID(dbauthz.AsSystemRestricted(ctx), clientID)
|
||||
if err != nil {
|
||||
writeOAuth2RegistrationError(ctx, rw, http.StatusInternalServerError,
|
||||
"server_error", "Failed to delete client")
|
||||
@@ -453,8 +453,8 @@ func RequireRegistrationAccessToken(db database.Store) func(http.Handler) http.H
|
||||
}
|
||||
|
||||
// Get the client and verify the registration access token
|
||||
//nolint:gocritic // OAuth2 system context — RFC 7592 registration access token validation
|
||||
app, err := db.GetOAuth2ProviderAppByClientID(dbauthz.AsSystemOAuth2(ctx), clientID)
|
||||
//nolint:gocritic // RFC 7592 endpoints need system access to validate dynamically registered clients
|
||||
app, err := db.GetOAuth2ProviderAppByClientID(dbauthz.AsSystemRestricted(ctx), clientID)
|
||||
if err != nil {
|
||||
if xerrors.Is(err, sql.ErrNoRows) {
|
||||
// Return 401 for authentication-related issues, not 404
|
||||
|
||||
@@ -217,8 +217,8 @@ func authorizationCodeGrant(ctx context.Context, db database.Store, app database
|
||||
if err != nil {
|
||||
return codersdk.OAuth2TokenResponse{}, errBadSecret
|
||||
}
|
||||
//nolint:gocritic // OAuth2 system context — users cannot read secrets
|
||||
dbSecret, err := db.GetOAuth2ProviderAppSecretByPrefix(dbauthz.AsSystemOAuth2(ctx), []byte(secret.Prefix))
|
||||
//nolint:gocritic // Users cannot read secrets so we must use the system.
|
||||
dbSecret, err := db.GetOAuth2ProviderAppSecretByPrefix(dbauthz.AsSystemRestricted(ctx), []byte(secret.Prefix))
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return codersdk.OAuth2TokenResponse{}, errBadSecret
|
||||
}
|
||||
@@ -236,8 +236,8 @@ func authorizationCodeGrant(ctx context.Context, db database.Store, app database
|
||||
if err != nil {
|
||||
return codersdk.OAuth2TokenResponse{}, errBadCode
|
||||
}
|
||||
//nolint:gocritic // OAuth2 system context — no authenticated user during token exchange
|
||||
dbCode, err := db.GetOAuth2ProviderAppCodeByPrefix(dbauthz.AsSystemOAuth2(ctx), []byte(code.Prefix))
|
||||
//nolint:gocritic // There is no user yet so we must use the system.
|
||||
dbCode, err := db.GetOAuth2ProviderAppCodeByPrefix(dbauthz.AsSystemRestricted(ctx), []byte(code.Prefix))
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return codersdk.OAuth2TokenResponse{}, errBadCode
|
||||
}
|
||||
@@ -384,8 +384,8 @@ func refreshTokenGrant(ctx context.Context, db database.Store, app database.OAut
|
||||
if err != nil {
|
||||
return codersdk.OAuth2TokenResponse{}, errBadToken
|
||||
}
|
||||
//nolint:gocritic // OAuth2 system context — no authenticated user during refresh
|
||||
dbToken, err := db.GetOAuth2ProviderAppTokenByPrefix(dbauthz.AsSystemOAuth2(ctx), []byte(token.Prefix))
|
||||
//nolint:gocritic // There is no user yet so we must use the system.
|
||||
dbToken, err := db.GetOAuth2ProviderAppTokenByPrefix(dbauthz.AsSystemRestricted(ctx), []byte(token.Prefix))
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return codersdk.OAuth2TokenResponse{}, errBadToken
|
||||
}
|
||||
@@ -411,8 +411,8 @@ func refreshTokenGrant(ctx context.Context, db database.Store, app database.OAut
|
||||
}
|
||||
|
||||
// Grab the user roles so we can perform the refresh as the user.
|
||||
//nolint:gocritic // OAuth2 system context — need to read the previous API key
|
||||
prevKey, err := db.GetAPIKeyByID(dbauthz.AsSystemOAuth2(ctx), dbToken.APIKeyID)
|
||||
//nolint:gocritic // There is no user yet so we must use the system.
|
||||
prevKey, err := db.GetAPIKeyByID(dbauthz.AsSystemRestricted(ctx), dbToken.APIKeyID)
|
||||
if err != nil {
|
||||
return codersdk.OAuth2TokenResponse{}, err
|
||||
}
|
||||
|
||||
@@ -1881,8 +1881,8 @@ func (s *server) completeTemplateImportJob(ctx context.Context, job database.Pro
|
||||
hashBytes := sha256.Sum256(moduleFiles)
|
||||
hash := hex.EncodeToString(hashBytes[:])
|
||||
|
||||
//nolint:gocritic // Acting as provisionerd
|
||||
file, err := db.GetFileByHashAndCreator(dbauthz.AsProvisionerd(ctx), database.GetFileByHashAndCreatorParams{Hash: hash, CreatedBy: uuid.Nil})
|
||||
// nolint:gocritic // Requires reading "system" files
|
||||
file, err := db.GetFileByHashAndCreator(dbauthz.AsSystemRestricted(ctx), database.GetFileByHashAndCreatorParams{Hash: hash, CreatedBy: uuid.Nil})
|
||||
switch {
|
||||
case err == nil:
|
||||
// This set of modules is already cached, which means we can reuse them
|
||||
@@ -1893,8 +1893,8 @@ func (s *server) completeTemplateImportJob(ctx context.Context, job database.Pro
|
||||
case !xerrors.Is(err, sql.ErrNoRows):
|
||||
return xerrors.Errorf("check for cached modules: %w", err)
|
||||
default:
|
||||
//nolint:gocritic // Acting as provisionerd
|
||||
file, err = db.InsertFile(dbauthz.AsProvisionerd(ctx), database.InsertFileParams{
|
||||
// nolint:gocritic // Requires creating a "system" file
|
||||
file, err = db.InsertFile(dbauthz.AsSystemRestricted(ctx), database.InsertFileParams{
|
||||
ID: uuid.New(),
|
||||
Hash: hash,
|
||||
CreatedBy: uuid.Nil,
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
package pubsub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
// ChatConfigEventChannel is the pubsub channel for chat config
|
||||
// changes (providers, model configs, user prompts). All replicas
|
||||
// subscribe to this channel to invalidate their local caches.
|
||||
const ChatConfigEventChannel = "chat:config_change"
|
||||
|
||||
// HandleChatConfigEvent wraps a typed callback for ChatConfigEvent
|
||||
// messages, following the same pattern as HandleChatEvent.
|
||||
func HandleChatConfigEvent(cb func(ctx context.Context, payload ChatConfigEvent, err error)) func(ctx context.Context, message []byte, err error) {
|
||||
return func(ctx context.Context, message []byte, err error) {
|
||||
if err != nil {
|
||||
cb(ctx, ChatConfigEvent{}, xerrors.Errorf("chat config event pubsub: %w", err))
|
||||
return
|
||||
}
|
||||
var payload ChatConfigEvent
|
||||
if err := json.Unmarshal(message, &payload); err != nil {
|
||||
cb(ctx, ChatConfigEvent{}, xerrors.Errorf("unmarshal chat config event: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
cb(ctx, payload, err)
|
||||
}
|
||||
}
|
||||
|
||||
// ChatConfigEvent is published when chat configuration changes
|
||||
// (provider CRUD, model config CRUD, or user prompt updates).
|
||||
// Subscribers use this to invalidate their local caches.
|
||||
type ChatConfigEvent struct {
|
||||
Kind ChatConfigEventKind `json:"kind"`
|
||||
// EntityID carries context for the invalidation:
|
||||
// - For providers: uuid.Nil (all providers are invalidated).
|
||||
// - For model configs: the specific config ID.
|
||||
// - For user prompts: the user ID.
|
||||
EntityID uuid.UUID `json:"entity_id"`
|
||||
}
|
||||
|
||||
type ChatConfigEventKind string
|
||||
|
||||
const (
|
||||
ChatConfigEventProviders ChatConfigEventKind = "providers"
|
||||
ChatConfigEventModelConfig ChatConfigEventKind = "model_config"
|
||||
ChatConfigEventUserPrompt ChatConfigEventKind = "user_prompt"
|
||||
)
|
||||
@@ -82,16 +82,6 @@ var (
|
||||
Type: "chat",
|
||||
}
|
||||
|
||||
// ResourceChatAutomation
|
||||
// Valid Actions
|
||||
// - "ActionCreate" :: create a chat automation
|
||||
// - "ActionDelete" :: delete a chat automation
|
||||
// - "ActionRead" :: read chat automation configuration
|
||||
// - "ActionUpdate" :: update a chat automation
|
||||
ResourceChatAutomation = Object{
|
||||
Type: "chat_automation",
|
||||
}
|
||||
|
||||
// ResourceConnectionLog
|
||||
// Valid Actions
|
||||
// - "ActionRead" :: read connection logs
|
||||
@@ -450,7 +440,6 @@ func AllResources() []Objecter {
|
||||
ResourceAuditLog,
|
||||
ResourceBoundaryUsage,
|
||||
ResourceChat,
|
||||
ResourceChatAutomation,
|
||||
ResourceConnectionLog,
|
||||
ResourceCryptoKey,
|
||||
ResourceDebugInfo,
|
||||
|
||||
@@ -84,13 +84,6 @@ var chatActions = map[Action]ActionDefinition{
|
||||
ActionDelete: "delete a chat",
|
||||
}
|
||||
|
||||
var chatAutomationActions = map[Action]ActionDefinition{
|
||||
ActionCreate: "create a chat automation",
|
||||
ActionRead: "read chat automation configuration",
|
||||
ActionUpdate: "update a chat automation",
|
||||
ActionDelete: "delete a chat automation",
|
||||
}
|
||||
|
||||
// RBACPermissions is indexed by the type
|
||||
var RBACPermissions = map[string]PermissionDefinition{
|
||||
// Wildcard is every object, and the action "*" provides all actions.
|
||||
@@ -120,9 +113,6 @@ var RBACPermissions = map[string]PermissionDefinition{
|
||||
"chat": {
|
||||
Actions: chatActions,
|
||||
},
|
||||
"chat_automation": {
|
||||
Actions: chatAutomationActions,
|
||||
},
|
||||
// Dormant workspaces have the same perms as workspaces.
|
||||
"workspace_dormant": {
|
||||
Actions: workspaceActions,
|
||||
|
||||
+4
-38
@@ -21,7 +21,6 @@ const (
|
||||
templateAdmin string = "template-admin"
|
||||
userAdmin string = "user-admin"
|
||||
auditor string = "auditor"
|
||||
agentsAccess string = "agents-access"
|
||||
// customSiteRole is a placeholder for all custom site roles.
|
||||
// This is used for what roles can assign other roles.
|
||||
// TODO: Make this more dynamic to allow other roles to grant.
|
||||
@@ -143,7 +142,6 @@ func RoleTemplateAdmin() RoleIdentifier { return RoleIdentifier{Name: templateAd
|
||||
func RoleUserAdmin() RoleIdentifier { return RoleIdentifier{Name: userAdmin} }
|
||||
func RoleMember() RoleIdentifier { return RoleIdentifier{Name: member} }
|
||||
func RoleAuditor() RoleIdentifier { return RoleIdentifier{Name: auditor} }
|
||||
func RoleAgentsAccess() RoleIdentifier { return RoleIdentifier{Name: agentsAccess} }
|
||||
|
||||
func RoleOrgAdmin() string {
|
||||
return orgAdmin
|
||||
@@ -318,7 +316,7 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
|
||||
denyPermissions...,
|
||||
),
|
||||
User: append(
|
||||
allPermsExcept(ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceWorkspace, ResourceUser, ResourceOrganizationMember, ResourceBoundaryUsage, ResourceAibridgeInterception, ResourceChat),
|
||||
allPermsExcept(ResourceWorkspaceDormant, ResourcePrebuiltWorkspace, ResourceWorkspace, ResourceUser, ResourceOrganizationMember, ResourceOrganizationMember, ResourceBoundaryUsage, ResourceAibridgeInterception),
|
||||
Permissions(map[string][]policy.Action{
|
||||
// Users cannot do create/update/delete on themselves, but they
|
||||
// can read their own details.
|
||||
@@ -404,21 +402,6 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
|
||||
ByOrgID: map[string]OrgPermissions{},
|
||||
}.withCachedRegoValue()
|
||||
|
||||
agentsAccessRole := Role{
|
||||
Identifier: RoleAgentsAccess(),
|
||||
DisplayName: "Coder Agents User",
|
||||
Site: []Permission{},
|
||||
User: Permissions(map[string][]policy.Action{
|
||||
ResourceChat.Type: {
|
||||
policy.ActionCreate,
|
||||
policy.ActionRead,
|
||||
policy.ActionUpdate,
|
||||
policy.ActionDelete,
|
||||
},
|
||||
}),
|
||||
ByOrgID: map[string]OrgPermissions{},
|
||||
}.withCachedRegoValue()
|
||||
|
||||
builtInRoles = map[string]func(orgID uuid.UUID) Role{
|
||||
// admin grants all actions to all resources.
|
||||
owner: func(_ uuid.UUID) Role {
|
||||
@@ -445,13 +428,6 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
|
||||
return userAdminRole
|
||||
},
|
||||
|
||||
// agentsAccess grants all actions on chat resources owned
|
||||
// by the user. Without this role, members cannot create
|
||||
// or interact with chats.
|
||||
agentsAccess: func(_ uuid.UUID) Role {
|
||||
return agentsAccessRole
|
||||
},
|
||||
|
||||
// orgAdmin returns a role with all actions allows in a given
|
||||
// organization scope.
|
||||
orgAdmin: func(organizationID uuid.UUID) Role {
|
||||
@@ -624,7 +600,6 @@ var assignRoles = map[string]map[string]bool{
|
||||
userAdmin: true,
|
||||
customSiteRole: true,
|
||||
customOrganizationRole: true,
|
||||
agentsAccess: true,
|
||||
},
|
||||
owner: {
|
||||
owner: true,
|
||||
@@ -640,12 +615,10 @@ var assignRoles = map[string]map[string]bool{
|
||||
userAdmin: true,
|
||||
customSiteRole: true,
|
||||
customOrganizationRole: true,
|
||||
agentsAccess: true,
|
||||
},
|
||||
userAdmin: {
|
||||
member: true,
|
||||
orgMember: true,
|
||||
agentsAccess: true,
|
||||
member: true,
|
||||
orgMember: true,
|
||||
},
|
||||
orgAdmin: {
|
||||
orgAdmin: true,
|
||||
@@ -881,20 +854,13 @@ func SiteBuiltInRoles() []Role {
|
||||
for _, roleF := range builtInRoles {
|
||||
// Must provide some non-nil uuid to filter out org roles.
|
||||
role := roleF(uuid.New())
|
||||
if !role.Identifier.IsOrgRole() && role.Identifier != RoleAgentsAccess() {
|
||||
if !role.Identifier.IsOrgRole() {
|
||||
roles = append(roles, role)
|
||||
}
|
||||
}
|
||||
return roles
|
||||
}
|
||||
|
||||
// AgentsAccessRole returns the agents-access role for use by callers
|
||||
// that need to include it conditionally (e.g. when the agents
|
||||
// experiment is enabled).
|
||||
func AgentsAccessRole() Role {
|
||||
return builtInRoles[agentsAccess](uuid.Nil)
|
||||
}
|
||||
|
||||
// ChangeRoleSet is a helper function that finds the difference of 2 sets of
|
||||
// roles. When setting a user's new roles, it is equivalent to adding and
|
||||
// removing roles. This set determines the changes, so that the appropriate
|
||||
|
||||
+82
-100
@@ -49,11 +49,6 @@ func TestBuiltInRoles(t *testing.T) {
|
||||
require.NoError(t, r.Valid(), "invalid role")
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("agents-access", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
require.NoError(t, rbac.AgentsAccessRole().Valid(), "invalid role")
|
||||
})
|
||||
}
|
||||
|
||||
// permissionGranted checks whether a permission list contains a
|
||||
@@ -204,7 +199,6 @@ func TestRolePermissions(t *testing.T) {
|
||||
orgUserAdmin := authSubject{Name: "org_user_admin", Actor: rbac.Subject{ID: templateAdminID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.ScopedRoleOrgUserAdmin(orgID)}, Scope: rbac.ScopeAll}.WithCachedASTValue()}
|
||||
orgTemplateAdmin := authSubject{Name: "org_template_admin", Actor: rbac.Subject{ID: userAdminID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.ScopedRoleOrgTemplateAdmin(orgID)}, Scope: rbac.ScopeAll}.WithCachedASTValue()}
|
||||
orgAdminBanWorkspace := authSubject{Name: "org_admin_workspace_ban", Actor: rbac.Subject{ID: adminID.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.ScopedRoleOrgAdmin(orgID), rbac.ScopedRoleOrgWorkspaceCreationBan(orgID)}, Scope: rbac.ScopeAll}.WithCachedASTValue()}
|
||||
agentsAccessUser := authSubject{Name: "chat_access", Actor: rbac.Subject{ID: currentUser.String(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.RoleAgentsAccess()}, Scope: rbac.ScopeAll}.WithCachedASTValue()}
|
||||
setOrgNotMe := authSubjectSet{orgAdmin, orgAuditor, orgUserAdmin, orgTemplateAdmin}
|
||||
|
||||
otherOrgAdmin := authSubject{Name: "org_admin_other", Actor: rbac.Subject{ID: uuid.NewString(), Roles: rbac.RoleIdentifiers{rbac.RoleMember(), rbac.ScopedRoleOrgAdmin(otherOrg)}, Scope: rbac.ScopeAll}.WithCachedASTValue()}
|
||||
@@ -216,7 +210,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
// requiredSubjects are required to be asserted in each test case. This is
|
||||
// to make sure one is not forgotten.
|
||||
requiredSubjects := []authSubject{
|
||||
memberMe, owner, agentsAccessUser,
|
||||
memberMe, owner,
|
||||
orgAdmin, otherOrgAdmin, orgAuditor, orgUserAdmin, orgTemplateAdmin,
|
||||
templateAdmin, userAdmin, otherOrgAuditor, otherOrgUserAdmin, otherOrgTemplateAdmin,
|
||||
}
|
||||
@@ -239,7 +233,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Actions: []policy.Action{policy.ActionRead},
|
||||
Resource: rbac.ResourceUserObject(currentUser),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgUserAdmin, otherOrgAdmin, otherOrgUserAdmin, orgAdmin},
|
||||
true: {owner, memberMe, templateAdmin, userAdmin, orgUserAdmin, otherOrgAdmin, otherOrgUserAdmin, orgAdmin},
|
||||
false: {
|
||||
orgTemplateAdmin, orgAuditor,
|
||||
otherOrgAuditor, otherOrgTemplateAdmin,
|
||||
@@ -252,7 +246,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Resource: rbac.ResourceUser,
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, userAdmin},
|
||||
false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin},
|
||||
false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -262,7 +256,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Resource: rbac.ResourceWorkspace.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, orgAdmin, templateAdmin, orgTemplateAdmin, orgAdminBanWorkspace},
|
||||
false: {setOtherOrg, memberMe, agentsAccessUser, userAdmin, orgAuditor, orgUserAdmin},
|
||||
false: {setOtherOrg, memberMe, userAdmin, orgAuditor, orgUserAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -272,7 +266,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Resource: rbac.ResourceWorkspace.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, orgAdmin, orgAdminBanWorkspace},
|
||||
false: {setOtherOrg, memberMe, agentsAccessUser, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor},
|
||||
false: {setOtherOrg, memberMe, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -282,7 +276,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Resource: rbac.ResourceWorkspace.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, orgAdmin},
|
||||
false: {setOtherOrg, memberMe, agentsAccessUser, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor, orgAdminBanWorkspace},
|
||||
false: {setOtherOrg, memberMe, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor, orgAdminBanWorkspace},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -292,7 +286,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Resource: rbac.ResourceWorkspace.InOrg(orgID).WithOwner(policy.WildcardSymbol),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, orgAdmin},
|
||||
false: {setOtherOrg, orgUserAdmin, orgAuditor, memberMe, agentsAccessUser, userAdmin, templateAdmin, orgTemplateAdmin},
|
||||
false: {setOtherOrg, orgUserAdmin, orgAuditor, memberMe, userAdmin, templateAdmin, orgTemplateAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -302,7 +296,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Resource: rbac.ResourceWorkspace.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner},
|
||||
false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin},
|
||||
false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -312,7 +306,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Resource: rbac.ResourceWorkspace.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner},
|
||||
false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin},
|
||||
false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -321,7 +315,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Resource: rbac.ResourceWorkspace.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, orgAdmin},
|
||||
false: {setOtherOrg, memberMe, agentsAccessUser, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor, orgAdminBanWorkspace},
|
||||
false: {setOtherOrg, memberMe, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor, orgAdminBanWorkspace},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -330,7 +324,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Resource: rbac.ResourceWorkspace.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, orgAdmin, orgAdminBanWorkspace},
|
||||
false: {setOtherOrg, memberMe, agentsAccessUser, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor},
|
||||
false: {setOtherOrg, memberMe, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -343,7 +337,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, orgAdmin, orgAdminBanWorkspace},
|
||||
false: {
|
||||
memberMe, agentsAccessUser, setOtherOrg,
|
||||
memberMe, setOtherOrg,
|
||||
templateAdmin, userAdmin,
|
||||
orgTemplateAdmin, orgUserAdmin, orgAuditor,
|
||||
},
|
||||
@@ -360,7 +354,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
true: {},
|
||||
false: {
|
||||
orgAdmin, owner, setOtherOrg,
|
||||
userAdmin, memberMe, agentsAccessUser,
|
||||
userAdmin, memberMe,
|
||||
templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor,
|
||||
orgAdminBanWorkspace,
|
||||
},
|
||||
@@ -372,7 +366,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Resource: rbac.ResourceTemplate.WithID(templateID).InOrg(orgID),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, orgAdmin, templateAdmin, orgTemplateAdmin},
|
||||
false: {setOtherOrg, orgUserAdmin, orgAuditor, memberMe, agentsAccessUser, userAdmin},
|
||||
false: {setOtherOrg, orgUserAdmin, orgAuditor, memberMe, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -381,7 +375,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Resource: rbac.ResourceTemplate.InOrg(orgID),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, orgAuditor, orgAdmin, templateAdmin, orgTemplateAdmin},
|
||||
false: {setOtherOrg, orgUserAdmin, memberMe, agentsAccessUser, userAdmin},
|
||||
false: {setOtherOrg, orgUserAdmin, memberMe, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -392,7 +386,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
}),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, orgAdmin, templateAdmin, orgTemplateAdmin},
|
||||
false: {setOtherOrg, orgAuditor, orgUserAdmin, memberMe, agentsAccessUser, userAdmin},
|
||||
false: {setOtherOrg, orgAuditor, orgUserAdmin, memberMe, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -403,7 +397,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
true: {owner, templateAdmin},
|
||||
// Org template admins can only read org scoped files.
|
||||
// File scope is currently not org scoped :cry:
|
||||
false: {setOtherOrg, orgTemplateAdmin, orgAdmin, memberMe, agentsAccessUser, userAdmin, orgAuditor, orgUserAdmin},
|
||||
false: {setOtherOrg, orgTemplateAdmin, orgAdmin, memberMe, userAdmin, orgAuditor, orgUserAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -411,7 +405,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Actions: []policy.Action{policy.ActionCreate, policy.ActionRead},
|
||||
Resource: rbac.ResourceFile.WithID(fileID).WithOwner(currentUser.String()),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, memberMe, agentsAccessUser, templateAdmin},
|
||||
true: {owner, memberMe, templateAdmin},
|
||||
false: {setOtherOrg, setOrgNotMe, userAdmin},
|
||||
},
|
||||
},
|
||||
@@ -421,7 +415,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Resource: rbac.ResourceOrganization,
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner},
|
||||
false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin},
|
||||
false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -430,7 +424,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Resource: rbac.ResourceOrganization.WithID(orgID).InOrg(orgID),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, orgAdmin},
|
||||
false: {setOtherOrg, orgTemplateAdmin, orgUserAdmin, orgAuditor, memberMe, agentsAccessUser, templateAdmin, userAdmin},
|
||||
false: {setOtherOrg, orgTemplateAdmin, orgUserAdmin, orgAuditor, memberMe, templateAdmin, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -439,7 +433,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Resource: rbac.ResourceOrganization.WithID(orgID).InOrg(orgID),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, orgAdmin, templateAdmin, orgTemplateAdmin, auditor, orgAuditor, userAdmin, orgUserAdmin},
|
||||
false: {setOtherOrg, memberMe, agentsAccessUser},
|
||||
false: {setOtherOrg, memberMe},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -448,7 +442,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Resource: rbac.ResourceAssignOrgRole,
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner},
|
||||
false: {setOtherOrg, setOrgNotMe, userAdmin, memberMe, agentsAccessUser, templateAdmin},
|
||||
false: {setOtherOrg, setOrgNotMe, userAdmin, memberMe, templateAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -457,7 +451,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Resource: rbac.ResourceAssignRole,
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, userAdmin},
|
||||
false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin},
|
||||
false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -465,7 +459,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Actions: []policy.Action{policy.ActionRead},
|
||||
Resource: rbac.ResourceAssignRole,
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {setOtherOrg, setOrgNotMe, owner, memberMe, agentsAccessUser, templateAdmin, userAdmin},
|
||||
true: {setOtherOrg, setOrgNotMe, owner, memberMe, templateAdmin, userAdmin},
|
||||
false: {},
|
||||
},
|
||||
},
|
||||
@@ -475,7 +469,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Resource: rbac.ResourceAssignOrgRole.InOrg(orgID),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, orgAdmin, userAdmin, orgUserAdmin},
|
||||
false: {setOtherOrg, memberMe, agentsAccessUser, templateAdmin, orgTemplateAdmin, orgAuditor},
|
||||
false: {setOtherOrg, memberMe, templateAdmin, orgTemplateAdmin, orgAuditor},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -484,7 +478,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Resource: rbac.ResourceAssignOrgRole.InOrg(orgID),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, orgAdmin},
|
||||
false: {setOtherOrg, orgUserAdmin, orgTemplateAdmin, orgAuditor, memberMe, agentsAccessUser, templateAdmin, userAdmin},
|
||||
false: {setOtherOrg, orgUserAdmin, orgTemplateAdmin, orgAuditor, memberMe, templateAdmin, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -493,7 +487,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Resource: rbac.ResourceAssignOrgRole.InOrg(orgID),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, orgAdmin, orgUserAdmin, userAdmin, templateAdmin},
|
||||
false: {setOtherOrg, memberMe, agentsAccessUser, orgAuditor, orgTemplateAdmin},
|
||||
false: {setOtherOrg, memberMe, orgAuditor, orgTemplateAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -501,7 +495,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionDelete, policy.ActionUpdate},
|
||||
Resource: rbac.ResourceApiKey.WithID(apiKeyID).WithOwner(currentUser.String()),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, memberMe, agentsAccessUser},
|
||||
true: {owner, memberMe},
|
||||
false: {setOtherOrg, setOrgNotMe, templateAdmin, userAdmin},
|
||||
},
|
||||
},
|
||||
@@ -513,7 +507,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Resource: rbac.ResourceInboxNotification.WithID(uuid.New()).InOrg(orgID).WithOwner(currentUser.String()),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, orgAdmin},
|
||||
false: {setOtherOrg, orgUserAdmin, orgTemplateAdmin, orgAuditor, templateAdmin, userAdmin, memberMe, agentsAccessUser},
|
||||
false: {setOtherOrg, orgUserAdmin, orgTemplateAdmin, orgAuditor, templateAdmin, userAdmin, memberMe},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -521,7 +515,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Actions: []policy.Action{policy.ActionReadPersonal, policy.ActionUpdatePersonal},
|
||||
Resource: rbac.ResourceUserObject(currentUser),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, memberMe, agentsAccessUser, userAdmin},
|
||||
true: {owner, memberMe, userAdmin},
|
||||
false: {setOtherOrg, setOrgNotMe, templateAdmin},
|
||||
},
|
||||
},
|
||||
@@ -531,7 +525,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Resource: rbac.ResourceOrganizationMember.WithID(currentUser).InOrg(orgID).WithOwner(currentUser.String()),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, orgAdmin, userAdmin, orgUserAdmin},
|
||||
false: {setOtherOrg, orgTemplateAdmin, orgAuditor, memberMe, agentsAccessUser, templateAdmin},
|
||||
false: {setOtherOrg, orgTemplateAdmin, orgAuditor, memberMe, templateAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -540,7 +534,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Resource: rbac.ResourceOrganizationMember.WithID(currentUser).InOrg(orgID).WithOwner(currentUser.String()),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, orgAuditor, orgAdmin, userAdmin, templateAdmin, orgUserAdmin, orgTemplateAdmin},
|
||||
false: {memberMe, agentsAccessUser, setOtherOrg},
|
||||
false: {memberMe, setOtherOrg},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -553,7 +547,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, orgAdmin, templateAdmin, orgUserAdmin, orgTemplateAdmin, orgAuditor},
|
||||
false: {setOtherOrg, memberMe, agentsAccessUser, userAdmin},
|
||||
false: {setOtherOrg, memberMe, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -566,7 +560,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
}),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, orgAdmin, userAdmin, orgUserAdmin},
|
||||
false: {setOtherOrg, memberMe, agentsAccessUser, templateAdmin, orgTemplateAdmin, orgAuditor},
|
||||
false: {setOtherOrg, memberMe, templateAdmin, orgTemplateAdmin, orgAuditor},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -579,7 +573,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
}),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, orgAdmin, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor},
|
||||
false: {setOtherOrg, memberMe, agentsAccessUser},
|
||||
false: {setOtherOrg, memberMe},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -588,7 +582,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Resource: rbac.ResourceGroupMember.WithID(currentUser).InOrg(orgID).WithOwner(currentUser.String()),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, orgAuditor, orgAdmin, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin},
|
||||
false: {setOtherOrg, memberMe, agentsAccessUser},
|
||||
false: {setOtherOrg, memberMe},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -597,7 +591,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Resource: rbac.ResourceGroupMember.WithID(adminID).InOrg(orgID).WithOwner(adminID.String()),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, orgAuditor, orgAdmin, userAdmin, templateAdmin, orgTemplateAdmin, orgUserAdmin},
|
||||
false: {setOtherOrg, memberMe, agentsAccessUser},
|
||||
false: {setOtherOrg, memberMe},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -606,7 +600,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Resource: rbac.ResourceWorkspaceDormant.WithID(uuid.New()).InOrg(orgID).WithOwner(memberMe.Actor.ID),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {orgAdmin, owner},
|
||||
false: {setOtherOrg, userAdmin, memberMe, agentsAccessUser, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor},
|
||||
false: {setOtherOrg, userAdmin, memberMe, templateAdmin, orgTemplateAdmin, orgUserAdmin, orgAuditor},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -615,7 +609,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Resource: rbac.ResourceWorkspaceDormant.WithID(uuid.New()).InOrg(orgID).WithOwner(memberMe.Actor.ID),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {},
|
||||
false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, userAdmin, owner, templateAdmin},
|
||||
false: {setOtherOrg, setOrgNotMe, memberMe, userAdmin, owner, templateAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -624,7 +618,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Resource: rbac.ResourceWorkspace.WithID(uuid.New()).InOrg(orgID).WithOwner(memberMe.Actor.ID),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, orgAdmin},
|
||||
false: {setOtherOrg, userAdmin, templateAdmin, memberMe, agentsAccessUser, orgTemplateAdmin, orgUserAdmin, orgAuditor},
|
||||
false: {setOtherOrg, userAdmin, templateAdmin, memberMe, orgTemplateAdmin, orgUserAdmin, orgAuditor},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -633,7 +627,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Resource: rbac.ResourcePrebuiltWorkspace.WithID(uuid.New()).InOrg(orgID).WithOwner(database.PrebuildsSystemUserID.String()),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, orgAdmin, templateAdmin, orgTemplateAdmin},
|
||||
false: {setOtherOrg, userAdmin, memberMe, agentsAccessUser, orgUserAdmin, orgAuditor},
|
||||
false: {setOtherOrg, userAdmin, memberMe, orgUserAdmin, orgAuditor},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -642,7 +636,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Resource: rbac.ResourceTask.WithID(uuid.New()).InOrg(orgID).WithOwner(memberMe.Actor.ID),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, orgAdmin},
|
||||
false: {setOtherOrg, userAdmin, templateAdmin, memberMe, agentsAccessUser, orgTemplateAdmin, orgUserAdmin, orgAuditor},
|
||||
false: {setOtherOrg, userAdmin, templateAdmin, memberMe, orgTemplateAdmin, orgUserAdmin, orgAuditor},
|
||||
},
|
||||
},
|
||||
// Some admin style resources
|
||||
@@ -652,7 +646,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Resource: rbac.ResourceLicense,
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner},
|
||||
false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin},
|
||||
false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -661,7 +655,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Resource: rbac.ResourceDeploymentStats,
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner},
|
||||
false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin},
|
||||
false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -670,7 +664,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Resource: rbac.ResourceDeploymentConfig,
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner},
|
||||
false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin},
|
||||
false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -679,7 +673,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Resource: rbac.ResourceDebugInfo,
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner},
|
||||
false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin},
|
||||
false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -688,7 +682,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Resource: rbac.ResourceReplicas,
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner},
|
||||
false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin},
|
||||
false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -697,7 +691,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Resource: rbac.ResourceTailnetCoordinator,
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner},
|
||||
false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin},
|
||||
false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -706,7 +700,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Resource: rbac.ResourceAuditLog,
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner},
|
||||
false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin},
|
||||
false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -715,7 +709,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Resource: rbac.ResourceProvisionerDaemon.InOrg(orgID),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, templateAdmin, orgAdmin, orgTemplateAdmin},
|
||||
false: {setOtherOrg, orgAuditor, orgUserAdmin, memberMe, agentsAccessUser, userAdmin},
|
||||
false: {setOtherOrg, orgAuditor, orgUserAdmin, memberMe, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -724,7 +718,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Resource: rbac.ResourceProvisionerDaemon.InOrg(orgID),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, templateAdmin, orgAdmin, orgTemplateAdmin},
|
||||
false: {setOtherOrg, memberMe, agentsAccessUser, userAdmin, orgAuditor, orgUserAdmin},
|
||||
false: {setOtherOrg, memberMe, userAdmin, orgAuditor, orgUserAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -733,7 +727,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Resource: rbac.ResourceProvisionerDaemon.WithOwner(currentUser.String()).InOrg(orgID),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, templateAdmin, orgTemplateAdmin, orgAdmin},
|
||||
false: {setOtherOrg, memberMe, agentsAccessUser, userAdmin, orgUserAdmin, orgAuditor},
|
||||
false: {setOtherOrg, memberMe, userAdmin, orgUserAdmin, orgAuditor},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -742,7 +736,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Resource: rbac.ResourceProvisionerJobs.InOrg(orgID),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, orgTemplateAdmin, orgAdmin},
|
||||
false: {setOtherOrg, memberMe, agentsAccessUser, templateAdmin, userAdmin, orgUserAdmin, orgAuditor},
|
||||
false: {setOtherOrg, memberMe, templateAdmin, userAdmin, orgUserAdmin, orgAuditor},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -751,7 +745,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Resource: rbac.ResourceSystem,
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner},
|
||||
false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin},
|
||||
false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -760,7 +754,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Resource: rbac.ResourceOauth2App,
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner},
|
||||
false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin},
|
||||
false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -768,7 +762,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Actions: []policy.Action{policy.ActionRead},
|
||||
Resource: rbac.ResourceOauth2App,
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, setOrgNotMe, setOtherOrg, memberMe, agentsAccessUser, templateAdmin, userAdmin},
|
||||
true: {owner, setOrgNotMe, setOtherOrg, memberMe, templateAdmin, userAdmin},
|
||||
false: {},
|
||||
},
|
||||
},
|
||||
@@ -778,7 +772,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Resource: rbac.ResourceOauth2AppSecret,
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner},
|
||||
false: {setOrgNotMe, setOtherOrg, memberMe, agentsAccessUser, templateAdmin, userAdmin},
|
||||
false: {setOrgNotMe, setOtherOrg, memberMe, templateAdmin, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -787,7 +781,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Resource: rbac.ResourceOauth2AppCodeToken,
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner},
|
||||
false: {setOrgNotMe, setOtherOrg, memberMe, agentsAccessUser, templateAdmin, userAdmin},
|
||||
false: {setOrgNotMe, setOtherOrg, memberMe, templateAdmin, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -796,7 +790,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Resource: rbac.ResourceWorkspaceProxy,
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner},
|
||||
false: {setOrgNotMe, setOtherOrg, memberMe, agentsAccessUser, templateAdmin, userAdmin},
|
||||
false: {setOrgNotMe, setOtherOrg, memberMe, templateAdmin, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -804,7 +798,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Actions: []policy.Action{policy.ActionRead},
|
||||
Resource: rbac.ResourceWorkspaceProxy,
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, setOrgNotMe, setOtherOrg, memberMe, agentsAccessUser, templateAdmin, userAdmin},
|
||||
true: {owner, setOrgNotMe, setOtherOrg, memberMe, templateAdmin, userAdmin},
|
||||
false: {},
|
||||
},
|
||||
},
|
||||
@@ -815,7 +809,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Actions: []policy.Action{policy.ActionRead, policy.ActionUpdate},
|
||||
Resource: rbac.ResourceNotificationPreference.WithOwner(currentUser.String()),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {memberMe, agentsAccessUser, owner},
|
||||
true: {memberMe, owner},
|
||||
false: {
|
||||
userAdmin, orgUserAdmin, templateAdmin,
|
||||
orgAuditor, orgTemplateAdmin,
|
||||
@@ -832,7 +826,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner},
|
||||
false: {
|
||||
memberMe, agentsAccessUser, userAdmin, orgUserAdmin, templateAdmin,
|
||||
memberMe, userAdmin, orgUserAdmin, templateAdmin,
|
||||
orgAuditor, orgTemplateAdmin,
|
||||
otherOrgAuditor, otherOrgUserAdmin, otherOrgTemplateAdmin,
|
||||
orgAdmin, otherOrgAdmin,
|
||||
@@ -846,7 +840,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner},
|
||||
false: {
|
||||
memberMe, agentsAccessUser,
|
||||
memberMe,
|
||||
orgAdmin, otherOrgAdmin,
|
||||
orgAuditor, otherOrgAuditor,
|
||||
templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin,
|
||||
@@ -864,7 +858,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner},
|
||||
false: {
|
||||
memberMe, agentsAccessUser, templateAdmin, orgUserAdmin, userAdmin,
|
||||
memberMe, templateAdmin, orgUserAdmin, userAdmin,
|
||||
orgAdmin, orgAuditor, orgTemplateAdmin,
|
||||
otherOrgAuditor, otherOrgUserAdmin, otherOrgTemplateAdmin,
|
||||
otherOrgAdmin,
|
||||
@@ -877,7 +871,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionDelete},
|
||||
Resource: rbac.ResourceWebpushSubscription.WithOwner(currentUser.String()),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, memberMe, agentsAccessUser},
|
||||
true: {owner, memberMe},
|
||||
false: {orgAdmin, otherOrgAdmin, orgAuditor, otherOrgAuditor, templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin, userAdmin, orgUserAdmin, otherOrgUserAdmin},
|
||||
},
|
||||
},
|
||||
@@ -889,7 +883,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, userAdmin, orgAdmin, otherOrgAdmin, orgUserAdmin, otherOrgUserAdmin},
|
||||
false: {
|
||||
memberMe, agentsAccessUser, templateAdmin,
|
||||
memberMe, templateAdmin,
|
||||
orgTemplateAdmin, orgAuditor,
|
||||
otherOrgAuditor, otherOrgTemplateAdmin,
|
||||
},
|
||||
@@ -902,7 +896,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin, orgAdmin, otherOrgAdmin},
|
||||
false: {
|
||||
userAdmin, memberMe, agentsAccessUser,
|
||||
userAdmin, memberMe,
|
||||
orgAuditor, orgUserAdmin,
|
||||
otherOrgAuditor, otherOrgUserAdmin,
|
||||
},
|
||||
@@ -915,7 +909,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, orgAdmin, otherOrgAdmin},
|
||||
false: {
|
||||
memberMe, agentsAccessUser, userAdmin, templateAdmin,
|
||||
memberMe, userAdmin, templateAdmin,
|
||||
orgAuditor, orgUserAdmin, orgTemplateAdmin,
|
||||
otherOrgAuditor, otherOrgUserAdmin, otherOrgTemplateAdmin,
|
||||
},
|
||||
@@ -927,7 +921,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Resource: rbac.ResourceCryptoKey,
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner},
|
||||
false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin},
|
||||
false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -938,7 +932,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
true: {owner, orgAdmin, orgUserAdmin, userAdmin},
|
||||
false: {
|
||||
otherOrgAdmin,
|
||||
memberMe, agentsAccessUser, templateAdmin,
|
||||
memberMe, templateAdmin,
|
||||
orgAuditor, orgTemplateAdmin,
|
||||
otherOrgAuditor, otherOrgUserAdmin, otherOrgTemplateAdmin,
|
||||
},
|
||||
@@ -953,7 +947,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
false: {
|
||||
orgAdmin, orgUserAdmin,
|
||||
otherOrgAdmin,
|
||||
memberMe, agentsAccessUser, templateAdmin,
|
||||
memberMe, templateAdmin,
|
||||
orgAuditor, orgTemplateAdmin,
|
||||
otherOrgAuditor, otherOrgUserAdmin, otherOrgTemplateAdmin,
|
||||
},
|
||||
@@ -966,7 +960,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner},
|
||||
false: {
|
||||
memberMe, agentsAccessUser,
|
||||
memberMe,
|
||||
orgAdmin, otherOrgAdmin,
|
||||
orgAuditor, otherOrgAuditor,
|
||||
templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin,
|
||||
@@ -981,7 +975,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner},
|
||||
false: {
|
||||
memberMe, agentsAccessUser,
|
||||
memberMe,
|
||||
orgAdmin, otherOrgAdmin,
|
||||
orgAuditor, otherOrgAuditor,
|
||||
templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin,
|
||||
@@ -995,7 +989,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Resource: rbac.ResourceConnectionLog,
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner},
|
||||
false: {setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin},
|
||||
false: {setOtherOrg, setOrgNotMe, memberMe, templateAdmin, userAdmin},
|
||||
},
|
||||
},
|
||||
// Only the user themselves can access their own secrets — no one else.
|
||||
@@ -1004,7 +998,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
|
||||
Resource: rbac.ResourceUserSecret.WithOwner(currentUser.String()),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {memberMe, agentsAccessUser},
|
||||
true: {memberMe},
|
||||
false: {
|
||||
owner, orgAdmin,
|
||||
otherOrgAdmin, orgAuditor, orgUserAdmin, orgTemplateAdmin,
|
||||
@@ -1020,7 +1014,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
true: {},
|
||||
false: {
|
||||
owner,
|
||||
memberMe, agentsAccessUser,
|
||||
memberMe,
|
||||
orgAdmin, otherOrgAdmin,
|
||||
orgAuditor, otherOrgAuditor,
|
||||
templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin,
|
||||
@@ -1034,7 +1028,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate},
|
||||
Resource: rbac.ResourceAibridgeInterception.WithOwner(currentUser.String()),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, memberMe, agentsAccessUser},
|
||||
true: {owner, memberMe},
|
||||
false: {
|
||||
orgAdmin, otherOrgAdmin,
|
||||
orgAuditor, otherOrgAuditor,
|
||||
@@ -1051,7 +1045,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, auditor},
|
||||
false: {
|
||||
memberMe, agentsAccessUser,
|
||||
memberMe,
|
||||
orgAdmin, otherOrgAdmin,
|
||||
orgAuditor, otherOrgAuditor,
|
||||
templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin,
|
||||
@@ -1064,7 +1058,7 @@ func TestRolePermissions(t *testing.T) {
|
||||
Actions: []policy.Action{policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
|
||||
Resource: rbac.ResourceBoundaryUsage,
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
false: {owner, setOtherOrg, setOrgNotMe, memberMe, agentsAccessUser, templateAdmin, userAdmin},
|
||||
false: {owner, setOtherOrg, setOrgNotMe, memberMe, templateAdmin, userAdmin},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -1072,9 +1066,8 @@ func TestRolePermissions(t *testing.T) {
|
||||
Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
|
||||
Resource: rbac.ResourceChat.WithOwner(currentUser.String()),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, agentsAccessUser},
|
||||
true: {owner, memberMe},
|
||||
false: {
|
||||
memberMe,
|
||||
orgAdmin, otherOrgAdmin,
|
||||
orgAuditor, otherOrgAuditor,
|
||||
templateAdmin, orgTemplateAdmin, otherOrgTemplateAdmin,
|
||||
@@ -1082,20 +1075,8 @@ func TestRolePermissions(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// Chat automations are admin-managed. Regular org
|
||||
// members cannot manage automations even if they
|
||||
// are the owner. The owner_id field is for audit
|
||||
// tracking, not RBAC grants.
|
||||
Name: "ChatAutomation",
|
||||
Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
|
||||
Resource: rbac.ResourceChatAutomation.InOrg(orgID).WithOwner(currentUser.String()),
|
||||
AuthorizeMap: map[bool][]hasAuthSubjects{
|
||||
true: {owner, orgAdmin},
|
||||
false: {setOtherOrg, memberMe, agentsAccessUser, orgAuditor, orgUserAdmin, orgTemplateAdmin, templateAdmin, userAdmin},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Build coverage set from test case definitions statically,
|
||||
// so we don't need shared mutable state during execution.
|
||||
// This allows subtests to run in parallel.
|
||||
@@ -1236,6 +1217,7 @@ func TestListRoles(t *testing.T) {
|
||||
"user-admin",
|
||||
},
|
||||
siteRoleNames)
|
||||
|
||||
orgID := uuid.New()
|
||||
orgRoles := rbac.OrganizationRoles(orgID)
|
||||
orgRoleNames := make([]string, 0, len(orgRoles))
|
||||
|
||||
@@ -32,10 +32,6 @@ const (
|
||||
ScopeChatDelete ScopeName = "chat:delete"
|
||||
ScopeChatRead ScopeName = "chat:read"
|
||||
ScopeChatUpdate ScopeName = "chat:update"
|
||||
ScopeChatAutomationCreate ScopeName = "chat_automation:create"
|
||||
ScopeChatAutomationDelete ScopeName = "chat_automation:delete"
|
||||
ScopeChatAutomationRead ScopeName = "chat_automation:read"
|
||||
ScopeChatAutomationUpdate ScopeName = "chat_automation:update"
|
||||
ScopeConnectionLogRead ScopeName = "connection_log:read"
|
||||
ScopeConnectionLogUpdate ScopeName = "connection_log:update"
|
||||
ScopeCryptoKeyCreate ScopeName = "crypto_key:create"
|
||||
@@ -200,10 +196,6 @@ func (e ScopeName) Valid() bool {
|
||||
ScopeChatDelete,
|
||||
ScopeChatRead,
|
||||
ScopeChatUpdate,
|
||||
ScopeChatAutomationCreate,
|
||||
ScopeChatAutomationDelete,
|
||||
ScopeChatAutomationRead,
|
||||
ScopeChatAutomationUpdate,
|
||||
ScopeConnectionLogRead,
|
||||
ScopeConnectionLogUpdate,
|
||||
ScopeCryptoKeyCreate,
|
||||
@@ -369,10 +361,6 @@ func AllScopeNameValues() []ScopeName {
|
||||
ScopeChatDelete,
|
||||
ScopeChatRead,
|
||||
ScopeChatUpdate,
|
||||
ScopeChatAutomationCreate,
|
||||
ScopeChatAutomationDelete,
|
||||
ScopeChatAutomationRead,
|
||||
ScopeChatAutomationUpdate,
|
||||
ScopeConnectionLogRead,
|
||||
ScopeConnectionLogUpdate,
|
||||
ScopeCryptoKeyCreate,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user