Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
libxmlsec1-dev \
make \
neovim \
nginx \
openssh-client \
pkg-config \
postgresql-client \
Expand Down
157 changes: 135 additions & 22 deletions .github/workflows/pr-craft-k8s-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ env:
SANDBOX_TURN_TIMEOUT_SECONDS: "120"
KIND_REGISTRY_NAME: "kind-registry"
KIND_REGISTRY_PORT: "5001"
KIND_VERSION: "v0.31.0"
KUBECTL_VERSION: "v1.35.0"

# The pytest process runs on the runner, so it reaches chart-managed
# Postgres/Redis through kubectl port-forwards.
Expand Down Expand Up @@ -159,6 +161,121 @@ jobs:
fi
echo "test-files=[${entries%,}]" >> "$GITHUB_OUTPUT"

prepare-craft-assets:
name: Prepare Craft test assets
needs: changes
if: needs.changes.outputs.craft_k8s == 'true'
runs-on:
- runs-on
- runner=2cpu-linux-x64
- spot=false
- ${{ format('run-id={0}-craft-k8s-tools', github.run_id) }}
timeout-minutes: 10
permissions:
contents: read
steps:
- uses: runs-on/action@4e5f72399b6b17f2e79c511c1b38a315a64d22dc

- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # ratchet:actions/checkout@v6
with:
persist-credentials: false

# The run-scoped key prevents pull-request code from poisoning caches used
# by other runs. Test shards restore the completed cache after this job.
- name: Restore kind tools
id: kind-tools-cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # zizmor: ignore[cache-poisoning]
with:
path: ${{ runner.tool_cache }}/kind/${{ env.KIND_VERSION }}/amd64
key: craft-kind-tools-${{ runner.os }}-${{ runner.arch }}-${{ env.KIND_VERSION }}-${{ env.KUBECTL_VERSION }}-${{ github.run_id }}

- name: Download and verify kind tools
if: steps.kind-tools-cache.outputs.cache-hit != 'true'
run: |
set -euo pipefail

download() {
curl --fail --location --silent --show-error \
--retry 5 --retry-delay 2 --retry-all-errors \
--output "$2" "$1"
}

cache_dir="${RUNNER_TOOL_CACHE}/kind/${KIND_VERSION}/amd64"
kind_dir="${cache_dir}/kind/bin"
kubectl_dir="${cache_dir}/kubectl/bin"
mkdir -p "${kind_dir}" "${kubectl_dir}"

kind_filename="kind-linux-amd64"
kind_url="https://github.com/kubernetes-sigs/kind/releases/download/${KIND_VERSION}"
download "${kind_url}/${kind_filename}" "${kind_dir}/${kind_filename}"
download "${kind_url}/${kind_filename}.sha256sum" "${kind_dir}/${kind_filename}.sha256sum"
(
cd "${kind_dir}"
grep "${kind_filename}" "${kind_filename}.sha256sum" | sha256sum --check -
mv "${kind_filename}" kind
rm "${kind_filename}.sha256sum"
chmod +x kind
)

kubectl_url="https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64"
download "${kubectl_url}/kubectl" "${kubectl_dir}/kubectl"
download "${kubectl_url}/kubectl.sha256" "${kubectl_dir}/kubectl.sha256"
(
cd "${kubectl_dir}"
echo "$(cat kubectl.sha256) kubectl" | sha256sum --check -
rm kubectl.sha256
chmod +x kubectl
)

- name: Restore Helm chart dependencies
id: helm-dependencies-cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # zizmor: ignore[cache-poisoning]
with:
path: deployment/helm/charts/onyx/charts
key: craft-helm-dependencies-${{ hashFiles('deployment/helm/charts/onyx/Chart.yaml', 'deployment/helm/charts/onyx/Chart.lock') }}-${{ github.run_id }}

- name: Download Helm chart dependencies
if: steps.helm-dependencies-cache.outputs.cache-hit != 'true'
run: |
set -euo pipefail

retry() {
local attempt
for attempt in 1 2 3 4 5; do
if "$@"; then
return 0
fi
if [ "${attempt}" -eq 5 ]; then
return 1
fi
echo "Command failed (attempt ${attempt}/5); retrying ..."
sleep $((attempt * 5))
done
}

retry helm repo add --force-update ingress-nginx https://kubernetes.github.io/ingress-nginx
retry helm repo add --force-update opensearch https://opensearch-project.github.io/helm-charts
retry helm repo add --force-update cloudnative-pg https://cloudnative-pg.github.io/charts
retry helm repo add --force-update ot-container-kit https://ot-container-kit.github.io/helm-charts
retry helm repo add --force-update minio https://charts.min.io/
retry helm repo add --force-update code-interpreter https://onyx-dot-app.github.io/python-sandbox/
retry helm repo update
if ! retry helm dependency build --skip-refresh deployment/helm/charts/onyx; then
echo "helm dependency build failed; pulling disabled code-interpreter dependency directly"
code_interpreter_version=$(awk '
$1 == "-" && $2 == "name:" && $3 == "code-interpreter" { found = 1 }
found && $1 == "version:" { print $2; exit }
' deployment/helm/charts/onyx/Chart.yaml)
test -n "${code_interpreter_version}"
retry helm pull code-interpreter/code-interpreter \
--version "${code_interpreter_version}" \
--destination deployment/helm/charts/onyx/charts
fi
helm dependency list deployment/helm/charts/onyx
helm dependency list deployment/helm/charts/onyx \
| awk 'NR > 1 && NF && $4 != "ok" { bad = 1 } END { exit bad }'

build-images:
# Build both images in parallel (one matrix leg each) and push to the shared
# ECR repo so each test shard pulls prebuilt images instead of cold-building.
Expand Down Expand Up @@ -229,7 +346,7 @@ jobs:

craft-k8s-tests:
name: craft-k8s (${{ matrix.test-file.name }})
needs: [changes, discover-test-files, build-images]
needs: [changes, discover-test-files, prepare-craft-assets, build-images]
if: needs.changes.outputs.craft_k8s == 'true'
# spot=false: this is a long lane (full kind cluster per shard); on-demand
# avoids mid-run spot reclamation. Matches the compose lane.
Expand Down Expand Up @@ -275,6 +392,20 @@ jobs:
backend/requirements/dev.txt
backend/requirements/ee.txt

- name: Restore kind tools
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9
with:
path: ${{ runner.tool_cache }}/kind/${{ env.KIND_VERSION }}/amd64
key: craft-kind-tools-${{ runner.os }}-${{ runner.arch }}-${{ env.KIND_VERSION }}-${{ env.KUBECTL_VERSION }}-${{ github.run_id }}
fail-on-cache-miss: true

- name: Restore Helm chart dependencies
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9
with:
path: deployment/helm/charts/onyx/charts
key: craft-helm-dependencies-${{ hashFiles('deployment/helm/charts/onyx/Chart.yaml', 'deployment/helm/charts/onyx/Chart.lock') }}-${{ github.run_id }}
fail-on-cache-miss: true

- name: Log in to ECR pull-through cache
uses: ./.github/actions/login-ecr-pullthrough-cache
with:
Expand Down Expand Up @@ -318,6 +449,8 @@ jobs:
- name: Create kind cluster
uses: helm/kind-action@ef37e7f390d99f746eb8b610417061a60e82a6cc # ratchet:helm/kind-action@v1.14.0
with:
version: ${{ env.KIND_VERSION }}
kubectl_version: ${{ env.KUBECTL_VERSION }}
cluster_name: onyx-craft-ci
node_image: kindest/node:v1.33.1
config: ${{ runner.temp }}/kind-config.yaml
Expand All @@ -337,28 +470,8 @@ jobs:
- name: Label kind node for sandbox workload
run: kubectl label node onyx-craft-ci-control-plane onyx.app/workload=sandbox

# `helm upgrade --install` validates Chart.yaml dependencies up front.
# Add the repos and build deps before installing the CI release.
- name: Build helm chart dependencies
- name: Validate Helm chart dependencies
run: |
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo add opensearch https://opensearch-project.github.io/helm-charts
helm repo add cloudnative-pg https://cloudnative-pg.github.io/charts
helm repo add ot-container-kit https://ot-container-kit.github.io/helm-charts
helm repo add minio https://charts.min.io/
helm repo add code-interpreter https://onyx-dot-app.github.io/python-sandbox/
helm repo update
if ! helm dependency build deployment/helm/charts/onyx; then
echo "helm dependency build failed; pulling disabled code-interpreter dependency directly"
code_interpreter_version=$(awk '
$1 == "-" && $2 == "name:" && $3 == "code-interpreter" { found = 1 }
found && $1 == "version:" { print $2; exit }
' deployment/helm/charts/onyx/Chart.yaml)
test -n "${code_interpreter_version}"
helm pull code-interpreter/code-interpreter \
--version "${code_interpreter_version}" \
--destination deployment/helm/charts/onyx/charts
fi
helm dependency list deployment/helm/charts/onyx
helm dependency list deployment/helm/charts/onyx \
| awk 'NR > 1 && NF && $4 != "ok" { bad = 1 } END { exit bad }'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""add incognito to user_usage

Revision ID: 17135ac06582
Revises: 3260759d6965
Create Date: 2026-08-10 12:30:00.000000

"""

import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision = "17135ac06582"
down_revision = "3260759d6965"
branch_labels = None
depends_on = None

UNIQUE_INDEX = "uq_user_usage_dims"
BASE_DIMENSIONS = ["user_id", "window_start", "model", "flow", "provider"]


def upgrade() -> None:
op.add_column(
"user_usage",
sa.Column(
"incognito",
sa.Boolean(),
nullable=False,
server_default=sa.false(),
),
)
# incognito joins the rollup dimension tuple, so the upsert's unique index
# must include it or two rows that differ only by mode would collide.
op.drop_index(UNIQUE_INDEX, table_name="user_usage")
op.create_index(
UNIQUE_INDEX,
"user_usage",
[*BASE_DIMENSIONS, "incognito"],
unique=True,
)


def downgrade() -> None:
op.drop_index(UNIQUE_INDEX, table_name="user_usage")
# Incognito rows only exist because of this revision, and without the
# column they would collide with their ordinary counterparts on the
# narrower index.
op.execute(sa.text("DELETE FROM user_usage WHERE incognito"))
op.drop_column("user_usage", "incognito")
op.create_index(UNIQUE_INDEX, "user_usage", BASE_DIMENSIONS, unique=True)
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""add incognito columns to user_file

Revision ID: 3260759d6965
Revises: c7f1a9d4e206
Create Date: 2026-08-10 13:00:00.000000

"""

import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision = "3260759d6965"
down_revision = "c7f1a9d4e206"
branch_labels = None
depends_on = None


def upgrade() -> None:
op.add_column(
"user_file",
sa.Column(
"incognito",
sa.Boolean(),
nullable=False,
server_default=sa.false(),
),
)
# No foreign key: the chat_session row is deleted at teardown and these
# rows must outlive it long enough for the orphan sweep to find them.
op.add_column(
"user_file",
sa.Column("incognito_session_id", sa.UUID(as_uuid=True), nullable=True),
)
# Partial index for the stale-incognito sweep, tiny since incognito rows
# are short-lived.
op.create_index(
"ix_user_file_incognito_sweep",
"user_file",
["incognito_session_id", "status", "last_accessed_at"],
postgresql_where=sa.text("incognito"),
)


def downgrade() -> None:
op.drop_index("ix_user_file_incognito_sweep", table_name="user_file")
op.drop_column("user_file", "incognito_session_id")
op.drop_column("user_file", "incognito")
14 changes: 11 additions & 3 deletions backend/ee/onyx/connectors/capability_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,20 @@
CapabilityCheckContext,
CredentialCapability,
)
from onyx.connectors.slack.capability_checks import (
build_slack_doc_permission_sync_checks,
)
from onyx.connectors.source_operations import get_source_operations_class

# Named perm-sync checks per source. Empty at framework stage: per-connector
# work registers named checks here.
_DOC_PERMISSION_SYNC_CHECKS_BY_SOURCE: dict[DocumentSource, list[CapabilityCheck]] = {}
# Named perm-sync checks per source. Per-connector work registers named checks
# here.
_DOC_PERMISSION_SYNC_CHECKS_BY_SOURCE: dict[DocumentSource, list[CapabilityCheck]] = {
DocumentSource.SLACK: build_slack_doc_permission_sync_checks(),
}

# Slack registers nothing here by design: it has no group sync (channel access
# resolves usergroups to individual users, so there is no usergroup-to-document
# mapping).
_EXTERNAL_GROUP_SYNC_CHECKS_BY_SOURCE: dict[DocumentSource, list[CapabilityCheck]] = {}


Expand Down
3 changes: 2 additions & 1 deletion backend/ee/onyx/db/query_history.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,9 @@ def fetch_chat_sessions_eagerly_by_time(
asc_time_order: UnaryExpression = asc(ChatSession.time_created)
message_order: UnaryExpression = asc(ChatMessage.id)

# Unfiltered on record mode: this backs the usage report, which carries
# token counts and no message content, and every mode meters usage.
filters: list[ColumnElement | BinaryExpression] = [
content_persisting_sessions_filter(),
ChatSession.time_created.between(start, end),
]

Expand Down
2 changes: 2 additions & 0 deletions backend/ee/onyx/server/reporting/usage_export_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ def generate_usage_breakdown_report(
"model",
"flow",
"provider",
"incognito",
"input_tokens",
"output_tokens",
"cache_read_tokens",
Expand All @@ -170,6 +171,7 @@ def generate_usage_breakdown_report(
sanitize_csv_cell_or_none(row.model),
sanitize_csv_cell_or_none(row.flow),
sanitize_csv_cell_or_none(row.provider),
row.incognito,
row.input_tokens,
row.output_tokens,
row.cache_read_tokens,
Expand Down
Loading
Loading