commit 42f0a3b91b1b3bd34c0d9411b56c51b45666b684
Author: ZhenYi <434836402@qq.com>
Date: Tue Apr 14 19:02:01 2026 +0800
commit: no msg
diff --git a/.agents/agents/code-reviewer.md b/.agents/agents/code-reviewer.md
new file mode 100644
index 0000000..fd936f4
--- /dev/null
+++ b/.agents/agents/code-reviewer.md
@@ -0,0 +1,42 @@
+
+You are a Senior Code Reviewer with expertise in software architecture, design patterns, and best practices. Your role is to review completed project steps against original plans and ensure code quality standards are met.
+
+When reviewing completed work, you will:
+
+1. **Plan Alignment Analysis**:
+ - Compare the implementation against the original planning document or step description
+ - Identify any deviations from the planned approach, architecture, or requirements
+ - Assess whether deviations are justified improvements or problematic departures
+ - Verify that all planned functionality has been implemented
+
+2. **Code Quality Assessment**:
+ - Review code for adherence to established patterns and conventions
+ - Check for proper error handling, type safety, and defensive programming
+ - Evaluate code organization, naming conventions, and maintainability
+ - Assess test coverage and quality of test implementations
+ - Look for potential security vulnerabilities or performance issues
+
+3. **Architecture and Design Review**:
+ - Ensure the implementation follows SOLID principles and established architectural patterns
+ - Check for proper separation of concerns and loose coupling
+ - Verify that the code integrates well with existing systems
+ - Assess scalability and extensibility considerations
+
+4. **Documentation and Standards**:
+ - Verify that code includes appropriate comments and documentation
+ - Check that file headers, function documentation, and inline comments are present and accurate
+ - Ensure adherence to project-specific coding standards and conventions
+
+5. **Issue Identification and Recommendations**:
+ - Clearly categorize issues as: Critical (must fix), Important (should fix), or Suggestions (nice to have)
+ - For each issue, provide specific examples and actionable recommendations
+ - When you identify plan deviations, explain whether they're problematic or beneficial
+ - Suggest specific improvements with code examples when helpful
+
+6. **Communication Protocol**:
+ - If you find significant deviations from the plan, ask the coding agent to review and confirm the changes
+ - If you identify issues with the original plan itself, recommend plan updates
+ - For implementation problems, provide clear guidance on fixes needed
+ - Always acknowledge what was done well before highlighting issues
+
+Your output should be structured, actionable, and focused on helping maintain high code quality while ensuring project goals are met. Be thorough but concise, and always provide constructive feedback that helps improve both the current implementation and future development practices.
diff --git a/.claude/work.yaml b/.claude/work.yaml
new file mode 100644
index 0000000..476b5fe
--- /dev/null
+++ b/.claude/work.yaml
@@ -0,0 +1,4 @@
+list:
+ - "优化libs/api/error.rs 为标准错误 (Branch)"
+ - "2d76bf69-9ca9-4da9-b693-31005c5c4c0d"
+ - "c3f51440-f6ee-483a-8887-556e94f0897f"
\ No newline at end of file
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..aa068ac
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,9 @@
+target/
+.git/
+.idea/
+.vscode/
+node_modules/
+*.log
+.env
+.env.local
+.env.*.local
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..a55d1b7
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,109 @@
+# =============================================================================
+# Required - 程序启动必须配置
+# =============================================================================
+
+# 数据库连接
+APP_DATABASE_URL=postgresql://user:password@localhost:5432/dbname
+APP_DATABASE_SCHEMA_SEARCH_PATH=public
+
+# Redis(支持多节点,逗号分隔)
+APP_REDIS_URL=redis://localhost:6379
+# APP_REDIS_URLS=redis://localhost:6379,redis://localhost:6378
+
+# AI 服务
+APP_AI_BASIC_URL=https://api.openai.com/v1
+APP_AI_API_KEY=sk-xxxxx
+
+# Embedding + 向量检索
+APP_EMBED_MODEL_BASE_URL=https://api.openai.com/v1
+APP_EMBED_MODEL_API_KEY=sk-xxxxx
+APP_EMBED_MODEL_NAME=text-embedding-3-small
+APP_EMBED_MODEL_DIMENSIONS=1536
+APP_QDRANT_URL=http://localhost:6333
+# APP_QDRANT_API_KEY=
+
+# SMTP 邮件
+APP_SMTP_HOST=smtp.example.com
+APP_SMTP_PORT=587
+APP_SMTP_USERNAME=noreply@example.com
+APP_SMTP_PASSWORD=xxxxx
+APP_SMTP_FROM=noreply@example.com
+APP_SMTP_TLS=true
+APP_SMTP_TIMEOUT=30
+
+# 文件存储
+APP_AVATAR_PATH=/data/avatars
+# Git 仓库存储根目录
+APP_REPOS_ROOT=/data/repos
+
+# =============================================================================
+# Domain / URL(可选,有默认值)
+# =============================================================================
+
+APP_DOMAIN_URL=http://127.0.0.1
+# APP_STATIC_DOMAIN=
+# APP_MEDIA_DOMAIN=
+# APP_GIT_HTTP_DOMAIN=
+
+# =============================================================================
+# Database Pool(可选,有默认值)
+# =============================================================================
+
+# APP_DATABASE_MAX_CONNECTIONS=10
+# APP_DATABASE_MIN_CONNECTIONS=2
+# APP_DATABASE_IDLE_TIMEOUT=60000
+# APP_DATABASE_MAX_LIFETIME=300000
+# APP_DATABASE_CONNECTION_TIMEOUT=5000
+# APP_DATABASE_REPLICAS=
+# APP_DATABASE_HEALTH_CHECK_INTERVAL=30
+# APP_DATABASE_RETRY_ATTEMPTS=3
+# APP_DATABASE_RETRY_DELAY=5
+
+# =============================================================================
+# Redis Pool(可选,有默认值)
+# =============================================================================
+
+# APP_REDIS_POOL_SIZE=10
+# APP_REDIS_CONNECT_TIMEOUT=5
+# APP_REDIS_ACQUIRE_TIMEOUT=5
+
+# =============================================================================
+# SSH(可选,有默认值)
+# =============================================================================
+
+# APP_SSH_DOMAIN=
+# APP_SSH_PORT=22
+# APP_SSH_SERVER_PRIVATE_KEY=
+# APP_SSH_SERVER_PUBLIC_KEY=
+
+# =============================================================================
+# Logging(可选,有默认值)
+# =============================================================================
+
+# APP_LOG_LEVEL=info
+# APP_LOG_FORMAT=json
+# APP_LOG_FILE_ENABLED=false
+# APP_LOG_FILE_PATH=./logs
+# APP_LOG_FILE_ROTATION=daily
+# APP_LOG_FILE_MAX_FILES=7
+# APP_LOG_FILE_MAX_SIZE=104857600
+
+# OpenTelemetry(可选,默认关闭)
+# APP_OTEL_ENABLED=false
+# APP_OTEL_ENDPOINT=http://localhost:5080/api/default/v1/traces
+# APP_OTEL_SERVICE_NAME=
+# APP_OTEL_SERVICE_VERSION=
+# APP_OTEL_AUTHORIZATION=
+# APP_OTEL_ORGANIZATION=
+
+# =============================================================================
+# NATS / Hook Pool(可选,有默认值)
+# =============================================================================
+
+# HOOK_POOL_MAX_CONCURRENT=(CPU 核数)
+# HOOK_POOL_CPU_THRESHOLD=80.0
+# HOOK_POOL_REDIS_LIST_PREFIX={hook}
+# HOOK_POOL_REDIS_LOG_CHANNEL=hook:logs
+# HOOK_POOL_REDIS_BLOCK_TIMEOUT=5
+# HOOK_POOL_REDIS_MAX_RETRIES=3
+# HOOK_POOL_WORKER_ID=(随机 UUID)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..5e3ce22
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,15 @@
+/target
+node_modules
+.claude
+.zed
+.vscode
+.idea
+.env
+.env.local
+dist
+.codex
+.qwen
+.opencode
+.omc
+AGENT.md
+ARCHITECTURE.md
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..b6b1ecf
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,10 @@
+# 默认忽略的文件
+/shelf/
+/workspace.xml
+# 已忽略包含查询文件的默认文件夹
+/queries/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
+# 基于编辑器的 HTTP 客户端请求
+/httpRequests/
diff --git a/.idea/code.iml b/.idea/code.iml
new file mode 100644
index 0000000..2961ea3
--- /dev/null
+++ b/.idea/code.iml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..23968dc
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/AGENT.md b/AGENT.md
new file mode 100644
index 0000000..3959737
--- /dev/null
+++ b/AGENT.md
@@ -0,0 +1,182 @@
+You are a deterministic autonomous coding agent.
+
+Your purpose is NOT to be fast or clever.
+Your purpose is to produce correct, verifiable, minimal, and stable results.
+
+You MUST operate under strict discipline.
+
+---
+
+## CORE EXECUTION MODEL
+
+You MUST follow this exact loop:
+
+1. UNDERSTAND
+2. PLAN
+3. EXECUTE (single step only)
+4. VERIFY (mandatory)
+5. REVIEW (mandatory)
+6. FIX or CONTINUE
+
+You are NOT allowed to skip any step.
+
+---
+
+## STEP 1 — UNDERSTAND
+
+- Restate the task clearly
+- Identify constraints, risks, and unknowns
+- If anything is unclear → explicitly state assumptions
+
+DO NOT WRITE CODE.
+
+---
+
+## STEP 2 — PLAN
+
+- Break task into atomic steps
+- Each step must:
+ - affect only ONE logical unit (function/module)
+ - be independently testable
+- Avoid multi-file or large-scope changes
+- Prefer more steps over fewer
+
+Output a numbered plan.
+
+---
+
+## STEP 3 — EXECUTE
+
+- Execute ONLY ONE step
+- Modify minimal code
+- DO NOT refactor unrelated code
+- DO NOT optimize
+- DO NOT expand scope
+
+All code must be complete and runnable.
+
+---
+
+## STEP 4 — VERIFY (CRITICAL)
+
+You MUST:
+
+- Describe how this step can fail
+- Provide concrete validation steps (tests, commands, checks)
+- Consider:
+ - edge cases
+ - invalid input
+ - runtime errors
+ - integration issues
+
+If verification is not possible → mark as "UNVERIFIABLE"
+
+---
+
+## STEP 5 — REVIEW (CRITICAL)
+
+You MUST critically evaluate your own output:
+
+- What could be wrong?
+- What assumptions may break?
+- Did you overreach scope?
+- Is there a simpler or safer solution?
+
+Be skeptical. Assume you are wrong.
+
+---
+
+## STEP 6 — FIX OR CONTINUE
+
+IF issues found:
+
+- Fix them immediately
+- DO NOT proceed to next step
+
+IF no issues:
+
+- Move to next step
+
+---
+
+## HARD CONSTRAINTS
+
+- NEVER implement the whole solution at once
+- NEVER skip verification
+- NEVER assume correctness
+- ALWAYS minimize change scope
+- ALWAYS prefer boring, simple solutions
+- NEVER hallucinate APIs or functions
+- IF uncertain → explicitly say "UNCERTAIN"
+
+---
+
+## FAILURE HANDLING
+
+If you fail twice:
+
+- STOP
+- Re-evaluate the entire plan
+- Propose a different approach
+
+---
+
+## OUTPUT FORMAT (STRICT)
+
+## Step X:
+
+### Understand
+
+...
+
+### Plan
+
+...
+
+### Execute
+
+...
+
+### Verify
+
+...
+
+### Review
+
+...
+
+---
+
+## ENVIRONMENT RULES
+
+- You are operating in a real codebase
+- All edits must be precise and minimal
+- Always indicate file paths when modifying code
+- Do not create unnecessary files
+- Prefer editing existing code
+
+---
+
+## PRIORITY ORDER
+
+Correctness > Verifiability > Stability > Maintainability > Speed
+
+---
+
+## BEHAVIORAL DIRECTIVES
+
+- Be slow and deliberate
+- Think before acting
+- Act in small steps
+- Validate everything
+- Trust nothing (including your own output)
+
+EXECUTION DISCIPLINE:
+
+- You are NOT allowed to jump steps
+- You are NOT allowed to combine steps
+- Each response must contain ONLY ONE step execution
+- After each step, STOP and wait
+
+If the user does not explicitly say "continue":
+DO NOT proceed to next step
\ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..e734d0f
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,8862 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "ab_glyph"
+version = "0.2.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2"
+dependencies = [
+ "ab_glyph_rasterizer",
+ "owned_ttf_parser",
+]
+
+[[package]]
+name = "ab_glyph_rasterizer"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
+
+[[package]]
+name = "actix"
+version = "0.13.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de7fa236829ba0841304542f7614c42b80fca007455315c45c785ccfa873a85b"
+dependencies = [
+ "actix-macros",
+ "actix-rt",
+ "actix_derive",
+ "bitflags",
+ "bytes",
+ "crossbeam-channel",
+ "futures-core",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+ "log",
+ "once_cell",
+ "parking_lot",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+ "tokio-util",
+]
+
+[[package]]
+name = "actix-codec"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a"
+dependencies = [
+ "bitflags",
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "memchr",
+ "pin-project-lite",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "actix-cors"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "daa239b93927be1ff123eebada5a3ff23e89f0124ccb8609234e5103d5a5ae6d"
+dependencies = [
+ "actix-utils",
+ "actix-web",
+ "derive_more",
+ "futures-util",
+ "log",
+ "once_cell",
+ "smallvec",
+]
+
+[[package]]
+name = "actix-http"
+version = "3.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f860ee6746d0c5b682147b2f7f8ef036d4f92fe518251a3a35ffa3650eafdf0e"
+dependencies = [
+ "actix-codec",
+ "actix-rt",
+ "actix-service",
+ "actix-utils",
+ "base64 0.22.1",
+ "bitflags",
+ "brotli",
+ "bytes",
+ "bytestring",
+ "derive_more",
+ "encoding_rs",
+ "flate2",
+ "foldhash",
+ "futures-core",
+ "h2 0.3.27",
+ "http 0.2.12",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "language-tags",
+ "local-channel",
+ "mime",
+ "percent-encoding",
+ "pin-project-lite",
+ "rand 0.9.2",
+ "sha1",
+ "smallvec",
+ "tokio",
+ "tokio-util",
+ "tracing",
+ "zstd",
+]
+
+[[package]]
+name = "actix-macros"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb"
+dependencies = [
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "actix-router"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14f8c75c51892f18d9c46150c5ac7beb81c95f78c8b83a634d49f4ca32551fe7"
+dependencies = [
+ "bytestring",
+ "cfg-if",
+ "http 0.2.12",
+ "regex",
+ "regex-lite",
+ "serde",
+ "tracing",
+]
+
+[[package]]
+name = "actix-rt"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92589714878ca59a7626ea19734f0e07a6a875197eec751bb5d3f99e64998c63"
+dependencies = [
+ "futures-core",
+ "tokio",
+]
+
+[[package]]
+name = "actix-server"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a65064ea4a457eaf07f2fba30b4c695bf43b721790e9530d26cb6f9019ff7502"
+dependencies = [
+ "actix-rt",
+ "actix-service",
+ "actix-utils",
+ "futures-core",
+ "futures-util",
+ "mio",
+ "socket2 0.5.10",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "actix-service"
+version = "2.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e46f36bf0e5af44bdc4bdb36fbbd421aa98c79a9bce724e1edeb3894e10dc7f"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "actix-utils"
+version = "3.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8"
+dependencies = [
+ "local-waker",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "actix-web"
+version = "4.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff87453bc3b56e9b2b23c1cc0b1be8797184accf51d2abe0f8a33ec275d316bf"
+dependencies = [
+ "actix-codec",
+ "actix-http",
+ "actix-macros",
+ "actix-router",
+ "actix-rt",
+ "actix-server",
+ "actix-service",
+ "actix-utils",
+ "actix-web-codegen",
+ "bytes",
+ "bytestring",
+ "cfg-if",
+ "cookie",
+ "derive_more",
+ "encoding_rs",
+ "foldhash",
+ "futures-core",
+ "futures-util",
+ "impl-more",
+ "itoa",
+ "language-tags",
+ "log",
+ "mime",
+ "once_cell",
+ "pin-project-lite",
+ "regex",
+ "regex-lite",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "smallvec",
+ "socket2 0.6.3",
+ "time",
+ "tracing",
+ "url",
+]
+
+[[package]]
+name = "actix-web-codegen"
+version = "4.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8"
+dependencies = [
+ "actix-router",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "actix-ws"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "decf53c3cdd63dd6f289980b430238f9a2f6d19f8bce8e418272e08d3da43f0f"
+dependencies = [
+ "actix-codec",
+ "actix-http",
+ "actix-web",
+ "bytestring",
+ "futures-core",
+ "futures-sink",
+ "tokio",
+ "tokio-util",
+]
+
+[[package]]
+name = "actix_derive"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6ac1e58cded18cb28ddc17143c4dea5345b3ad575e14f32f66e4054a56eb271"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "aead"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
+dependencies = [
+ "crypto-common 0.1.7",
+ "generic-array 0.14.7",
+]
+
+[[package]]
+name = "aead"
+version = "0.6.0-rc.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b657e772794c6b04730ea897b66a058ccd866c16d1967da05eeeecec39043fe"
+dependencies = [
+ "crypto-common 0.2.1",
+ "inout 0.2.2",
+]
+
+[[package]]
+name = "aes"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
+dependencies = [
+ "cfg-if",
+ "cipher 0.4.4",
+ "cpufeatures 0.2.17",
+]
+
+[[package]]
+name = "aes"
+version = "0.9.0-rc.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04097e08a47d9ad181c2e1f4a5fabc9ae06ce8839a333ba9a949bcb0d31fd2a3"
+dependencies = [
+ "cipher 0.5.1",
+ "cpubits",
+ "cpufeatures 0.2.17",
+ "zeroize",
+]
+
+[[package]]
+name = "aes-gcm"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1"
+dependencies = [
+ "aead 0.5.2",
+ "aes 0.8.4",
+ "cipher 0.4.4",
+ "ctr 0.9.2",
+ "ghash 0.5.1",
+ "subtle",
+]
+
+[[package]]
+name = "aes-gcm"
+version = "0.11.0-rc.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e22c0c90bbe8d4f77c3ca9ddabe41a1f8382d6fc1f7cea89459d0f320371f972"
+dependencies = [
+ "aead 0.6.0-rc.10",
+ "aes 0.9.0-rc.4",
+ "cipher 0.5.1",
+ "ctr 0.10.0-rc.4",
+ "ghash 0.6.0",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "agent"
+version = "0.2.9"
+dependencies = [
+ "agent-tool-derive",
+ "async-openai",
+ "async-trait",
+ "chrono",
+ "config",
+ "db",
+ "futures",
+ "models",
+ "once_cell",
+ "qdrant-client",
+ "regex",
+ "sea-orm",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.18",
+ "tiktoken-rs",
+ "tokio",
+ "uuid",
+]
+
+[[package]]
+name = "agent-tool-derive"
+version = "0.2.9"
+dependencies = [
+ "convert_case 0.11.0",
+ "futures",
+ "proc-macro2",
+ "quote",
+ "serde",
+ "serde_json",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "ahash"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9"
+dependencies = [
+ "getrandom 0.2.17",
+ "once_cell",
+ "version_check",
+]
+
+[[package]]
+name = "ahash"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
+dependencies = [
+ "cfg-if",
+ "const-random",
+ "getrandom 0.3.4",
+ "once_cell",
+ "version_check",
+ "zerocopy",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "aliasable"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
+
+[[package]]
+name = "aligned"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685"
+dependencies = [
+ "as-slice",
+]
+
+[[package]]
+name = "aligned-vec"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b"
+dependencies = [
+ "equator",
+]
+
+[[package]]
+name = "alloc-no-stdlib"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
+
+[[package]]
+name = "alloc-stdlib"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece"
+dependencies = [
+ "alloc-no-stdlib",
+]
+
+[[package]]
+name = "allocator-api2"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anstream"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
+
+[[package]]
+name = "anstyle-parse"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
+
+[[package]]
+name = "api"
+version = "0.2.9"
+dependencies = [
+ "actix",
+ "actix-cors",
+ "actix-web",
+ "actix-ws",
+ "anyhow",
+ "base64 0.22.1",
+ "chrono",
+ "config",
+ "db",
+ "email",
+ "futures",
+ "git",
+ "models",
+ "queue",
+ "room",
+ "serde",
+ "serde_json",
+ "service",
+ "session",
+ "slog",
+ "tokio",
+ "tokio-stream",
+ "utoipa",
+ "uuid",
+]
+
+[[package]]
+name = "app"
+version = "0.2.9"
+dependencies = [
+ "actix-cors",
+ "actix-web",
+ "anyhow",
+ "api",
+ "chrono",
+ "clap",
+ "config",
+ "db",
+ "futures",
+ "migrate",
+ "sea-orm",
+ "serde_json",
+ "service",
+ "session",
+ "slog",
+ "tokio",
+ "uuid",
+]
+
+[[package]]
+name = "approx"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "arbitrary"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
+
+[[package]]
+name = "arc-swap"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a07d1f37ff60921c83bdfc7407723bdefe89b44b98a9b772f225c8f9d67141a6"
+dependencies = [
+ "rustversion",
+]
+
+[[package]]
+name = "arcstr"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03918c3dbd7701a85c6b9887732e2921175f26c350b4563841d0958c21d57e6d"
+
+[[package]]
+name = "arg_enum_proc_macro"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "argon2"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
+dependencies = [
+ "base64ct",
+ "blake2",
+ "cpufeatures 0.2.17",
+ "password-hash",
+]
+
+[[package]]
+name = "arrayvec"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
+
+[[package]]
+name = "arrow"
+version = "57.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4754a624e5ae42081f464514be454b39711daae0458906dacde5f4c632f33a8"
+dependencies = [
+ "arrow-arith",
+ "arrow-array",
+ "arrow-buffer",
+ "arrow-cast",
+ "arrow-data",
+ "arrow-ord",
+ "arrow-row",
+ "arrow-schema",
+ "arrow-select",
+ "arrow-string",
+]
+
+[[package]]
+name = "arrow-arith"
+version = "57.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7b3141e0ec5145a22d8694ea8b6d6f69305971c4fa1c1a13ef0195aef2d678b"
+dependencies = [
+ "arrow-array",
+ "arrow-buffer",
+ "arrow-data",
+ "arrow-schema",
+ "chrono",
+ "num-traits",
+]
+
+[[package]]
+name = "arrow-array"
+version = "57.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c8955af33b25f3b175ee10af580577280b4bd01f7e823d94c7cdef7cf8c9aef"
+dependencies = [
+ "ahash 0.8.12",
+ "arrow-buffer",
+ "arrow-data",
+ "arrow-schema",
+ "chrono",
+ "half",
+ "hashbrown 0.16.1",
+ "num-complex",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "arrow-buffer"
+version = "57.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c697ddca96183182f35b3a18e50b9110b11e916d7b7799cbfd4d34662f2c56c2"
+dependencies = [
+ "bytes",
+ "half",
+ "num-bigint",
+ "num-traits",
+]
+
+[[package]]
+name = "arrow-cast"
+version = "57.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "646bbb821e86fd57189c10b4fcdaa941deaf4181924917b0daa92735baa6ada5"
+dependencies = [
+ "arrow-array",
+ "arrow-buffer",
+ "arrow-data",
+ "arrow-ord",
+ "arrow-schema",
+ "arrow-select",
+ "atoi",
+ "base64 0.22.1",
+ "chrono",
+ "half",
+ "lexical-core",
+ "num-traits",
+ "ryu",
+]
+
+[[package]]
+name = "arrow-data"
+version = "57.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fdd994a9d28e6365aa78e15da3f3950c0fdcea6b963a12fa1c391afb637b304"
+dependencies = [
+ "arrow-buffer",
+ "arrow-schema",
+ "half",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "arrow-ord"
+version = "57.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7d8f1870e03d4cbed632959498bcc84083b5a24bded52905ae1695bd29da45b"
+dependencies = [
+ "arrow-array",
+ "arrow-buffer",
+ "arrow-data",
+ "arrow-schema",
+ "arrow-select",
+]
+
+[[package]]
+name = "arrow-row"
+version = "57.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18228633bad92bff92a95746bbeb16e5fc318e8382b75619dec26db79e4de4c0"
+dependencies = [
+ "arrow-array",
+ "arrow-buffer",
+ "arrow-data",
+ "arrow-schema",
+ "half",
+]
+
+[[package]]
+name = "arrow-schema"
+version = "57.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c872d36b7bf2a6a6a2b40de9156265f0242910791db366a2c17476ba8330d68"
+
+[[package]]
+name = "arrow-select"
+version = "57.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68bf3e3efbd1278f770d67e5dc410257300b161b93baedb3aae836144edcaf4b"
+dependencies = [
+ "ahash 0.8.12",
+ "arrow-array",
+ "arrow-buffer",
+ "arrow-data",
+ "arrow-schema",
+ "num-traits",
+]
+
+[[package]]
+name = "arrow-string"
+version = "57.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85e968097061b3c0e9fe3079cf2e703e487890700546b5b0647f60fca1b5a8d8"
+dependencies = [
+ "arrow-array",
+ "arrow-buffer",
+ "arrow-data",
+ "arrow-schema",
+ "arrow-select",
+ "memchr",
+ "num-traits",
+ "regex",
+ "regex-syntax",
+]
+
+[[package]]
+name = "as-slice"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516"
+dependencies = [
+ "stable_deref_trait",
+]
+
+[[package]]
+name = "async-broadcast"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
+dependencies = [
+ "event-listener",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-lock"
+version = "3.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
+dependencies = [
+ "event-listener",
+ "event-listener-strategy",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-openai"
+version = "0.34.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec08254d61379df136135d3d1ac04301be7699fd7d9e57655c63ac7d650a6922"
+dependencies = [
+ "async-openai-macros",
+ "backoff",
+ "base64 0.22.1",
+ "bytes",
+ "derive_builder",
+ "eventsource-stream",
+ "futures",
+ "getrandom 0.3.4",
+ "rand 0.9.2",
+ "reqwest 0.12.28",
+ "reqwest-eventsource",
+ "secrecy",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "thiserror 2.0.18",
+ "tokio",
+ "tokio-stream",
+ "tokio-util",
+ "tracing",
+ "url",
+]
+
+[[package]]
+name = "async-openai-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81872a8e595e8ceceab71c6ba1f9078e313b452a1e31934e6763ef5d308705e4"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "async-stream"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476"
+dependencies = [
+ "async-stream-impl",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-stream-impl"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "async-trait"
+version = "0.1.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "atoi"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "autocfg"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
+[[package]]
+name = "av-scenechange"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394"
+dependencies = [
+ "aligned",
+ "anyhow",
+ "arg_enum_proc_macro",
+ "arrayvec",
+ "log",
+ "num-rational",
+ "num-traits",
+ "pastey",
+ "rayon",
+ "thiserror 2.0.18",
+ "v_frame",
+ "y4m",
+]
+
+[[package]]
+name = "av1-grain"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8"
+dependencies = [
+ "anyhow",
+ "arrayvec",
+ "log",
+ "nom 8.0.0",
+ "num-rational",
+ "v_frame",
+]
+
+[[package]]
+name = "avatar"
+version = "0.2.9"
+dependencies = [
+ "anyhow",
+ "config",
+ "image",
+ "serde",
+]
+
+[[package]]
+name = "avif-serialize"
+version = "0.8.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "375082f007bd67184fb9c0374614b29f9aaa604ec301635f72338bb65386a53d"
+dependencies = [
+ "arrayvec",
+]
+
+[[package]]
+name = "axum"
+version = "0.7.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
+dependencies = [
+ "async-trait",
+ "axum-core",
+ "bytes",
+ "futures-util",
+ "http 1.4.0",
+ "http-body",
+ "http-body-util",
+ "itoa",
+ "matchit",
+ "memchr",
+ "mime",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustversion",
+ "serde",
+ "sync_wrapper",
+ "tower 0.5.3",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "axum-core"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199"
+dependencies = [
+ "async-trait",
+ "bytes",
+ "futures-util",
+ "http 1.4.0",
+ "http-body",
+ "http-body-util",
+ "mime",
+ "pin-project-lite",
+ "rustversion",
+ "sync_wrapper",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "backoff"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1"
+dependencies = [
+ "futures-core",
+ "getrandom 0.2.17",
+ "instant",
+ "pin-project-lite",
+ "rand 0.8.5",
+ "tokio",
+]
+
+[[package]]
+name = "backon"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef"
+dependencies = [
+ "fastrand",
+]
+
+[[package]]
+name = "base16ct"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
+
+[[package]]
+name = "base16ct"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6"
+
+[[package]]
+name = "base64"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5"
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "base64ct"
+version = "1.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
+
+[[package]]
+name = "bcrypt-pbkdf"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6aeac2e1fe888769f34f05ac343bbef98b14d1ffb292ab69d4608b3abc86f2a2"
+dependencies = [
+ "blowfish",
+ "pbkdf2",
+ "sha2 0.10.9",
+]
+
+[[package]]
+name = "bigdecimal"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d6867f1565b3aad85681f1015055b087fcfd840d6aeee6eee7f2da317603695"
+dependencies = [
+ "autocfg",
+ "libm",
+ "num-bigint",
+ "num-integer",
+ "num-traits",
+ "serde",
+]
+
+[[package]]
+name = "bit-set"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1"
+dependencies = [
+ "bit-vec",
+]
+
+[[package]]
+name = "bit-vec"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
+
+[[package]]
+name = "bit_field"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6"
+
+[[package]]
+name = "bitflags"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "bitstream-io"
+version = "4.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60d4bd9d1db2c6bdf285e223a7fa369d5ce98ec767dec949c6ca62863ce61757"
+dependencies = [
+ "core2",
+]
+
+[[package]]
+name = "bitvec"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
+dependencies = [
+ "funty",
+ "radium",
+ "tap",
+ "wyz",
+]
+
+[[package]]
+name = "blake2"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
+dependencies = [
+ "digest 0.10.7",
+]
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array 0.14.7",
+]
+
+[[package]]
+name = "block-buffer"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be"
+dependencies = [
+ "hybrid-array",
+ "zeroize",
+]
+
+[[package]]
+name = "block-padding"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93"
+dependencies = [
+ "generic-array 0.14.7",
+]
+
+[[package]]
+name = "blowfish"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7"
+dependencies = [
+ "byteorder",
+ "cipher 0.4.4",
+]
+
+[[package]]
+name = "borsh"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a"
+dependencies = [
+ "borsh-derive",
+ "bytes",
+ "cfg_aliases",
+]
+
+[[package]]
+name = "borsh-derive"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59"
+dependencies = [
+ "once_cell",
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "brotli"
+version = "8.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+ "brotli-decompressor",
+]
+
+[[package]]
+name = "brotli-decompressor"
+version = "5.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+]
+
+[[package]]
+name = "bstr"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab"
+dependencies = [
+ "memchr",
+ "regex-automata",
+ "serde",
+]
+
+[[package]]
+name = "built"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64"
+
+[[package]]
+name = "bumpalo"
+version = "3.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
+
+[[package]]
+name = "bytecheck"
+version = "0.6.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2"
+dependencies = [
+ "bytecheck_derive",
+ "ptr_meta",
+ "simdutf8",
+]
+
+[[package]]
+name = "bytecheck_derive"
+version = "0.6.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "bytemuck"
+version = "1.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "byteorder-lite"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
+
+[[package]]
+name = "bytes"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
+
+[[package]]
+name = "bytestring"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "113b4343b5f6617e7ad401ced8de3cc8b012e73a594347c307b90db3e9271289"
+dependencies = [
+ "bytes",
+]
+
+[[package]]
+name = "bzip2"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c"
+dependencies = [
+ "libbz2-rs-sys",
+]
+
+[[package]]
+name = "captcha-rs"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea23e9ba29e482e553d48391849195b95f055ffb059f785a28c9c5a046844223"
+dependencies = [
+ "ab_glyph",
+ "base64 0.22.1",
+ "image",
+ "imageproc",
+ "rand 0.9.2",
+]
+
+[[package]]
+name = "cbc"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6"
+dependencies = [
+ "cipher 0.4.4",
+]
+
+[[package]]
+name = "cc"
+version = "1.2.58"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1"
+dependencies = [
+ "find-msvc-tools",
+ "jobserver",
+ "libc",
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
+[[package]]
+name = "chacha20"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
+dependencies = [
+ "cfg-if",
+ "cipher 0.4.4",
+ "cpufeatures 0.2.17",
+]
+
+[[package]]
+name = "chacha20"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
+dependencies = [
+ "cfg-if",
+ "cipher 0.5.1",
+ "cpufeatures 0.3.0",
+ "rand_core 0.10.0",
+ "zeroize",
+]
+
+[[package]]
+name = "chrono"
+version = "0.4.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
+dependencies = [
+ "iana-time-zone",
+ "js-sys",
+ "num-traits",
+ "serde",
+ "wasm-bindgen",
+ "windows-link",
+]
+
+[[package]]
+name = "cipher"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
+dependencies = [
+ "crypto-common 0.1.7",
+ "inout 0.1.4",
+]
+
+[[package]]
+name = "cipher"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e34d8227fe1ba289043aeb13792056ff80fd6de1a9f49137a5f499de8e8c78ea"
+dependencies = [
+ "block-buffer 0.12.0",
+ "crypto-common 0.2.1",
+ "inout 0.2.2",
+ "zeroize",
+]
+
+[[package]]
+name = "clap"
+version = "4.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a"
+dependencies = [
+ "heck 0.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "clap_lex"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
+
+[[package]]
+name = "cmov"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de0758edba32d61d1fd9f4d69491b47604b91ee2f7e6b33de7e54ca4ebe55dc3"
+
+[[package]]
+name = "color_quant"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
+
+[[package]]
+name = "combine"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "memchr",
+ "pin-project-lite",
+ "tokio",
+ "tokio-util",
+]
+
+[[package]]
+name = "concurrent-queue"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "config"
+version = "0.2.9"
+dependencies = [
+ "anyhow",
+ "dotenvy",
+ "num_cpus",
+ "serde",
+ "uuid",
+]
+
+[[package]]
+name = "const-oid"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
+
+[[package]]
+name = "const-oid"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
+
+[[package]]
+name = "const-random"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
+dependencies = [
+ "const-random-macro",
+]
+
+[[package]]
+name = "const-random-macro"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
+dependencies = [
+ "getrandom 0.2.17",
+ "once_cell",
+ "tiny-keccak",
+]
+
+[[package]]
+name = "constant_time_eq"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"
+
+[[package]]
+name = "convert_case"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "convert_case"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49"
+dependencies = [
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "cookie"
+version = "0.16.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb"
+dependencies = [
+ "aes-gcm 0.10.3",
+ "base64 0.20.0",
+ "hkdf",
+ "hmac",
+ "percent-encoding",
+ "rand 0.8.5",
+ "sha2 0.10.9",
+ "subtle",
+ "time",
+ "version_check",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "core-models"
+version = "0.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0940496e5c83c54f3b753d5317daec82e8edac71c33aaa1f666d76f518de2444"
+dependencies = [
+ "hax-lib",
+ "pastey",
+ "rand 0.9.2",
+]
+
+[[package]]
+name = "core2"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "cpubits"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ef0c543070d296ea414df2dd7625d1b24866ce206709d8a4a424f28377f5861"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d"
+dependencies = [
+ "crc-catalog",
+]
+
+[[package]]
+name = "crc-catalog"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
+
+[[package]]
+name = "crc16"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "338089f42c427b86394a5ee60ff321da23a5c89c9d89514c829687b26359fcff"
+
+[[package]]
+name = "crc32fast"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crossbeam-channel"
+version = "0.5.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-queue"
+version = "0.3.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "crunchy"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
+
+[[package]]
+name = "crypto-bigint"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
+dependencies = [
+ "generic-array 0.14.7",
+ "rand_core 0.6.4",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+dependencies = [
+ "generic-array 0.14.7",
+ "rand_core 0.6.4",
+ "typenum",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710"
+dependencies = [
+ "hybrid-array",
+]
+
+[[package]]
+name = "ctr"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
+dependencies = [
+ "cipher 0.4.4",
+]
+
+[[package]]
+name = "ctr"
+version = "0.10.0-rc.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fee683dd898fbd052617b4514bc31f98bc32081a83b69ec46adef3b1ef4ae36f"
+dependencies = [
+ "cipher 0.5.1",
+]
+
+[[package]]
+name = "ctutils"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1005a6d4446f5120ef475ad3d2af2b30c49c2c9c6904258e3bb30219bebed5e4"
+dependencies = [
+ "cmov",
+]
+
+[[package]]
+name = "curve25519-dalek"
+version = "4.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.2.17",
+ "curve25519-dalek-derive",
+ "digest 0.10.7",
+ "fiat-crypto",
+ "rustc_version",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "curve25519-dalek-derive"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "darling"
+version = "0.20.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.20.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.20.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "dashmap"
+version = "7.0.0-rc2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4a1e35a65fe0538a60167f0ada6e195ad5d477f6ddae273943596d4a1a5730b"
+dependencies = [
+ "cfg-if",
+ "crossbeam-utils",
+ "equivalent",
+ "hashbrown 0.15.5",
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "data-encoding"
+version = "2.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea"
+
+[[package]]
+name = "db"
+version = "0.2.9"
+dependencies = [
+ "anyhow",
+ "async-trait",
+ "config",
+ "deadpool-redis",
+ "rand 0.10.0",
+ "sea-orm",
+ "tokio",
+]
+
+[[package]]
+name = "deadpool"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "883466cb8db62725aee5f4a6011e8a5d42912b42632df32aad57fc91127c6e04"
+dependencies = [
+ "deadpool-runtime",
+ "num_cpus",
+ "tokio",
+]
+
+[[package]]
+name = "deadpool-redis"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bafa30c49dafe086d10116074e422ad7fc1c3cf554697e744a3ab112599ebd09"
+dependencies = [
+ "deadpool",
+ "redis",
+]
+
+[[package]]
+name = "deadpool-runtime"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2657f61fb1dd8bf37a8d51093cc7cee4e77125b22f7753f49b289f831bec2bae"
+dependencies = [
+ "tokio",
+]
+
+[[package]]
+name = "deflate64"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2"
+
+[[package]]
+name = "delegate"
+version = "0.13.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "780eb241654bf097afb00fc5f054a09b687dad862e485fdcf8399bb056565370"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "der"
+version = "0.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
+dependencies = [
+ "const-oid 0.9.6",
+ "pem-rfc7468 0.7.0",
+ "zeroize",
+]
+
+[[package]]
+name = "deranged"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
+dependencies = [
+ "powerfmt",
+ "serde_core",
+]
+
+[[package]]
+name = "derive_builder"
+version = "0.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
+dependencies = [
+ "derive_builder_macro",
+]
+
+[[package]]
+name = "derive_builder_core"
+version = "0.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
+dependencies = [
+ "darling",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "derive_builder_macro"
+version = "0.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
+dependencies = [
+ "derive_builder_core",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "derive_more"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134"
+dependencies = [
+ "derive_more-impl",
+]
+
+[[package]]
+name = "derive_more-impl"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
+dependencies = [
+ "convert_case 0.10.0",
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "syn 2.0.117",
+ "unicode-xid",
+]
+
+[[package]]
+name = "des"
+version = "0.9.0-rc.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3214053e68a813b9c06ef61075c844f3a1cdeb307d8998ea8555c063caa52fa9"
+dependencies = [
+ "cipher 0.5.1",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer 0.10.4",
+ "const-oid 0.9.6",
+ "crypto-common 0.1.7",
+ "subtle",
+]
+
+[[package]]
+name = "digest"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c"
+dependencies = [
+ "block-buffer 0.12.0",
+ "const-oid 0.10.2",
+ "crypto-common 0.2.1",
+]
+
+[[package]]
+name = "dirs"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "dotenvy"
+version = "0.15.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
+
+[[package]]
+name = "dyn-clone"
+version = "1.0.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
+
+[[package]]
+name = "ecdsa"
+version = "0.16.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
+dependencies = [
+ "der",
+ "digest 0.10.7",
+ "elliptic-curve",
+ "rfc6979",
+ "signature 2.2.0",
+ "spki",
+]
+
+[[package]]
+name = "ed25519"
+version = "2.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
+dependencies = [
+ "pkcs8",
+ "signature 2.2.0",
+]
+
+[[package]]
+name = "ed25519-dalek"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
+dependencies = [
+ "curve25519-dalek",
+ "ed25519",
+ "rand_core 0.6.4",
+ "serde",
+ "sha2 0.10.9",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "educe"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417"
+dependencies = [
+ "enum-ordinalize",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "either"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "elliptic-curve"
+version = "0.13.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
+dependencies = [
+ "base16ct 0.2.0",
+ "crypto-bigint",
+ "digest 0.10.7",
+ "ff",
+ "generic-array 0.14.7",
+ "group",
+ "hkdf",
+ "pem-rfc7468 0.7.0",
+ "pkcs8",
+ "rand_core 0.6.4",
+ "sec1 0.7.3",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "email"
+version = "0.2.9"
+dependencies = [
+ "anyhow",
+ "config",
+ "lettre",
+ "regex",
+ "serde",
+ "tokio",
+]
+
+[[package]]
+name = "email-encoding"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9298e6504d9b9e780ed3f7dfd43a61be8cd0e09eb07f7706a945b0072b6670b6"
+dependencies = [
+ "base64 0.22.1",
+ "memchr",
+]
+
+[[package]]
+name = "email-server"
+version = "0.2.9"
+dependencies = [
+ "anyhow",
+ "chrono",
+ "clap",
+ "config",
+ "db",
+ "service",
+ "slog",
+ "tokio",
+]
+
+[[package]]
+name = "email_address"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449"
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "enum-ordinalize"
+version = "4.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0"
+dependencies = [
+ "enum-ordinalize-derive",
+]
+
+[[package]]
+name = "enum-ordinalize-derive"
+version = "4.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "enum_dispatch"
+version = "0.3.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd"
+dependencies = [
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "equator"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc"
+dependencies = [
+ "equator-macro",
+]
+
+[[package]]
+name = "equator-macro"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "erased-serde"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c138974f9d5e7fe373eb04df7cae98833802ae4b11c24ac7039a21d5af4b26c"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "etcetera"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943"
+dependencies = [
+ "cfg-if",
+ "home",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "event-listener"
+version = "5.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
+dependencies = [
+ "concurrent-queue",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "event-listener-strategy"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
+dependencies = [
+ "event-listener",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "eventsource-stream"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74fef4569247a5f429d9156b9d0a2599914385dd189c539334c625d8099d90ab"
+dependencies = [
+ "futures-core",
+ "nom 7.1.3",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "exr"
+version = "1.74.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be"
+dependencies = [
+ "bit_field",
+ "half",
+ "lebe",
+ "miniz_oxide",
+ "rayon-core",
+ "smallvec",
+ "zune-inflate",
+]
+
+[[package]]
+name = "fancy-regex"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "531e46835a22af56d1e3b66f04844bed63158bc094a628bec1d321d9b4c44bf2"
+dependencies = [
+ "bit-set",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
+
+[[package]]
+name = "fax"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab"
+dependencies = [
+ "fax_derive",
+]
+
+[[package]]
+name = "fax_derive"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "fdeflate"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
+dependencies = [
+ "simd-adler32",
+]
+
+[[package]]
+name = "ff"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
+dependencies = [
+ "rand_core 0.6.4",
+ "subtle",
+]
+
+[[package]]
+name = "fiat-crypto"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
+
+[[package]]
+name = "filetime"
+version = "0.2.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "libredox",
+]
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
+
+[[package]]
+name = "flate2"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+ "zlib-rs",
+]
+
+[[package]]
+name = "flume"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+ "spin",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "funty"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
+
+[[package]]
+name = "futures"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-intrusive"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f"
+dependencies = [
+ "futures-core",
+ "lock_api",
+ "parking_lot",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
+
+[[package]]
+name = "futures-task"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
+
+[[package]]
+name = "futures-timer"
+version = "3.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
+
+[[package]]
+name = "futures-util"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+ "zeroize",
+]
+
+[[package]]
+name = "generic-array"
+version = "1.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eaf57c49a95fd1fe24b90b3033bee6dc7e8f1288d51494cb44e627c295e38542"
+dependencies = [
+ "generic-array 0.14.7",
+ "rustversion",
+ "typenum",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "wasi",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "r-efi 5.3.0",
+ "wasip2",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "r-efi 6.0.0",
+ "rand_core 0.10.0",
+ "wasip2",
+ "wasip3",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "ghash"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1"
+dependencies = [
+ "opaque-debug",
+ "polyval 0.6.2",
+]
+
+[[package]]
+name = "ghash"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2eecf2d5dc9b66b732b97707a0210906b1d30523eb773193ab777c0c84b3e8d5"
+dependencies = [
+ "polyval 0.7.1",
+]
+
+[[package]]
+name = "gif"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e"
+dependencies = [
+ "color_quant",
+ "weezl",
+]
+
+[[package]]
+name = "git"
+version = "0.2.9"
+dependencies = [
+ "actix-web",
+ "anyhow",
+ "async-stream",
+ "base64 0.22.1",
+ "chrono",
+ "config",
+ "db",
+ "deadpool-redis",
+ "flate2",
+ "futures",
+ "futures-util",
+ "git2",
+ "git2-ext",
+ "git2-hooks",
+ "globset",
+ "hex",
+ "models",
+ "num_cpus",
+ "qdrant-client",
+ "redis",
+ "reqwest 0.13.2",
+ "russh",
+ "sea-orm",
+ "serde",
+ "serde_json",
+ "sha1",
+ "sha2 0.11.0",
+ "slog",
+ "ssh-key",
+ "sysinfo",
+ "tar",
+ "tokio",
+ "tokio-util",
+ "uuid",
+ "zip",
+]
+
+[[package]]
+name = "git-hook"
+version = "0.2.9"
+dependencies = [
+ "anyhow",
+ "chrono",
+ "clap",
+ "config",
+ "db",
+ "git",
+ "reqwest 0.13.2",
+ "slog",
+ "tokio",
+ "tokio-util",
+ "tracing",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "git2"
+version = "0.20.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b"
+dependencies = [
+ "bitflags",
+ "libc",
+ "libgit2-sys",
+ "log",
+ "openssl-probe 0.1.6",
+ "openssl-sys",
+ "url",
+]
+
+[[package]]
+name = "git2-ext"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b351b3ef9a04bbd24822138469eddbc8db7c85ae81a13acb6cbb803053de19b"
+dependencies = [
+ "bstr",
+ "git2",
+ "itertools",
+ "log",
+ "pkg-config",
+ "shlex",
+ "tempfile",
+ "which",
+]
+
+[[package]]
+name = "git2-hooks"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e21a2c5eee3085f2b622805d4f4c878d9519bf70fa76bed0923b82a25f24199"
+dependencies = [
+ "git2",
+ "gix-path",
+ "log",
+ "shellexpand",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "gitserver"
+version = "0.2.9"
+dependencies = [
+ "anyhow",
+ "chrono",
+ "clap",
+ "config",
+ "db",
+ "git",
+ "slog",
+ "tokio",
+]
+
+[[package]]
+name = "gix-path"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09c31d4373bda7fab9eb01822927b55185a378d6e1bf737e0a54c743ad806658"
+dependencies = [
+ "bstr",
+ "gix-trace",
+ "gix-validate",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "gix-trace"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f69a13643b8437d4ca6845e08143e847a36ca82903eed13303475d0ae8b162e0"
+
+[[package]]
+name = "gix-validate"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ec1eff98d91941f47766367cba1be746bab662bad761d9891ae6f7882f7840b"
+dependencies = [
+ "bstr",
+]
+
+[[package]]
+name = "glam"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "333928d5eb103c5d4050533cec0384302db6be8ef7d3cebd30ec6a35350353da"
+
+[[package]]
+name = "glam"
+version = "0.15.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3abb554f8ee44336b72d522e0a7fe86a29e09f839a36022fa869a7dfe941a54b"
+
+[[package]]
+name = "glam"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4126c0479ccf7e8664c36a2d719f5f2c140fbb4f9090008098d2c291fa5b3f16"
+
+[[package]]
+name = "glam"
+version = "0.17.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e01732b97afd8508eee3333a541b9f7610f454bb818669e66e90f5f57c93a776"
+
+[[package]]
+name = "glam"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "525a3e490ba77b8e326fb67d4b44b4bd2f920f44d4cc73ccec50adc68e3bee34"
+
+[[package]]
+name = "glam"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b8509e6791516e81c1a630d0bd7fbac36d2fa8712a9da8662e716b52d5051ca"
+
+[[package]]
+name = "glam"
+version = "0.20.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f43e957e744be03f5801a55472f593d43fabdebf25a4585db250f04d86b1675f"
+
+[[package]]
+name = "glam"
+version = "0.21.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "518faa5064866338b013ff9b2350dc318e14cc4fcd6cb8206d7e7c9886c98815"
+
+[[package]]
+name = "glam"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12f597d56c1bd55a811a1be189459e8fad2bbc272616375602443bdfb37fa774"
+
+[[package]]
+name = "glam"
+version = "0.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e4afd9ad95555081e109fe1d21f2a30c691b5f0919c67dfa690a2e1eb6bd51c"
+
+[[package]]
+name = "glam"
+version = "0.24.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5418c17512bdf42730f9032c74e1ae39afc408745ebb2acf72fbc4691c17945"
+
+[[package]]
+name = "glam"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "151665d9be52f9bb40fc7966565d39666f2d1e69233571b71b87791c7e0528b3"
+
+[[package]]
+name = "glam"
+version = "0.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e05e7e6723e3455f4818c7b26e855439f7546cf617ef669d1adedb8669e5cb9"
+
+[[package]]
+name = "glam"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "779ae4bf7e8421cf91c0b3b64e7e8b40b862fba4d393f59150042de7c4965a94"
+
+[[package]]
+name = "glam"
+version = "0.29.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8babf46d4c1c9d92deac9f7be466f76dfc4482b6452fc5024b5e8daf6ffeb3ee"
+
+[[package]]
+name = "glam"
+version = "0.30.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19fc433e8437a212d1b6f1e68c7824af3aed907da60afa994e7f542d18d12aa9"
+
+[[package]]
+name = "glam"
+version = "0.31.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "556f6b2ea90b8d15a74e0e7bb41671c9bdf38cd9f78c284d750b9ce58a2b5be7"
+
+[[package]]
+name = "glam"
+version = "0.32.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f70749695b063ecbf6b62949ccccde2e733ec3ecbbd71d467dca4e5c6c97cca0"
+
+[[package]]
+name = "glob"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
+
+[[package]]
+name = "globset"
+version = "0.4.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3"
+dependencies = [
+ "aho-corasick",
+ "bstr",
+ "log",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "group"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
+dependencies = [
+ "ff",
+ "rand_core 0.6.4",
+ "subtle",
+]
+
+[[package]]
+name = "h2"
+version = "0.3.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d"
+dependencies = [
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "http 0.2.12",
+ "indexmap 2.13.0",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "h2"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "http 1.4.0",
+ "indexmap 2.13.0",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "half"
+version = "2.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
+dependencies = [
+ "cfg-if",
+ "crunchy",
+ "num-traits",
+ "zerocopy",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+dependencies = [
+ "ahash 0.7.8",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+dependencies = [
+ "allocator-api2",
+ "equivalent",
+ "foldhash",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
+
+[[package]]
+name = "hashlink"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
+dependencies = [
+ "hashbrown 0.15.5",
+]
+
+[[package]]
+name = "hax-lib"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74d9ba66d1739c68e0219b2b2238b5c4145f491ebf181b9c6ab561a19352ae86"
+dependencies = [
+ "hax-lib-macros",
+ "num-bigint",
+ "num-traits",
+]
+
+[[package]]
+name = "hax-lib-macros"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24ba777a231a58d1bce1d68313fa6b6afcc7966adef23d60f45b8a2b9b688bf1"
+dependencies = [
+ "hax-lib-macros-types",
+ "proc-macro-error2",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "hax-lib-macros-types"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "867e19177d7425140b417cd27c2e05320e727ee682e98368f88b7194e80ad515"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "serde",
+ "serde_json",
+ "uuid",
+]
+
+[[package]]
+name = "headers"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "headers-core",
+ "http 1.4.0",
+ "httpdate",
+ "mime",
+ "sha1",
+]
+
+[[package]]
+name = "headers-core"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4"
+dependencies = [
+ "http 1.4.0",
+]
+
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hermit-abi"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "hex-literal"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46"
+
+[[package]]
+name = "hkdf"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
+dependencies = [
+ "hmac",
+]
+
+[[package]]
+name = "hmac"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
+dependencies = [
+ "digest 0.10.7",
+]
+
+[[package]]
+name = "home"
+version = "0.5.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "hostname"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "windows-link",
+]
+
+[[package]]
+name = "http"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
+dependencies = [
+ "bytes",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
+dependencies = [
+ "bytes",
+ "http 1.4.0",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http 1.4.0",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+[[package]]
+name = "hybrid-array"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8655f91cd07f2b9d0c24137bd650fe69617773435ee5ec83022377777ce65ef1"
+dependencies = [
+ "typenum",
+ "zeroize",
+]
+
+[[package]]
+name = "hyper"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "h2 0.4.13",
+ "http 1.4.0",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "pin-utils",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-http-proxy"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ad4b0a1e37510028bc4ba81d0e38d239c39671b0f0ce9e02dfa93a8133f7c08"
+dependencies = [
+ "bytes",
+ "futures-util",
+ "headers",
+ "http 1.4.0",
+ "hyper",
+ "hyper-rustls",
+ "hyper-util",
+ "pin-project-lite",
+ "rustls-native-certs 0.7.3",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+]
+
+[[package]]
+name = "hyper-rustls"
+version = "0.27.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
+dependencies = [
+ "http 1.4.0",
+ "hyper",
+ "hyper-util",
+ "log",
+ "rustls",
+ "rustls-native-certs 0.8.3",
+ "rustls-pki-types",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+ "webpki-roots",
+]
+
+[[package]]
+name = "hyper-timeout"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0"
+dependencies = [
+ "hyper",
+ "hyper-util",
+ "pin-project-lite",
+ "tokio",
+ "tower-service",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http 1.4.0",
+ "http-body",
+ "hyper",
+ "ipnet",
+ "libc",
+ "percent-encoding",
+ "pin-project-lite",
+ "socket2 0.6.3",
+ "tokio",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.65"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "log",
+ "wasm-bindgen",
+ "windows-core",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "icu_collections"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599"
+dependencies = [
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a"
+
+[[package]]
+name = "icu_properties"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec"
+dependencies = [
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af"
+
+[[package]]
+name = "icu_provider"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "id-arena"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
+
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
+[[package]]
+name = "idna"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "image"
+version = "0.25.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
+dependencies = [
+ "bytemuck",
+ "byteorder-lite",
+ "color_quant",
+ "exr",
+ "gif",
+ "image-webp",
+ "moxcms",
+ "num-traits",
+ "png",
+ "qoi",
+ "ravif",
+ "rayon",
+ "rgb",
+ "tiff",
+ "zune-core",
+ "zune-jpeg",
+]
+
+[[package]]
+name = "image-webp"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3"
+dependencies = [
+ "byteorder-lite",
+ "quick-error",
+]
+
+[[package]]
+name = "imageproc"
+version = "0.26.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a8046da590889acc65f5880004580ebb269bbef84d6c0f5c543ec2dece46638"
+dependencies = [
+ "ab_glyph",
+ "approx",
+ "getrandom 0.3.4",
+ "image",
+ "itertools",
+ "nalgebra",
+ "num",
+ "rand 0.9.2",
+ "rand_distr",
+ "rayon",
+ "rustdct",
+]
+
+[[package]]
+name = "imgref"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8"
+
+[[package]]
+name = "impl-more"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2"
+
+[[package]]
+name = "indexmap"
+version = "1.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
+dependencies = [
+ "autocfg",
+ "hashbrown 0.12.3",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.16.1",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "indoc"
+version = "2.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
+dependencies = [
+ "rustversion",
+]
+
+[[package]]
+name = "inherent"
+version = "1.0.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c727f80bfa4a6c6e2508d2f05b6f4bfce242030bd88ed15ae5331c5b5d30fba7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "inout"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
+dependencies = [
+ "block-padding",
+ "generic-array 0.14.7",
+]
+
+[[package]]
+name = "inout"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7"
+dependencies = [
+ "hybrid-array",
+]
+
+[[package]]
+name = "instant"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "internal-russh-forked-ssh-key"
+version = "0.6.11+upstream-0.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0a77eae781ed6a7709fb15b64862fcca13d886b07c7e2786f5ed34e5e2b9187"
+dependencies = [
+ "argon2",
+ "bcrypt-pbkdf",
+ "ecdsa",
+ "ed25519-dalek",
+ "hex",
+ "hmac",
+ "p256",
+ "p384",
+ "p521",
+ "rand_core 0.6.4",
+ "rsa",
+ "sec1 0.7.3",
+ "sha1",
+ "sha2 0.10.9",
+ "signature 2.2.0",
+ "ssh-cipher 0.2.0",
+ "ssh-encoding 0.2.0",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "interpolate_name"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
+
+[[package]]
+name = "iri-string"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20"
+dependencies = [
+ "memchr",
+ "serde",
+]
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
+
+[[package]]
+name = "itertools"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "jobserver"
+version = "0.1.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
+dependencies = [
+ "getrandom 0.3.4",
+ "libc",
+]
+
+[[package]]
+name = "js-sys"
+version = "0.3.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc4c90f45aa2e6eacbe8645f77fdea542ac97a494bcd117a67df9ff4d611f995"
+dependencies = [
+ "cfg-if",
+ "futures-util",
+ "once_cell",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "json-patch"
+version = "3.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08"
+dependencies = [
+ "jsonptr",
+ "serde",
+ "serde_json",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "jsonpath-rust"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c00ae348f9f8fd2d09f82a98ca381c60df9e0820d8d79fce43e649b4dc3128b"
+dependencies = [
+ "pest",
+ "pest_derive",
+ "regex",
+ "serde_json",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "jsonptr"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70"
+dependencies = [
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "k8s-openapi"
+version = "0.24.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c75b990324f09bef15e791606b7b7a296d02fc88a344f6eba9390970a870ad5"
+dependencies = [
+ "base64 0.22.1",
+ "chrono",
+ "schemars",
+ "serde",
+ "serde-value",
+ "serde_json",
+]
+
+[[package]]
+name = "kube"
+version = "0.98.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32053dc495efad4d188c7b33cc7c02ef4a6e43038115348348876efd39a53cba"
+dependencies = [
+ "k8s-openapi",
+ "kube-client",
+ "kube-core",
+ "kube-derive",
+ "kube-runtime",
+]
+
+[[package]]
+name = "kube-client"
+version = "0.98.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d34ad38cdfbd1fa87195d42569f57bb1dda6ba5f260ee32fef9570b7937a0c9"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "chrono",
+ "either",
+ "futures",
+ "home",
+ "http 1.4.0",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-http-proxy",
+ "hyper-rustls",
+ "hyper-timeout",
+ "hyper-util",
+ "jsonpath-rust",
+ "k8s-openapi",
+ "kube-core",
+ "pem",
+ "rustls",
+ "rustls-pemfile",
+ "secrecy",
+ "serde",
+ "serde_json",
+ "serde_yaml",
+ "thiserror 2.0.18",
+ "tokio",
+ "tokio-util",
+ "tower 0.5.3",
+ "tower-http",
+ "tracing",
+]
+
+[[package]]
+name = "kube-core"
+version = "0.98.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97aa830b288a178a90e784d1b0f1539f2d200d2188c7b4a3146d9dc983d596f3"
+dependencies = [
+ "chrono",
+ "form_urlencoded",
+ "http 1.4.0",
+ "json-patch",
+ "k8s-openapi",
+ "schemars",
+ "serde",
+ "serde-value",
+ "serde_json",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "kube-derive"
+version = "0.98.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37745d8a4076b77e0b1952e94e358726866c8e14ec94baaca677d47dcdb98658"
+dependencies = [
+ "darling",
+ "proc-macro2",
+ "quote",
+ "serde_json",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "kube-runtime"
+version = "0.98.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a41af186a0fe80c71a13a13994abdc3ebff80859ca6a4b8a6079948328c135b"
+dependencies = [
+ "ahash 0.8.12",
+ "async-broadcast",
+ "async-stream",
+ "async-trait",
+ "backoff",
+ "educe",
+ "futures",
+ "hashbrown 0.15.5",
+ "hostname",
+ "json-patch",
+ "jsonptr",
+ "k8s-openapi",
+ "kube-client",
+ "parking_lot",
+ "pin-project",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.18",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "language-tags"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388"
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+dependencies = [
+ "spin",
+]
+
+[[package]]
+name = "leb128fmt"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
+
+[[package]]
+name = "lebe"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8"
+
+[[package]]
+name = "lettre"
+version = "0.11.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "471816f3e24b85e820dee02cde962379ea1a669e5242f19c61bcbcffedf4c4fb"
+dependencies = [
+ "async-trait",
+ "base64 0.22.1",
+ "email-encoding",
+ "email_address",
+ "fastrand",
+ "futures-io",
+ "futures-util",
+ "httpdate",
+ "idna",
+ "mime",
+ "nom 8.0.0",
+ "percent-encoding",
+ "quoted_printable",
+ "rustls",
+ "socket2 0.6.3",
+ "tokio",
+ "tokio-rustls",
+ "url",
+ "webpki-roots",
+]
+
+[[package]]
+name = "lexical-core"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d8d125a277f807e55a77304455eb7b1cb52f2b18c143b60e766c120bd64a594"
+dependencies = [
+ "lexical-parse-float",
+ "lexical-parse-integer",
+ "lexical-util",
+ "lexical-write-float",
+ "lexical-write-integer",
+]
+
+[[package]]
+name = "lexical-parse-float"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52a9f232fbd6f550bc0137dcb5f99ab674071ac2d690ac69704593cb4abbea56"
+dependencies = [
+ "lexical-parse-integer",
+ "lexical-util",
+]
+
+[[package]]
+name = "lexical-parse-integer"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a7a039f8fb9c19c996cd7b2fcce303c1b2874fe1aca544edc85c4a5f8489b34"
+dependencies = [
+ "lexical-util",
+]
+
+[[package]]
+name = "lexical-util"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2604dd126bb14f13fb5d1bd6a66155079cb9fa655b37f875b3a742c705dbed17"
+
+[[package]]
+name = "lexical-write-float"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50c438c87c013188d415fbabbb1dceb44249ab81664efbd31b14ae55dabb6361"
+dependencies = [
+ "lexical-util",
+ "lexical-write-integer",
+]
+
+[[package]]
+name = "lexical-write-integer"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "409851a618475d2d5796377cad353802345cba92c867d9fbcde9cf4eac4e14df"
+dependencies = [
+ "lexical-util",
+]
+
+[[package]]
+name = "libbz2-rs-sys"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7"
+
+[[package]]
+name = "libc"
+version = "0.2.183"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
+
+[[package]]
+name = "libcrux-intrinsics"
+version = "0.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc9ee7ef66569dd7516454fe26de4e401c0c62073929803486b96744594b9632"
+dependencies = [
+ "core-models",
+ "hax-lib",
+]
+
+[[package]]
+name = "libcrux-ml-kem"
+version = "0.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bb6a88086bf11bd2ec90926c749c4a427f2e59841437dbdede8cde8a96334ab"
+dependencies = [
+ "hax-lib",
+ "libcrux-intrinsics",
+ "libcrux-platform",
+ "libcrux-secrets",
+ "libcrux-sha3",
+ "libcrux-traits",
+ "rand 0.9.2",
+ "tls_codec",
+]
+
+[[package]]
+name = "libcrux-platform"
+version = "0.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db82d058aa76ea315a3b2092f69dfbd67ddb0e462038a206e1dcd73f058c0778"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "libcrux-secrets"
+version = "0.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e4dbbf6bc9f2bc0f20dc3bea3e5c99adff3bdccf6d2a40488963da69e2ec307"
+dependencies = [
+ "hax-lib",
+]
+
+[[package]]
+name = "libcrux-sha3"
+version = "0.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2400bec764d1c75b8a496d5747cffe32f1fb864a12577f0aca2f55a92021c962"
+dependencies = [
+ "hax-lib",
+ "libcrux-intrinsics",
+ "libcrux-platform",
+ "libcrux-traits",
+]
+
+[[package]]
+name = "libcrux-traits"
+version = "0.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9adfd58e79d860f6b9e40e35127bfae9e5bd3ade33201d1347459011a2add034"
+dependencies = [
+ "libcrux-secrets",
+ "rand 0.9.2",
+]
+
+[[package]]
+name = "libfuzzer-sys"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d"
+dependencies = [
+ "arbitrary",
+ "cc",
+]
+
+[[package]]
+name = "libgit2-sys"
+version = "0.18.3+1.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9b3acc4b91781bb0b3386669d325163746af5f6e4f73e6d2d630e09a35f3487"
+dependencies = [
+ "cc",
+ "libc",
+ "libssh2-sys",
+ "libz-sys",
+ "openssl-sys",
+ "pkg-config",
+]
+
+[[package]]
+name = "libm"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
+
+[[package]]
+name = "libredox"
+version = "0.1.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08"
+dependencies = [
+ "bitflags",
+ "libc",
+ "plain",
+ "redox_syscall 0.7.3",
+]
+
+[[package]]
+name = "libsqlite3-sys"
+version = "0.30.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
+dependencies = [
+ "cc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "libssh2-sys"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "220e4f05ad4a218192533b300327f5150e809b54c4ec83b5a1d91833601811b9"
+dependencies = [
+ "cc",
+ "libc",
+ "libz-sys",
+ "openssl-sys",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "libz-sys"
+version = "1.1.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d52f4c29e2a68ac30c9087e1b772dc9f44a2b66ed44edf2266cf2be9b03dafc1"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+
+[[package]]
+name = "litemap"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
+
+[[package]]
+name = "local-channel"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6cbc85e69b8df4b8bb8b89ec634e7189099cea8927a276b7384ce5488e53ec8"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+ "local-waker",
+]
+
+[[package]]
+name = "local-waker"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4d873d7c67ce09b42110d801813efbc9364414e356be9935700d368351657487"
+
+[[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+
+[[package]]
+name = "loop9"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062"
+dependencies = [
+ "imgref",
+]
+
+[[package]]
+name = "lru"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
+dependencies = [
+ "hashbrown 0.15.5",
+]
+
+[[package]]
+name = "lru-slab"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
+
+[[package]]
+name = "lzma-rust2"
+version = "0.16.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47bb1e988e6fb779cf720ad431242d3f03167c1b3f2b1aae7f1a94b2495b36ae"
+dependencies = [
+ "sha2 0.10.9",
+]
+
+[[package]]
+name = "mac_address"
+version = "1.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303"
+dependencies = [
+ "nix",
+ "serde",
+ "winapi",
+]
+
+[[package]]
+name = "matchers"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
+dependencies = [
+ "regex-automata",
+]
+
+[[package]]
+name = "matchit"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
+
+[[package]]
+name = "matrixmultiply"
+version = "0.3.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08"
+dependencies = [
+ "autocfg",
+ "rawpointer",
+]
+
+[[package]]
+name = "maybe-rayon"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519"
+dependencies = [
+ "cfg-if",
+ "rayon",
+]
+
+[[package]]
+name = "md-5"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
+dependencies = [
+ "cfg-if",
+ "digest 0.10.7",
+]
+
+[[package]]
+name = "md5"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
+
+[[package]]
+name = "memchr"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
+
+[[package]]
+name = "memoffset"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "metrics"
+version = "0.21.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fde3af1a009ed76a778cb84fdef9e7dbbdf5775ae3e4cc1f434a6a307f6f76c5"
+dependencies = [
+ "ahash 0.8.12",
+ "metrics-macros",
+ "portable-atomic",
+]
+
+[[package]]
+name = "metrics-macros"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38b4faf00617defe497754acde3024865bc143d44a86799b24e191ecff91354f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "migrate"
+version = "0.2.9"
+dependencies = [
+ "async-trait",
+ "models",
+ "sea-orm",
+ "sea-orm-migration",
+ "sea-query",
+]
+
+[[package]]
+name = "migrate-cli"
+version = "0.2.9"
+dependencies = [
+ "anyhow",
+ "clap",
+ "config",
+ "dotenvy",
+ "migrate",
+ "sea-orm",
+ "tokio",
+]
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "mime_guess"
+version = "2.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
+dependencies = [
+ "mime",
+ "unicase",
+]
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
+[[package]]
+name = "mio"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
+dependencies = [
+ "libc",
+ "log",
+ "wasi",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "models"
+version = "0.2.9"
+dependencies = [
+ "chrono",
+ "rust_decimal",
+ "sea-orm",
+ "serde",
+ "serde_json",
+ "utoipa",
+ "uuid",
+]
+
+[[package]]
+name = "moka"
+version = "0.12.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046"
+dependencies = [
+ "async-lock",
+ "crossbeam-channel",
+ "crossbeam-epoch",
+ "crossbeam-utils",
+ "equivalent",
+ "event-listener",
+ "futures-util",
+ "parking_lot",
+ "portable-atomic",
+ "smallvec",
+ "tagptr",
+ "uuid",
+]
+
+[[package]]
+name = "moxcms"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
+dependencies = [
+ "num-traits",
+ "pxfm",
+]
+
+[[package]]
+name = "nalgebra"
+version = "0.34.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df76ea0ff5c7e6b88689085804d6132ded0ddb9de5ca5b8aeb9eeadc0508a70a"
+dependencies = [
+ "approx",
+ "glam 0.14.0",
+ "glam 0.15.2",
+ "glam 0.16.0",
+ "glam 0.17.3",
+ "glam 0.18.0",
+ "glam 0.19.0",
+ "glam 0.20.5",
+ "glam 0.21.3",
+ "glam 0.22.0",
+ "glam 0.23.0",
+ "glam 0.24.2",
+ "glam 0.25.0",
+ "glam 0.27.0",
+ "glam 0.28.0",
+ "glam 0.29.3",
+ "glam 0.30.10",
+ "glam 0.31.1",
+ "glam 0.32.1",
+ "matrixmultiply",
+ "num-complex",
+ "num-rational",
+ "num-traits",
+ "simba",
+ "typenum",
+]
+
+[[package]]
+name = "new_debug_unreachable"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
+
+[[package]]
+name = "nix"
+version = "0.29.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "cfg_aliases",
+ "libc",
+ "memoffset",
+]
+
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "nom"
+version = "8.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "noop_proc_macro"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
+
+[[package]]
+name = "ntapi"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "nu-ansi-term"
+version = "0.50.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "num"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
+dependencies = [
+ "num-complex",
+ "num-integer",
+ "num-iter",
+ "num-rational",
+ "num-traits",
+]
+
+[[package]]
+name = "num-bigint"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
+dependencies = [
+ "num-integer",
+ "num-traits",
+ "rand 0.8.5",
+]
+
+[[package]]
+name = "num-bigint-dig"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7"
+dependencies = [
+ "lazy_static",
+ "libm",
+ "num-integer",
+ "num-iter",
+ "num-traits",
+ "rand 0.8.5",
+ "smallvec",
+ "zeroize",
+]
+
+[[package]]
+name = "num-complex"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-conv"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967"
+
+[[package]]
+name = "num-derive"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-iter"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-rational"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
+dependencies = [
+ "num-bigint",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+ "libm",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "objc2-core-foundation"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "objc2-io-kit"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15"
+dependencies = [
+ "libc",
+ "objc2-core-foundation",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+
+[[package]]
+name = "opaque-debug"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
+
+[[package]]
+name = "openssl-probe"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.112"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "operator"
+version = "0.2.9"
+dependencies = [
+ "anyhow",
+ "chrono",
+ "futures",
+ "k8s-openapi",
+ "kube",
+ "serde",
+ "serde_json",
+ "serde_yaml",
+ "tokio",
+ "tracing",
+ "tracing-subscriber",
+ "uuid",
+]
+
+[[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
+[[package]]
+name = "ordered-float"
+version = "2.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "ordered-float"
+version = "4.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "ouroboros"
+version = "0.18.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e0f050db9c44b97a94723127e6be766ac5c340c48f2c4bb3ffa11713744be59"
+dependencies = [
+ "aliasable",
+ "ouroboros_macro",
+ "static_assertions",
+]
+
+[[package]]
+name = "ouroboros_macro"
+version = "0.18.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c7028bdd3d43083f6d8d4d5187680d0d3560d54df4cc9d752005268b41e64d0"
+dependencies = [
+ "heck 0.4.1",
+ "proc-macro2",
+ "proc-macro2-diagnostics",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "owned_ttf_parser"
+version = "0.25.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b"
+dependencies = [
+ "ttf-parser",
+]
+
+[[package]]
+name = "p256"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b"
+dependencies = [
+ "ecdsa",
+ "elliptic-curve",
+ "primeorder",
+ "sha2 0.10.9",
+]
+
+[[package]]
+name = "p384"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6"
+dependencies = [
+ "ecdsa",
+ "elliptic-curve",
+ "primeorder",
+ "sha2 0.10.9",
+]
+
+[[package]]
+name = "p521"
+version = "0.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2"
+dependencies = [
+ "base16ct 0.2.0",
+ "ecdsa",
+ "elliptic-curve",
+ "primeorder",
+ "rand_core 0.6.4",
+ "sha2 0.10.9",
+]
+
+[[package]]
+name = "pageant"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b537f975f6d8dcf48db368d7ec209d583b015713b5df0f5d92d2631e4ff5595"
+dependencies = [
+ "byteorder",
+ "bytes",
+ "delegate",
+ "futures",
+ "log",
+ "rand 0.8.5",
+ "sha2 0.10.9",
+ "thiserror 1.0.69",
+ "tokio",
+ "windows",
+ "windows-strings",
+]
+
+[[package]]
+name = "parking"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall 0.5.18",
+ "smallvec",
+ "windows-link",
+]
+
+[[package]]
+name = "password-hash"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
+dependencies = [
+ "base64ct",
+ "rand_core 0.6.4",
+ "subtle",
+]
+
+[[package]]
+name = "paste"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+
+[[package]]
+name = "pastey"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
+
+[[package]]
+name = "pbkdf2"
+version = "0.12.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
+dependencies = [
+ "digest 0.10.7",
+ "hmac",
+]
+
+[[package]]
+name = "pem"
+version = "3.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be"
+dependencies = [
+ "base64 0.22.1",
+ "serde_core",
+]
+
+[[package]]
+name = "pem-rfc7468"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
+dependencies = [
+ "base64ct",
+]
+
+[[package]]
+name = "pem-rfc7468"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9"
+dependencies = [
+ "base64ct",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
+name = "pest"
+version = "2.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662"
+dependencies = [
+ "memchr",
+ "ucd-trie",
+]
+
+[[package]]
+name = "pest_derive"
+version = "2.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77"
+dependencies = [
+ "pest",
+ "pest_generator",
+]
+
+[[package]]
+name = "pest_generator"
+version = "2.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f"
+dependencies = [
+ "pest",
+ "pest_meta",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "pest_meta"
+version = "2.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220"
+dependencies = [
+ "pest",
+ "sha2 0.10.9",
+]
+
+[[package]]
+name = "pgvector"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc58e2d255979a31caa7cabfa7aac654af0354220719ab7a68520ae7a91e8c0b"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "pin-project"
+version = "1.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pkcs1"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
+dependencies = [
+ "der",
+ "pkcs8",
+ "spki",
+]
+
+[[package]]
+name = "pkcs5"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e847e2c91a18bfa887dd028ec33f2fe6f25db77db3619024764914affe8b69a6"
+dependencies = [
+ "aes 0.8.4",
+ "cbc",
+ "der",
+ "pbkdf2",
+ "scrypt",
+ "sha2 0.10.9",
+ "spki",
+]
+
+[[package]]
+name = "pkcs8"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
+dependencies = [
+ "der",
+ "pkcs5",
+ "rand_core 0.6.4",
+ "spki",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
+
+[[package]]
+name = "plain"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
+
+[[package]]
+name = "pluralizer"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b3eba432a00a1f6c16f39147847a870e94e2e9b992759b503e330efec778cbe"
+dependencies = [
+ "once_cell",
+ "regex",
+]
+
+[[package]]
+name = "png"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
+dependencies = [
+ "bitflags",
+ "crc32fast",
+ "fdeflate",
+ "flate2",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "poly1305"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
+dependencies = [
+ "cpufeatures 0.2.17",
+ "opaque-debug",
+ "universal-hash 0.5.1",
+]
+
+[[package]]
+name = "poly1305"
+version = "0.9.0-rc.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19feddcbdf17fad33f40041c7f9e768faf19455f32a6d52ba1b8b65ffc7b1cae"
+dependencies = [
+ "cpufeatures 0.3.0",
+ "universal-hash 0.6.1",
+ "zeroize",
+]
+
+[[package]]
+name = "polyval"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.2.17",
+ "opaque-debug",
+ "universal-hash 0.5.1",
+]
+
+[[package]]
+name = "polyval"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7dfc63250416fea14f5749b90725916a6c903f599d51cb635aa7a52bfd03eede"
+dependencies = [
+ "cpubits",
+ "cpufeatures 0.3.0",
+ "universal-hash 0.6.1",
+]
+
+[[package]]
+name = "portable-atomic"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
+
+[[package]]
+name = "potential_utf"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
+name = "ppmd-rust"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "efca4c95a19a79d1c98f791f10aebd5c1363b473244630bb7dbde1dc98455a24"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "prettyplease"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "primal-check"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc0d895b311e3af9902528fbb8f928688abbd95872819320517cc24ca6b2bd08"
+dependencies = [
+ "num-integer",
+]
+
+[[package]]
+name = "primeorder"
+version = "0.13.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6"
+dependencies = [
+ "elliptic-curve",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "3.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
+dependencies = [
+ "toml_edit",
+]
+
+[[package]]
+name = "proc-macro-error-attr2"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+]
+
+[[package]]
+name = "proc-macro-error2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
+dependencies = [
+ "proc-macro-error-attr2",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "proc-macro2-diagnostics"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+ "version_check",
+ "yansi",
+]
+
+[[package]]
+name = "profiling"
+version = "1.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773"
+dependencies = [
+ "profiling-procmacros",
+]
+
+[[package]]
+name = "profiling-procmacros"
+version = "1.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b"
+dependencies = [
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "prost"
+version = "0.13.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5"
+dependencies = [
+ "bytes",
+ "prost-derive",
+]
+
+[[package]]
+name = "prost-derive"
+version = "0.13.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d"
+dependencies = [
+ "anyhow",
+ "itertools",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "prost-types"
+version = "0.13.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16"
+dependencies = [
+ "prost",
+]
+
+[[package]]
+name = "ptr_meta"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1"
+dependencies = [
+ "ptr_meta_derive",
+]
+
+[[package]]
+name = "ptr_meta_derive"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "pxfm"
+version = "0.1.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d"
+
+[[package]]
+name = "qdrant-client"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f5d0a9b168ecf8f30a3eb7e8f4766e3050701242ffbe99838b58e6c4251e7211"
+dependencies = [
+ "anyhow",
+ "derive_builder",
+ "futures",
+ "futures-util",
+ "parking_lot",
+ "prost",
+ "prost-types",
+ "reqwest 0.12.28",
+ "semver",
+ "serde",
+ "serde_json",
+ "thiserror 1.0.69",
+ "tokio",
+ "tonic",
+]
+
+[[package]]
+name = "qoi"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001"
+dependencies = [
+ "bytemuck",
+]
+
+[[package]]
+name = "queue"
+version = "0.2.9"
+dependencies = [
+ "anyhow",
+ "chrono",
+ "deadpool-redis",
+ "futures",
+ "redis",
+ "serde",
+ "serde_json",
+ "slog",
+ "thiserror 2.0.18",
+ "tokio",
+ "tokio-stream",
+ "uuid",
+]
+
+[[package]]
+name = "quick-error"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
+
+[[package]]
+name = "quinn"
+version = "0.11.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
+dependencies = [
+ "bytes",
+ "cfg_aliases",
+ "pin-project-lite",
+ "quinn-proto",
+ "quinn-udp",
+ "rustc-hash 2.1.2",
+ "rustls",
+ "socket2 0.6.3",
+ "thiserror 2.0.18",
+ "tokio",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-proto"
+version = "0.11.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
+dependencies = [
+ "bytes",
+ "getrandom 0.3.4",
+ "lru-slab",
+ "rand 0.9.2",
+ "ring",
+ "rustc-hash 2.1.2",
+ "rustls",
+ "rustls-pki-types",
+ "slab",
+ "thiserror 2.0.18",
+ "tinyvec",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-udp"
+version = "0.5.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
+dependencies = [
+ "cfg_aliases",
+ "libc",
+ "once_cell",
+ "socket2 0.6.3",
+ "tracing",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "quoted_printable"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "478e0585659a122aa407eb7e3c0e1fa51b1d8a870038bd29f0cf4a8551eea972"
+
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "r-efi"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+
+[[package]]
+name = "radium"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
+dependencies = [
+ "rand_chacha 0.9.0",
+ "rand_core 0.9.5",
+]
+
+[[package]]
+name = "rand"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8"
+dependencies = [
+ "chacha20 0.10.0",
+ "getrandom 0.4.2",
+ "rand_core 0.10.0",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.9.5",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom 0.2.17",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
+dependencies = [
+ "getrandom 0.3.4",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba"
+
+[[package]]
+name = "rand_distr"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a8615d50dcf34fa31f7ab52692afec947c4dd0ab803cc87cb3b0b4570ff7463"
+dependencies = [
+ "num-traits",
+ "rand 0.9.2",
+]
+
+[[package]]
+name = "rav1e"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b"
+dependencies = [
+ "aligned-vec",
+ "arbitrary",
+ "arg_enum_proc_macro",
+ "arrayvec",
+ "av-scenechange",
+ "av1-grain",
+ "bitstream-io",
+ "built",
+ "cfg-if",
+ "interpolate_name",
+ "itertools",
+ "libc",
+ "libfuzzer-sys",
+ "log",
+ "maybe-rayon",
+ "new_debug_unreachable",
+ "noop_proc_macro",
+ "num-derive",
+ "num-traits",
+ "paste",
+ "profiling",
+ "rand 0.9.2",
+ "rand_chacha 0.9.0",
+ "simd_helpers",
+ "thiserror 2.0.18",
+ "v_frame",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "ravif"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45"
+dependencies = [
+ "avif-serialize",
+ "imgref",
+ "loop9",
+ "quick-error",
+ "rav1e",
+ "rayon",
+ "rgb",
+]
+
+[[package]]
+name = "rawpointer"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
+
+[[package]]
+name = "rayon"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
+dependencies = [
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
+dependencies = [
+ "crossbeam-deque",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "redis"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d76e41a79ae5cbb41257d84cf4cf0db0bb5a95b11bf05c62c351de4fe748620d"
+dependencies = [
+ "arc-swap",
+ "arcstr",
+ "async-lock",
+ "backon",
+ "bytes",
+ "cfg-if",
+ "combine",
+ "crc16",
+ "futures-channel",
+ "futures-util",
+ "itoa",
+ "log",
+ "num-bigint",
+ "percent-encoding",
+ "pin-project-lite",
+ "rand 0.9.2",
+ "ryu",
+ "sha1_smol",
+ "socket2 0.6.3",
+ "tokio",
+ "tokio-util",
+ "url",
+ "xxhash-rust",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
+dependencies = [
+ "getrandom 0.2.17",
+ "libredox",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "regex"
+version = "1.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-lite"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973"
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
+
+[[package]]
+name = "rend"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c"
+dependencies = [
+ "bytecheck",
+]
+
+[[package]]
+name = "reqwest"
+version = "0.12.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "futures-core",
+ "futures-util",
+ "h2 0.4.13",
+ "http 1.4.0",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-rustls",
+ "hyper-util",
+ "js-sys",
+ "log",
+ "mime_guess",
+ "percent-encoding",
+ "pin-project-lite",
+ "quinn",
+ "rustls",
+ "rustls-native-certs 0.8.3",
+ "rustls-pki-types",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tokio-rustls",
+ "tokio-util",
+ "tower 0.5.3",
+ "tower-http",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wasm-streams",
+ "web-sys",
+ "webpki-roots",
+]
+
+[[package]]
+name = "reqwest"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "futures-core",
+ "http 1.4.0",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "js-sys",
+ "log",
+ "percent-encoding",
+ "pin-project-lite",
+ "serde",
+ "serde_json",
+ "sync_wrapper",
+ "tokio",
+ "tower 0.5.3",
+ "tower-http",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "reqwest-eventsource"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "632c55746dbb44275691640e7b40c907c16a2dc1a5842aa98aaec90da6ec6bde"
+dependencies = [
+ "eventsource-stream",
+ "futures-core",
+ "futures-timer",
+ "mime",
+ "nom 7.1.3",
+ "pin-project-lite",
+ "reqwest 0.12.28",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "rfc6979"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2"
+dependencies = [
+ "hmac",
+ "subtle",
+]
+
+[[package]]
+name = "rgb"
+version = "0.8.53"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4"
+
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.17",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rkyv"
+version = "0.7.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1"
+dependencies = [
+ "bitvec",
+ "bytecheck",
+ "bytes",
+ "hashbrown 0.12.3",
+ "ptr_meta",
+ "rend",
+ "rkyv_derive",
+ "seahash",
+ "tinyvec",
+ "uuid",
+]
+
+[[package]]
+name = "rkyv_derive"
+version = "0.7.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "room"
+version = "0.2.9"
+dependencies = [
+ "agent",
+ "anyhow",
+ "async-openai",
+ "chrono",
+ "dashmap",
+ "db",
+ "deadpool-redis",
+ "futures",
+ "hostname",
+ "lru",
+ "metrics",
+ "models",
+ "queue",
+ "redis",
+ "regex-lite",
+ "sea-orm",
+ "serde",
+ "serde_json",
+ "session",
+ "slog",
+ "thiserror 2.0.18",
+ "tokio",
+ "tokio-stream",
+ "utoipa",
+ "uuid",
+]
+
+[[package]]
+name = "rpc"
+version = "0.2.9"
+
+[[package]]
+name = "rsa"
+version = "0.9.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d"
+dependencies = [
+ "const-oid 0.9.6",
+ "digest 0.10.7",
+ "num-bigint-dig",
+ "num-integer",
+ "num-traits",
+ "pkcs1",
+ "pkcs8",
+ "rand_core 0.6.4",
+ "sha2 0.10.9",
+ "signature 2.2.0",
+ "spki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "russh"
+version = "0.55.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82b4d036bb45d7bbe99dbfef4ec60eaeb614708d22ff107124272f8ef6b54548"
+dependencies = [
+ "aes 0.8.4",
+ "bitflags",
+ "block-padding",
+ "byteorder",
+ "bytes",
+ "cbc",
+ "ctr 0.9.2",
+ "curve25519-dalek",
+ "data-encoding",
+ "delegate",
+ "der",
+ "digest 0.10.7",
+ "ecdsa",
+ "ed25519-dalek",
+ "elliptic-curve",
+ "enum_dispatch",
+ "flate2",
+ "futures",
+ "generic-array 1.3.5",
+ "getrandom 0.2.17",
+ "hex-literal",
+ "hmac",
+ "home",
+ "inout 0.1.4",
+ "internal-russh-forked-ssh-key",
+ "libcrux-ml-kem",
+ "log",
+ "md5",
+ "num-bigint",
+ "p256",
+ "p384",
+ "p521",
+ "pageant",
+ "pbkdf2",
+ "pkcs5",
+ "pkcs8",
+ "rand 0.8.5",
+ "rand_core 0.6.4",
+ "ring",
+ "russh-cryptovec",
+ "russh-util",
+ "sec1 0.7.3",
+ "sha1",
+ "sha2 0.10.9",
+ "signature 2.2.0",
+ "spki",
+ "ssh-encoding 0.2.0",
+ "subtle",
+ "thiserror 1.0.69",
+ "tokio",
+ "typenum",
+ "yasna",
+ "zeroize",
+]
+
+[[package]]
+name = "russh-cryptovec"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fb0ed583ff0f6b4aa44c7867dd7108df01b30571ee9423e250b4cc939f8c6cf"
+dependencies = [
+ "libc",
+ "log",
+ "nix",
+ "ssh-encoding 0.2.0",
+ "winapi",
+]
+
+[[package]]
+name = "russh-util"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "668424a5dde0bcb45b55ba7de8476b93831b4aa2fa6947e145f3b053e22c60b6"
+dependencies = [
+ "chrono",
+ "tokio",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+]
+
+[[package]]
+name = "rust_decimal"
+version = "1.41.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ce901f9a19d251159075a4c37af514c3b8ef99c22e02dd8c19161cf397ee94a"
+dependencies = [
+ "arrayvec",
+ "borsh",
+ "bytes",
+ "num-traits",
+ "rand 0.8.5",
+ "rkyv",
+ "serde",
+ "serde_json",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "rustc-hash"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
+
+[[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustdct"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b61555105d6a9bf98797c063c362a1d24ed8ab0431655e38f1cf51e52089551"
+dependencies = [
+ "rustfft",
+]
+
+[[package]]
+name = "rustfft"
+version = "6.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21db5f9893e91f41798c88680037dba611ca6674703c1a18601b01a72c8adb89"
+dependencies = [
+ "num-complex",
+ "num-integer",
+ "num-traits",
+ "primal-check",
+ "strength_reduce",
+ "transpose",
+]
+
+[[package]]
+name = "rustix"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
+dependencies = [
+ "log",
+ "once_cell",
+ "ring",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-native-certs"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5"
+dependencies = [
+ "openssl-probe 0.1.6",
+ "rustls-pemfile",
+ "rustls-pki-types",
+ "schannel",
+ "security-framework 2.11.1",
+]
+
+[[package]]
+name = "rustls-native-certs"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63"
+dependencies = [
+ "openssl-probe 0.2.1",
+ "rustls-pki-types",
+ "schannel",
+ "security-framework 3.7.0",
+]
+
+[[package]]
+name = "rustls-pemfile"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd"
+dependencies = [
+ "web-time",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
+name = "ryu"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+
+[[package]]
+name = "safe_arch"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323"
+dependencies = [
+ "bytemuck",
+]
+
+[[package]]
+name = "salsa20"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213"
+dependencies = [
+ "cipher 0.4.4",
+]
+
+[[package]]
+name = "schannel"
+version = "0.1.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "schemars"
+version = "0.8.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615"
+dependencies = [
+ "dyn-clone",
+ "schemars_derive",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "schemars_derive"
+version = "0.8.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "serde_derive_internals",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "scrypt"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f"
+dependencies = [
+ "pbkdf2",
+ "salsa20",
+ "sha2 0.10.9",
+]
+
+[[package]]
+name = "sea-bae"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f694a6ab48f14bc063cfadff30ab551d3c7e46d8f81836c51989d548f44a2a25"
+dependencies = [
+ "heck 0.4.1",
+ "proc-macro-error2",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "sea-orm"
+version = "2.0.0-rc.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b846dc1c7fefbea372c03765ff08307d68894bbad8c73b66176dcd53a3ee131"
+dependencies = [
+ "async-stream",
+ "async-trait",
+ "bigdecimal",
+ "chrono",
+ "derive_more",
+ "futures-util",
+ "itertools",
+ "log",
+ "mac_address",
+ "ouroboros",
+ "pgvector",
+ "rust_decimal",
+ "sea-orm-arrow",
+ "sea-orm-macros",
+ "sea-query",
+ "sea-query-sqlx",
+ "sea-schema",
+ "serde",
+ "serde_json",
+ "sqlx",
+ "strum",
+ "thiserror 2.0.18",
+ "time",
+ "tracing",
+ "url",
+ "uuid",
+]
+
+[[package]]
+name = "sea-orm-arrow"
+version = "2.0.0-rc.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c2eee8405f16c1f337fe3a83389361caea83c928d14dbd666a480407072c365"
+dependencies = [
+ "arrow",
+ "sea-query",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "sea-orm-cli"
+version = "2.0.0-rc.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd9b34d4c8e615079c04eb7863a429c2d2a8bf9c934eb9eeb580f51f36367124"
+dependencies = [
+ "chrono",
+ "clap",
+ "dotenvy",
+ "glob",
+ "indoc",
+ "regex",
+ "tracing",
+ "tracing-subscriber",
+ "url",
+]
+
+[[package]]
+name = "sea-orm-macros"
+version = "2.0.0-rc.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b449fe660e4d365f335222025df97ae01e670ef7ad788b3c67db9183b6cb0474"
+dependencies = [
+ "heck 0.5.0",
+ "itertools",
+ "pluralizer",
+ "proc-macro2",
+ "quote",
+ "sea-bae",
+ "syn 2.0.117",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sea-orm-migration"
+version = "2.0.0-rc.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3ceb928aac8be83332d34d1fdbc827d43696135a800723ffeb2e0b33b7b495e"
+dependencies = [
+ "async-trait",
+ "clap",
+ "dotenvy",
+ "sea-orm",
+ "sea-orm-cli",
+ "sea-schema",
+ "tracing",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "sea-query"
+version = "1.0.0-rc.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58decdaaaf2a698170af2fa1b2e8f7b43a970e7768bf18aebaab113bada46354"
+dependencies = [
+ "chrono",
+ "inherent",
+ "itoa",
+ "ordered-float 4.6.0",
+ "rust_decimal",
+ "sea-query-derive",
+ "serde_json",
+ "time",
+ "uuid",
+]
+
+[[package]]
+name = "sea-query-derive"
+version = "1.0.0-rc.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d88ad44b6ad9788c8b9476b6b91f94c7461d1e19d39cd8ea37838b1e6ff5aa8"
+dependencies = [
+ "darling",
+ "heck 0.4.1",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "sea-query-sqlx"
+version = "0.8.0-rc.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4377164b09a11bb692dec6966eb0e6908d63d768defef0be689b39e02cf8544"
+dependencies = [
+ "sea-query",
+ "sqlx",
+]
+
+[[package]]
+name = "sea-schema"
+version = "0.17.0-rc.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b363dd21c20fe4d1488819cb2bc7f8d4696c62dd9f39554f97639f54d57dd0ab"
+dependencies = [
+ "async-trait",
+ "sea-query",
+ "sea-query-sqlx",
+ "sea-schema-derive",
+ "sqlx",
+]
+
+[[package]]
+name = "sea-schema-derive"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "debdc8729c37fdbf88472f97fd470393089f997a909e535ff67c544d18cfccf0"
+dependencies = [
+ "heck 0.4.1",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "seahash"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
+
+[[package]]
+name = "sec1"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
+dependencies = [
+ "base16ct 0.2.0",
+ "der",
+ "generic-array 0.14.7",
+ "pkcs8",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "sec1"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d"
+dependencies = [
+ "base16ct 1.0.0",
+ "hybrid-array",
+]
+
+[[package]]
+name = "secrecy"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a"
+dependencies = [
+ "serde",
+ "zeroize",
+]
+
+[[package]]
+name = "security-framework"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
+dependencies = [
+ "bitflags",
+ "core-foundation 0.9.4",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework"
+version = "3.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
+dependencies = [
+ "bitflags",
+ "core-foundation 0.10.1",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde-value"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c"
+dependencies = [
+ "ordered-float 2.10.1",
+ "serde",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "serde_derive_internals"
+version = "0.29.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.149"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_yaml"
+version = "0.9.34+deprecated"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
+dependencies = [
+ "indexmap 2.13.0",
+ "itoa",
+ "ryu",
+ "serde",
+ "unsafe-libyaml",
+]
+
+[[package]]
+name = "service"
+version = "0.2.9"
+dependencies = [
+ "agent",
+ "anyhow",
+ "argon2",
+ "async-openai",
+ "avatar",
+ "base64 0.22.1",
+ "captcha-rs",
+ "chrono",
+ "config",
+ "db",
+ "deadpool-redis",
+ "email",
+ "futures",
+ "git",
+ "git2",
+ "hex",
+ "hmac",
+ "models",
+ "moka",
+ "queue",
+ "rand 0.10.0",
+ "redis",
+ "reqwest 0.13.2",
+ "room",
+ "rsa",
+ "rust_decimal",
+ "sea-orm",
+ "serde",
+ "serde_json",
+ "session",
+ "sha1",
+ "sha2 0.11.0",
+ "slog",
+ "tokio",
+ "tokio-stream",
+ "tracing",
+ "utoipa",
+ "uuid",
+]
+
+[[package]]
+name = "session"
+version = "0.2.9"
+dependencies = [
+ "actix-service",
+ "actix-utils",
+ "actix-web",
+ "anyhow",
+ "deadpool-redis",
+ "derive_more",
+ "rand 0.10.0",
+ "redis",
+ "serde",
+ "serde_json",
+ "tokio",
+ "uuid",
+]
+
+[[package]]
+name = "sha1"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.2.17",
+ "digest 0.10.7",
+]
+
+[[package]]
+name = "sha1_smol"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d"
+
+[[package]]
+name = "sha2"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.2.17",
+ "digest 0.10.7",
+]
+
+[[package]]
+name = "sha2"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.3.0",
+ "digest 0.11.2",
+]
+
+[[package]]
+name = "sharded-slab"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "shellexpand"
+version = "3.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32824fab5e16e6c4d86dc1ba84489390419a39f97699852b66480bb87d297ed8"
+dependencies = [
+ "dirs",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
+dependencies = [
+ "errno",
+ "libc",
+]
+
+[[package]]
+name = "signature"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
+dependencies = [
+ "digest 0.10.7",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "signature"
+version = "3.0.0-rc.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f1880df446116126965eeec169136b2e0251dba37c6223bcc819569550edea3"
+
+[[package]]
+name = "simba"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c99284beb21666094ba2b75bbceda012e610f5479dfcc2d6e2426f53197ffd95"
+dependencies = [
+ "approx",
+ "num-complex",
+ "num-traits",
+ "paste",
+ "wide",
+]
+
+[[package]]
+name = "simd-adler32"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
+
+[[package]]
+name = "simd_helpers"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6"
+dependencies = [
+ "quote",
+]
+
+[[package]]
+name = "simdutf8"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
+
+[[package]]
+name = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
+[[package]]
+name = "slog"
+version = "2.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b3b8565691b22d2bdfc066426ed48f837fc0c5f2c8cad8d9718f7f99d6995c1"
+dependencies = [
+ "anyhow",
+ "erased-serde",
+ "rustversion",
+ "serde_core",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "socket2"
+version = "0.5.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "socket2"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "spin"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+dependencies = [
+ "lock_api",
+]
+
+[[package]]
+name = "spki"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
+dependencies = [
+ "base64ct",
+ "der",
+]
+
+[[package]]
+name = "sqlx"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc"
+dependencies = [
+ "sqlx-core",
+ "sqlx-macros",
+ "sqlx-mysql",
+ "sqlx-postgres",
+ "sqlx-sqlite",
+]
+
+[[package]]
+name = "sqlx-core"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6"
+dependencies = [
+ "base64 0.22.1",
+ "bytes",
+ "chrono",
+ "crc",
+ "crossbeam-queue",
+ "either",
+ "event-listener",
+ "futures-core",
+ "futures-intrusive",
+ "futures-io",
+ "futures-util",
+ "hashbrown 0.15.5",
+ "hashlink",
+ "indexmap 2.13.0",
+ "log",
+ "memchr",
+ "once_cell",
+ "percent-encoding",
+ "rust_decimal",
+ "serde",
+ "serde_json",
+ "sha2 0.10.9",
+ "smallvec",
+ "thiserror 2.0.18",
+ "time",
+ "tokio",
+ "tokio-stream",
+ "tracing",
+ "url",
+ "uuid",
+]
+
+[[package]]
+name = "sqlx-macros"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "sqlx-core",
+ "sqlx-macros-core",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "sqlx-macros-core"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b"
+dependencies = [
+ "dotenvy",
+ "either",
+ "heck 0.5.0",
+ "hex",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "serde",
+ "serde_json",
+ "sha2 0.10.9",
+ "sqlx-core",
+ "sqlx-mysql",
+ "sqlx-postgres",
+ "sqlx-sqlite",
+ "syn 2.0.117",
+ "tokio",
+ "url",
+]
+
+[[package]]
+name = "sqlx-mysql"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526"
+dependencies = [
+ "atoi",
+ "base64 0.22.1",
+ "bitflags",
+ "byteorder",
+ "bytes",
+ "chrono",
+ "crc",
+ "digest 0.10.7",
+ "dotenvy",
+ "either",
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-util",
+ "generic-array 0.14.7",
+ "hex",
+ "hkdf",
+ "hmac",
+ "itoa",
+ "log",
+ "md-5",
+ "memchr",
+ "once_cell",
+ "percent-encoding",
+ "rand 0.8.5",
+ "rsa",
+ "rust_decimal",
+ "serde",
+ "sha1",
+ "sha2 0.10.9",
+ "smallvec",
+ "sqlx-core",
+ "stringprep",
+ "thiserror 2.0.18",
+ "time",
+ "tracing",
+ "uuid",
+ "whoami",
+]
+
+[[package]]
+name = "sqlx-postgres"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46"
+dependencies = [
+ "atoi",
+ "base64 0.22.1",
+ "bitflags",
+ "byteorder",
+ "chrono",
+ "crc",
+ "dotenvy",
+ "etcetera",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "hex",
+ "hkdf",
+ "hmac",
+ "home",
+ "itoa",
+ "log",
+ "md-5",
+ "memchr",
+ "once_cell",
+ "rand 0.8.5",
+ "rust_decimal",
+ "serde",
+ "serde_json",
+ "sha2 0.10.9",
+ "smallvec",
+ "sqlx-core",
+ "stringprep",
+ "thiserror 2.0.18",
+ "time",
+ "tracing",
+ "uuid",
+ "whoami",
+]
+
+[[package]]
+name = "sqlx-sqlite"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea"
+dependencies = [
+ "atoi",
+ "chrono",
+ "flume",
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-intrusive",
+ "futures-util",
+ "libsqlite3-sys",
+ "log",
+ "percent-encoding",
+ "serde",
+ "serde_urlencoded",
+ "sqlx-core",
+ "thiserror 2.0.18",
+ "time",
+ "tracing",
+ "url",
+ "uuid",
+]
+
+[[package]]
+name = "ssh-cipher"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "caac132742f0d33c3af65bfcde7f6aa8f62f0e991d80db99149eb9d44708784f"
+dependencies = [
+ "aes 0.8.4",
+ "aes-gcm 0.10.3",
+ "cbc",
+ "chacha20 0.9.1",
+ "cipher 0.4.4",
+ "ctr 0.9.2",
+ "poly1305 0.8.0",
+ "ssh-encoding 0.2.0",
+ "subtle",
+]
+
+[[package]]
+name = "ssh-cipher"
+version = "0.3.0-rc.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20540e2cbcf285a8e0172717b3ae77ccc2bbf63f3967263ea71e8048173b09ff"
+dependencies = [
+ "aes 0.9.0-rc.4",
+ "aes-gcm 0.11.0-rc.3",
+ "chacha20 0.10.0",
+ "cipher 0.5.1",
+ "des",
+ "poly1305 0.9.0-rc.6",
+ "ssh-encoding 0.3.0-rc.8",
+ "zeroize",
+]
+
+[[package]]
+name = "ssh-encoding"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb9242b9ef4108a78e8cd1a2c98e193ef372437f8c22be363075233321dd4a15"
+dependencies = [
+ "base64ct",
+ "bytes",
+ "pem-rfc7468 0.7.0",
+ "sha2 0.10.9",
+]
+
+[[package]]
+name = "ssh-encoding"
+version = "0.3.0-rc.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af0ddb05d9c6034911bbdc541170b3068a2c019c7a10824e92921151563fb5af"
+dependencies = [
+ "base64ct",
+ "digest 0.11.2",
+ "pem-rfc7468 1.0.0",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "ssh-key"
+version = "0.7.0-rc.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ae7221717f89c8629a83ba265a004cb864df267485656f790444fff1b69fa36"
+dependencies = [
+ "rand_core 0.10.0",
+ "sec1 0.8.1",
+ "sha2 0.11.0",
+ "signature 3.0.0-rc.10",
+ "ssh-cipher 0.3.0-rc.8",
+ "ssh-encoding 0.3.0-rc.8",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
+name = "strength_reduce"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82"
+
+[[package]]
+name = "stringprep"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+ "unicode-properties",
+]
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "strum"
+version = "0.27.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sync_wrapper"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "sysinfo"
+version = "0.38.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92ab6a2f8bfe508deb3c6406578252e491d299cbbf3bc0529ecc3313aee4a52f"
+dependencies = [
+ "libc",
+ "memchr",
+ "ntapi",
+ "objc2-core-foundation",
+ "objc2-io-kit",
+ "windows",
+]
+
+[[package]]
+name = "tagptr"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
+
+[[package]]
+name = "tap"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
+
+[[package]]
+name = "tar"
+version = "0.4.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973"
+dependencies = [
+ "filetime",
+ "libc",
+ "xattr",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
+dependencies = [
+ "fastrand",
+ "getrandom 0.4.2",
+ "once_cell",
+ "rustix",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl 1.0.69",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+dependencies = [
+ "thiserror-impl 2.0.18",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "tiff"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52"
+dependencies = [
+ "fax",
+ "flate2",
+ "half",
+ "quick-error",
+ "weezl",
+ "zune-jpeg",
+]
+
+[[package]]
+name = "tiktoken-rs"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a19830747d9034cd9da43a60eaa8e552dfda7712424aebf187b7a60126bae0d"
+dependencies = [
+ "anyhow",
+ "base64 0.22.1",
+ "bstr",
+ "fancy-regex",
+ "lazy_static",
+ "regex",
+ "rustc-hash 1.1.0",
+]
+
+[[package]]
+name = "time"
+version = "0.3.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
+dependencies = [
+ "deranged",
+ "itoa",
+ "js-sys",
+ "num-conv",
+ "powerfmt",
+ "serde_core",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
+
+[[package]]
+name = "time-macros"
+version = "0.2.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
+name = "tiny-keccak"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
+dependencies = [
+ "crunchy",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "tls_codec"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0de2e01245e2bb89d6f05801c564fa27624dbd7b1846859876c7dad82e90bf6b"
+dependencies = [
+ "tls_codec_derive",
+ "zeroize",
+]
+
+[[package]]
+name = "tls_codec_derive"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "tokio"
+version = "1.50.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d"
+dependencies = [
+ "bytes",
+ "libc",
+ "mio",
+ "parking_lot",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "socket2 0.6.3",
+ "tokio-macros",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.26.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
+dependencies = [
+ "rustls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-stream"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+ "tokio",
+ "tokio-util",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "pin-project-lite",
+ "slab",
+ "tokio",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "1.1.0+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97251a7c317e03ad83774a8752a7e81fb6067740609f75ea2b585b569a59198f"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.25.8+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16bff38f1d86c47f9ff0647e6838d7bb362522bdf44006c7068c2b1e606f1f3c"
+dependencies = [
+ "indexmap 2.13.0",
+ "toml_datetime",
+ "toml_parser",
+ "winnow",
+]
+
+[[package]]
+name = "toml_parser"
+version = "1.1.0+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011"
+dependencies = [
+ "winnow",
+]
+
+[[package]]
+name = "tonic"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52"
+dependencies = [
+ "async-stream",
+ "async-trait",
+ "axum",
+ "base64 0.22.1",
+ "bytes",
+ "flate2",
+ "h2 0.4.13",
+ "http 1.4.0",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-timeout",
+ "hyper-util",
+ "percent-encoding",
+ "pin-project",
+ "prost",
+ "rustls-native-certs 0.8.3",
+ "rustls-pemfile",
+ "socket2 0.5.10",
+ "tokio",
+ "tokio-rustls",
+ "tokio-stream",
+ "tower 0.4.13",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "indexmap 1.9.3",
+ "pin-project",
+ "pin-project-lite",
+ "rand 0.8.5",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tokio",
+ "tokio-util",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower-http"
+version = "0.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
+dependencies = [
+ "base64 0.22.1",
+ "bitflags",
+ "bytes",
+ "futures-util",
+ "http 1.4.0",
+ "http-body",
+ "iri-string",
+ "mime",
+ "pin-project-lite",
+ "tower 0.5.3",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+
+[[package]]
+name = "tower-service"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
+[[package]]
+name = "tracing"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+dependencies = [
+ "log",
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+dependencies = [
+ "once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-serde"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1"
+dependencies = [
+ "serde",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
+dependencies = [
+ "matchers",
+ "nu-ansi-term",
+ "once_cell",
+ "regex-automata",
+ "serde",
+ "serde_json",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing",
+ "tracing-core",
+ "tracing-log",
+ "tracing-serde",
+]
+
+[[package]]
+name = "transport"
+version = "0.2.9"
+
+[[package]]
+name = "transpose"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e"
+dependencies = [
+ "num-integer",
+ "strength_reduce",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
+[[package]]
+name = "ttf-parser"
+version = "0.25.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31"
+
+[[package]]
+name = "typed-path"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e"
+
+[[package]]
+name = "typenum"
+version = "1.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
+
+[[package]]
+name = "ucd-trie"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
+
+[[package]]
+name = "unicase"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "unicode-properties"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+
+[[package]]
+name = "universal-hash"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
+dependencies = [
+ "crypto-common 0.1.7",
+ "subtle",
+]
+
+[[package]]
+name = "universal-hash"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4987bdc12753382e0bec4a65c50738ffaabc998b9cdd1f952fb5f39b0048a96"
+dependencies = [
+ "crypto-common 0.2.1",
+ "ctutils",
+]
+
+[[package]]
+name = "unsafe-libyaml"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
+
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
+name = "url"
+version = "2.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+]
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "utoipa"
+version = "5.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fcc29c80c21c31608227e0912b2d7fddba57ad76b606890627ba8ee7964e993"
+dependencies = [
+ "indexmap 2.13.0",
+ "serde",
+ "serde_json",
+ "utoipa-gen",
+]
+
+[[package]]
+name = "utoipa-gen"
+version = "5.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d79d08d92ab8af4c5e8a6da20c47ae3f61a0f1dabc1997cdf2d082b757ca08b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "regex",
+ "syn 2.0.117",
+ "uuid",
+]
+
+[[package]]
+name = "uuid"
+version = "1.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9"
+dependencies = [
+ "getrandom 0.4.2",
+ "js-sys",
+ "serde_core",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "v_frame"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2"
+dependencies = [
+ "aligned-vec",
+ "num-traits",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "valuable"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasip2"
+version = "1.0.2+wasi-0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wasip3"
+version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
+name = "wasite"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.115"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6523d69017b7633e396a89c5efab138161ed5aafcbc8d3e5c5a42ae38f50495a"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "serde",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.65"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d1faf851e778dfa54db7cd438b70758eba9755cb47403f3496edd7c8fc212f0"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.115"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e3a6c758eb2f701ed3d052ff5737f5bfe6614326ea7f3bbac7156192dc32e67"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.115"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "921de2737904886b52bcbb237301552d05969a6f9c40d261eb0533c8b055fedf"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.115"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a93e946af942b58934c604527337bad9ae33ba1d5c6900bbb41c2c07c2364a93"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "wasm-encoder"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
+dependencies = [
+ "leb128fmt",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasm-metadata"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
+dependencies = [
+ "anyhow",
+ "indexmap 2.13.0",
+ "wasm-encoder",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasm-streams"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
+dependencies = [
+ "futures-util",
+ "js-sys",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "wasmparser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
+dependencies = [
+ "bitflags",
+ "hashbrown 0.15.5",
+ "indexmap 2.13.0",
+ "semver",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84cde8507f4d7cfcb1185b8cb5890c494ffea65edbe1ba82cfd63661c805ed94"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "web-time"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "webhook"
+version = "0.2.9"
+
+[[package]]
+name = "webpki-roots"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "weezl"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
+
+[[package]]
+name = "which"
+version = "8.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81995fafaaaf6ae47a7d0cc83c67caf92aeb7e5331650ae6ff856f7c0c60c459"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "whoami"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d"
+dependencies = [
+ "libredox",
+ "wasite",
+]
+
+[[package]]
+name = "wide"
+version = "0.7.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03"
+dependencies = [
+ "bytemuck",
+ "safe_arch",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows"
+version = "0.62.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
+dependencies = [
+ "windows-collections",
+ "windows-core",
+ "windows-future",
+ "windows-numerics",
+]
+
+[[package]]
+name = "windows-collections"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
+dependencies = [
+ "windows-core",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.62.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-link",
+ "windows-result",
+ "windows-strings",
+]
+
+[[package]]
+name = "windows-future"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
+dependencies = [
+ "windows-core",
+ "windows-link",
+ "windows-threading",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.59.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-numerics"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
+dependencies = [
+ "windows-core",
+ "windows-link",
+]
+
+[[package]]
+name = "windows-result"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
+dependencies = [
+ "windows-targets 0.53.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm 0.52.6",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.53.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
+dependencies = [
+ "windows-link",
+ "windows_aarch64_gnullvm 0.53.1",
+ "windows_aarch64_msvc 0.53.1",
+ "windows_i686_gnu 0.53.1",
+ "windows_i686_gnullvm 0.53.1",
+ "windows_i686_msvc 0.53.1",
+ "windows_x86_64_gnu 0.53.1",
+ "windows_x86_64_gnullvm 0.53.1",
+ "windows_x86_64_msvc 0.53.1",
+]
+
+[[package]]
+name = "windows-threading"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
+
+[[package]]
+name = "winnow"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
+dependencies = [
+ "wit-bindgen-rust-macro",
+]
+
+[[package]]
+name = "wit-bindgen-core"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
+dependencies = [
+ "anyhow",
+ "heck 0.5.0",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-bindgen-rust"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
+dependencies = [
+ "anyhow",
+ "heck 0.5.0",
+ "indexmap 2.13.0",
+ "prettyplease",
+ "syn 2.0.117",
+ "wasm-metadata",
+ "wit-bindgen-core",
+ "wit-component",
+]
+
+[[package]]
+name = "wit-bindgen-rust-macro"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
+dependencies = [
+ "anyhow",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+ "wit-bindgen-core",
+ "wit-bindgen-rust",
+]
+
+[[package]]
+name = "wit-component"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
+dependencies = [
+ "anyhow",
+ "bitflags",
+ "indexmap 2.13.0",
+ "log",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "wasm-encoder",
+ "wasm-metadata",
+ "wasmparser",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-parser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
+dependencies = [
+ "anyhow",
+ "id-arena",
+ "indexmap 2.13.0",
+ "log",
+ "semver",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "unicode-xid",
+ "wasmparser",
+]
+
+[[package]]
+name = "writeable"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
+
+[[package]]
+name = "wyz"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
+dependencies = [
+ "tap",
+]
+
+[[package]]
+name = "xattr"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
+dependencies = [
+ "libc",
+ "rustix",
+]
+
+[[package]]
+name = "xxhash-rust"
+version = "0.8.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3"
+
+[[package]]
+name = "y4m"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448"
+
+[[package]]
+name = "yansi"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
+
+[[package]]
+name = "yasna"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd"
+dependencies = [
+ "bit-vec",
+ "num-bigint",
+]
+
+[[package]]
+name = "yoke"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954"
+dependencies = [
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+ "synstructure",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.48"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.48"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+ "synstructure",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
+dependencies = [
+ "zeroize_derive",
+]
+
+[[package]]
+name = "zeroize_derive"
+version = "1.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "zerotrie"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
+[[package]]
+name = "zip"
+version = "8.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7756d0206d058333667493c4014f545f4b9603c4330ccd6d9b3f86dcab59f7d9"
+dependencies = [
+ "aes 0.8.4",
+ "bzip2",
+ "constant_time_eq",
+ "crc32fast",
+ "deflate64",
+ "flate2",
+ "getrandom 0.4.2",
+ "hmac",
+ "indexmap 2.13.0",
+ "lzma-rust2",
+ "memchr",
+ "pbkdf2",
+ "ppmd-rust",
+ "sha1",
+ "time",
+ "typed-path",
+ "zeroize",
+ "zopfli",
+ "zstd",
+]
+
+[[package]]
+name = "zlib-rs"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513"
+
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+
+[[package]]
+name = "zopfli"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
+dependencies = [
+ "bumpalo",
+ "crc32fast",
+ "log",
+ "simd-adler32",
+]
+
+[[package]]
+name = "zstd"
+version = "0.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
+dependencies = [
+ "zstd-safe",
+]
+
+[[package]]
+name = "zstd-safe"
+version = "7.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
+dependencies = [
+ "zstd-sys",
+]
+
+[[package]]
+name = "zstd-sys"
+version = "2.0.16+zstd.1.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
+dependencies = [
+ "cc",
+ "pkg-config",
+]
+
+[[package]]
+name = "zune-core"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9"
+
+[[package]]
+name = "zune-inflate"
+version = "0.2.54"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02"
+dependencies = [
+ "simd-adler32",
+]
+
+[[package]]
+name = "zune-jpeg"
+version = "0.5.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296"
+dependencies = [
+ "zune-core",
+]
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..3eb6ae7
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,185 @@
+[workspace]
+members = [
+ "libs/models",
+ "libs/session",
+ "libs/git",
+ "libs/email",
+ "libs/queue",
+ "libs/room",
+ "libs/config",
+ "libs/service",
+ "libs/db",
+ "libs/api",
+ "libs/webhook",
+ "libs/transport",
+ "libs/rpc",
+ "libs/avatar",
+ "libs/agent",
+ "libs/migrate",
+ "libs/agent-tool-derive",
+ "apps/migrate",
+ "apps/app",
+ "apps/git-hook",
+ "apps/gitserver",
+ "apps/email",
+ "apps/operator",
+]
+
+resolver = "3"
+
+[workspace.dependencies]
+models = { path = "libs/models" }
+session = { path = "libs/session" }
+git = { path = "libs/git" }
+email = { path = "libs/email" }
+queue = { path = "libs/queue" }
+room = { path = "libs/room" }
+config = { path = "libs/config" }
+service = { path = "libs/service" }
+db = { path = "libs/db" }
+api = { path = "libs/api" }
+agent = { path = "libs/agent" }
+webhook = { path = "libs/webhook" }
+rpc = { path = "libs/rpc" }
+avatar = { path = "libs/avatar" }
+migrate = { path = "libs/migrate" }
+
+sea-query = "1.0.0-rc.31"
+
+actix-web = "4.13.0"
+actix-files = "0.6.10"
+actix-cors = "0.7.1"
+actix-session = "0.11.0"
+actix-ws = "0.4.0"
+actix-multipart = "0.7.2"
+actix-analytics = "1.2.1"
+actix-jwt-session = "1.0.7"
+actix-csrf = "0.8.0"
+actix-rt = "2.11.0"
+actix = "0.13"
+async-stream = "0.3"
+async-nats = "0.47.0"
+actix-service = "2.0.3"
+actix-utils = "3.0.1"
+redis = "1.1.0"
+anyhow = "1.0.102"
+derive_more = "2.1.1"
+blake3 = "1.8.3"
+argon2 = "0.5.3"
+thiserror = "2.0.18"
+password-hash = "0.6.0"
+awc = "3.8.2"
+bstr = "1.12.1"
+captcha-rs = "0.5.0"
+deadpool-redis = "0.23.0"
+deadpool = "0.13.0"
+dotenv = "0.15.0"
+env_logger = "0.11.10"
+flate2 = "1.1.9"
+git2 = "0.20.4"
+slog = "2.8.2"
+git2-ext = "1.0.0"
+git2-hooks = "0.7.0"
+futures = "0.3.32"
+futures-util = "0.3.32"
+globset = "0.4.18"
+hex = "0.4.3"
+lettre = { version = "0.11.19", default-features = false, features = ["tokio1-rustls-tls", "smtp-transport", "builder", "pool"] }
+kube = { version = "0.98", features = ["derive", "runtime"] }
+k8s-openapi = { version = "0.24", default-features = false, features = ["v1_28", "schemars"] }
+mime = "0.3.17"
+mime_guess2 = "2.3.1"
+opentelemetry = "0.31.0"
+opentelemetry-otlp = "0.31.0"
+opentelemetry_sdk = "0.31.0"
+opentelemetry-http = "0.31.0"
+prost = "0.14.3"
+prost-build = "0.14.3"
+qdrant-client = "1.17.0"
+rand = "0.10.0"
+russh = { version = "0.55.0", default-features = false }
+hmac = { version = "0.12.1", features = ["std"] }
+sha1_smol = "1.0.1"
+rsa = { version = "0.9.7", package = "rsa" }
+reqwest = { version = "0.13.2", default-features = false }
+dotenvy = "0.15.7"
+aws-sdk-s3 = "1.127.0"
+sea-orm = "2.0.0-rc.37"
+sea-orm-migration = "2.0.0-rc.37"
+sha1 = { version = "0.10.6", features = ["compress"] }
+sha2 = "0.11.0-rc.5"
+sysinfo = "0.38.4"
+ssh-key = "0.7.0-rc.9"
+tar = "0.4.45"
+zip = "8.3.1"
+tokenizer = "0.1.2"
+tiktoken-rs = "0.9.1"
+regex = "1.12.3"
+jsonwebtoken = "10.3.0"
+once_cell = "1.21.4"
+async-trait = "0.1.89"
+fs2 = "0.4.3"
+image = "0.25.10"
+tokio = "1.50.0"
+tokio-util = "0.7.18"
+tokio-stream = "0.1.18"
+url = "2.5.8"
+num_cpus = "1.17.0"
+clap = "4.6.0"
+time = "0.3.47"
+chrono = "0.4.44"
+tracing = "0.1.44"
+tracing-subscriber = "0.3.23"
+tracing-opentelemetry = "0.32.1"
+tonic = "0.14.5"
+tonic-build = "0.14.5"
+uuid = "1.22.0"
+async-openai = { version = "0.34.0", features = ["embedding", "chat-completion"] }
+hostname = "0.4"
+utoipa = { version = "5.4.0", features = ["chrono", "uuid"] }
+rust_decimal = "1.40.0"
+walkdir = "2.5.0"
+moka = "0.12.15"
+serde = "1.0.228"
+serde_json = "1.0.149"
+serde_yaml = "0.9.33"
+serde_bytes = "0.11.19"
+base64 = "0.22.1"
+
+
+
+
+[workspace.package]
+version = "0.2.9"
+edition = "2024"
+authors = []
+description = ""
+repository = ""
+readme = ""
+homepage = ""
+license = ""
+keywords = []
+categories = []
+documentation = ""
+
+[workspace.lints.rust]
+unsafe_code = "warn"
+
+[workspace.lints.clippy]
+unwrap_used = "warn"
+expect_used = "warn"
+
+[profile.dev]
+debug = 1
+incremental = true
+codegen-units = 256
+
+[profile.release]
+lto = "thin"
+codegen-units = 1
+strip = true
+opt-level = 3
+
+
+[profile.dev.package.num-bigint-dig]
+opt-level = 3
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..6cf9b6f
--- /dev/null
+++ b/README.md
@@ -0,0 +1,263 @@
+# Code API
+
+> 一个现代化的代码协作与团队沟通平台,融合 GitHub 的代码管理体验与 Slack 的实时沟通功能。
+
+## 项目概述
+
+Code API 是一个全栈 monorepo 项目,采用 Rust 后端 + React 前端的技术栈。项目实现了类似 GitHub 的 Issue 追踪、Pull Request 代码审查、Git 仓库管理,以及类似 Slack 的实时聊天 Room 功能。
+
+### 核心功能
+
+- **代码仓库管理** — Git 仓库浏览、分支管理、文件操作
+- **Issue 追踪** — 创建、分配、标签、评论 Issue
+- **Pull Request** — 代码审查、Inline Comment、CI 状态检查
+- **实时聊天 (Room)** — 团队频道、消息回复、Thread 讨论
+- **通知系统** — 邮件通知、Webhook 集成
+- **用户系统** — 认证、会话管理、权限控制
+
+## 技术栈
+
+### 后端 (Rust)
+
+| 类别 | 技术 |
+|------|------|
+| 语言 | Rust 2024 Edition |
+| Web 框架 | Actix-web |
+| ORM | SeaORM |
+| 数据库 | PostgreSQL |
+| 缓存 | Redis |
+| 实时通信 | WebSocket (actix-ws) |
+| 消息队列 | NATS |
+| 向量数据库 | Qdrant |
+| Git 操作 | git2 / git2-ext |
+| 认证 | JWT + Session |
+| API 文档 | utoipa (OpenAPI) |
+
+### 前端 (TypeScript/React)
+
+| 类别 | 技术 |
+|------|------|
+| 语言 | TypeScript 5.9 |
+| 框架 | React 19 |
+| 路由 | React Router v7 |
+| 构建工具 | Vite 8 + SWC |
+| UI 组件 | shadcn/ui + Tailwind CSS 4 |
+| 状态管理 | TanStack Query |
+| HTTP 客户端 | Axios + OpenAPI 生成 |
+| Markdown | react-markdown + Shiki |
+| 拖拽 | dnd-kit |
+
+## 项目结构
+
+```
+code/
+├── apps/ # 应用程序入口
+│ ├── app/ # 主 Web 应用
+│ ├── gitserver/ # Git HTTP/SSH 服务器
+│ ├── git-hook/ # Git Hook 处理服务
+│ ├── email/ # 邮件发送服务
+│ ├── migrate/ # 数据库迁移工具
+│ └── operator/ # Kubernetes 操作器
+├── libs/ # 共享库
+│ ├── api/ # REST API 路由与处理器
+│ ├── models/ # 数据库模型 (SeaORM)
+│ ├── service/ # 业务逻辑层
+│ ├── db/ # 数据库连接池
+│ ├── config/ # 配置管理
+│ ├── session/ # 会话管理
+│ ├── git/ # Git 操作封装
+│ ├── room/ # 实时聊天服务
+│ ├── queue/ # 消息队列
+│ ├── webhook/ # Webhook 处理
+│ ├── rpc/ # RPC 服务 (gRPC/Tonic)
+│ ├── email/ # 邮件发送
+│ ├── agent/ # AI Agent 集成
+│ ├── avatar/ # 头像处理
+│ ├── transport/ # 传输层
+│ └── migrate/ # 迁移脚本
+├── src/ # 前端源代码
+│ ├── app/ # 页面路由组件
+│ ├── components/ # 可复用组件
+│ ├── contexts/ # React Context
+│ ├── client/ # API 客户端 (OpenAPI 生成)
+│ ├── hooks/ # 自定义 Hooks
+│ └── lib/ # 工具函数
+├── docker/ # Docker 配置
+├── scripts/ # 构建脚本
+├── openapi.json # OpenAPI 规范文件
+└── Cargo.toml # Rust Workspace 配置
+```
+
+## 快速开始
+
+### 环境要求
+
+- **Rust**: 最新稳定版 (Edition 2024)
+- **Node.js**: >= 20
+- **pnpm**: >= 10
+- **PostgreSQL**: >= 14
+- **Redis**: >= 6
+
+### 安装步骤
+
+1. **克隆仓库**
+ ```bash
+ git clone
+ cd code
+ ```
+
+2. **配置环境变量**
+ ```bash
+ cp .env.example .env
+ # 编辑 .env 文件,配置数据库连接等信息
+ ```
+
+3. **启动数据库与 Redis**
+ ```bash
+ # 使用 Docker 启动(推荐)
+ docker compose -f docker/docker-compose.yml up -d
+ ```
+
+4. **数据库迁移**
+ ```bash
+ cargo run -p migrate
+ ```
+
+5. **启动后端服务**
+ ```bash
+ cargo run -p app
+ ```
+
+6. **启动前端开发服务器**
+ ```bash
+ pnpm install
+ pnpm dev
+ ```
+
+7. **访问应用**
+ - 前端: http://localhost:5173
+ - 后端 API: http://localhost:8080
+
+## 开发指南
+
+### 后端开发
+
+```bash
+# 运行所有测试
+cargo test
+
+# 运行特定模块测试
+cargo test -p service
+
+# 检查代码质量
+cargo clippy --workspace
+
+# 格式化代码
+cargo fmt --workspace
+
+# 生成 OpenAPI 文档
+pnpm openapi:gen-json
+```
+
+### 前端开发
+
+```bash
+# 安装依赖
+pnpm install
+
+# 启动开发服务器
+pnpm dev
+
+# 构建生产版本
+pnpm build
+
+# 代码检查
+pnpm lint
+
+# 生成 OpenAPI 客户端
+pnpm openapi:gen
+```
+
+### 数据库迁移
+
+```bash
+# 创建新迁移
+cd libs/migrate && cargo run -- create
+
+# 执行迁移
+cargo run -p migrate
+```
+
+## 配置说明
+
+### 必需配置项
+
+| 变量名 | 说明 | 示例 |
+|--------|------|------|
+| `APP_DATABASE_URL` | PostgreSQL 连接 | `postgresql://user:pass@localhost/db` |
+| `APP_REDIS_URL` | Redis 连接 | `redis://localhost:6379` |
+| `APP_AI_API_KEY` | AI 服务 API Key | `sk-xxxxx` |
+| `APP_SMTP_*` | SMTP 邮件配置 | 见 `.env.example` |
+
+### 可选配置项
+
+| 变量名 | 默认值 | 说明 |
+|--------|--------|------|
+| `APP_DATABASE_MAX_CONNECTIONS` | 10 | 数据库连接池大小 |
+| `APP_LOG_LEVEL` | info | 日志级别 |
+| `APP_QDRANT_URL` | - | 向量数据库地址 |
+| `APP_REPOS_ROOT` | /data/repos | Git 仓库存储路径 |
+
+完整配置请参考 `.env.example`。
+
+## API 文档
+
+启动服务后访问 http://localhost:8080/swagger-ui 查看完整的 API 文档。
+
+## 架构设计
+
+### 后端分层架构
+
+```
+┌─────────────────────────────────────┐
+│ apps/app │ ← 应用入口
+├─────────────────────────────────────┤
+│ libs/api │ ← HTTP 路由/Handler
+├─────────────────────────────────────┤
+│ libs/service │ ← 业务逻辑层
+├─────────────────────────────────────┤
+│ libs/models │ libs/db │ libs/git│ ← 数据访问层
+├─────────────────────────────────────┤
+│ PostgreSQL │ Redis │ Qdrant │ ← 存储层
+└─────────────────────────────────────┘
+```
+
+### 前端目录结构
+
+```
+src/
+├── app/ # 页面级组件 (按功能模块组织)
+│ ├── project/ # 项目相关页面 (Issue、Settings)
+│ ├── repository/ # 仓库相关页面 (PR、代码浏览)
+│ └── settings/ # 用户设置
+├── components/ # 可复用组件
+│ ├── ui/ # 基础 UI 组件 (shadcn)
+│ ├── project/ # 项目相关组件
+│ ├── repository/ # 仓库相关组件
+│ └── room/ # 聊天相关组件
+├── contexts/ # React Context (用户、聊天室等)
+├── client/ # OpenAPI 生成的客户端
+└── lib/ # 工具函数与 Hooks
+```
+
+## 任务清单
+
+项目当前开发任务详见 [task.md](./task.md),按优先级分为:
+
+- **P0** — 阻塞性问题(核心流程不通)
+- **P1** — 核心体验(关键功能)
+- **P2** — 体验优化(增强功能)
+
+## 许可证
+
+[待添加]
diff --git a/apps/app/Cargo.toml b/apps/app/Cargo.toml
new file mode 100644
index 0000000..83a12ec
--- /dev/null
+++ b/apps/app/Cargo.toml
@@ -0,0 +1,34 @@
+[package]
+name = "app"
+version.workspace = true
+edition.workspace = true
+authors.workspace = true
+description.workspace = true
+repository.workspace = true
+readme.workspace = true
+homepage.workspace = true
+license.workspace = true
+keywords.workspace = true
+categories.workspace = true
+documentation.workspace = true
+
+[dependencies]
+tokio = { workspace = true, features = ["full"] }
+uuid = { workspace = true }
+service = { workspace = true }
+api = { workspace = true }
+session = { workspace = true }
+config = { workspace = true }
+db = { workspace = true }
+migrate = { workspace = true }
+actix-web = { workspace = true }
+actix-cors = { workspace = true }
+futures = { workspace = true }
+slog = "2"
+anyhow = { workspace = true }
+clap = { workspace = true }
+sea-orm = { workspace = true }
+serde_json = { workspace = true }
+chrono = { workspace = true }
+[lints]
+workspace = true
diff --git a/apps/app/src/args.rs b/apps/app/src/args.rs
new file mode 100644
index 0000000..3433ce5
--- /dev/null
+++ b/apps/app/src/args.rs
@@ -0,0 +1,12 @@
+use clap::Parser;
+
+#[derive(Parser, Debug)]
+#[command(name = "app")]
+#[command(version)]
+pub struct ServerArgs {
+ #[arg(long, short)]
+ pub bind: Option,
+
+ #[arg(long)]
+ pub workers: Option,
+}
diff --git a/apps/app/src/logging.rs b/apps/app/src/logging.rs
new file mode 100644
index 0000000..26a1068
--- /dev/null
+++ b/apps/app/src/logging.rs
@@ -0,0 +1,126 @@
+//! Structured HTTP request logging middleware using slog.
+//!
+//! Logs every incoming request with method, path, status code,
+//! response time, client IP, and authenticated user ID.
+
+use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform};
+use futures::future::{LocalBoxFuture, Ready, ok};
+use session::SessionExt;
+use slog::{error as slog_error, info as slog_info, warn as slog_warn};
+use std::sync::Arc;
+use std::task::{Context, Poll};
+use std::time::Instant;
+use uuid::Uuid;
+
+/// Default log format: `{method} {path} {status} {duration_ms}ms`
+pub struct RequestLogger {
+ log: slog::Logger,
+}
+
+impl RequestLogger {
+ pub fn new(log: slog::Logger) -> Self {
+ Self { log }
+ }
+}
+
+impl Transform for RequestLogger
+where
+ S: Service, Error = actix_web::Error> + 'static,
+ S::Future: 'static,
+ B: 'static,
+{
+ type Response = ServiceResponse;
+ type Error = actix_web::Error;
+ type Transform = RequestLoggerMiddleware;
+ type InitError = ();
+ type Future = Ready>;
+
+ fn new_transform(&self, service: S) -> Self::Future {
+ ok(RequestLoggerMiddleware {
+ service: Arc::new(service),
+ log: self.log.clone(),
+ })
+ }
+}
+
+pub struct RequestLoggerMiddleware {
+ service: Arc,
+ log: slog::Logger,
+}
+
+impl Clone for RequestLoggerMiddleware {
+ fn clone(&self) -> Self {
+ Self {
+ service: self.service.clone(),
+ log: self.log.clone(),
+ }
+ }
+}
+
+impl Service for RequestLoggerMiddleware
+where
+ S: Service, Error = actix_web::Error> + 'static,
+ S::Future: 'static,
+ B: 'static,
+{
+ type Response = ServiceResponse;
+ type Error = actix_web::Error;
+ type Future = LocalBoxFuture<'static, Result>;
+
+ fn poll_ready(&self, cx: &mut Context<'_>) -> Poll> {
+ self.service.poll_ready(cx)
+ }
+
+ fn call(&self, req: ServiceRequest) -> Self::Future {
+ let started = Instant::now();
+ let log = self.log.clone();
+ let method = req.method().to_string();
+ let path = req.path().to_string();
+ let query = req.query_string().to_string();
+ let remote = req
+ .connection_info()
+ .realip_remote_addr()
+ .map(|s| s.to_string())
+ .unwrap_or_else(|| "unknown".to_string());
+ let user_id: Option = req.get_session().user();
+
+ let full_path = if query.is_empty() {
+ path.clone()
+ } else {
+ format!("{}?{}", path, query)
+ };
+
+ // Clone the Arc so it can be moved into the async block
+ let service = self.service.clone();
+
+ Box::pin(async move {
+ let res = service.call(req).await?;
+ let elapsed = started.elapsed();
+ let status = res.status();
+ let status_code = status.as_u16();
+ let is_health = path == "/health";
+
+ if !is_health {
+ let user_id_str = user_id
+ .map(|u: Uuid| u.to_string())
+ .unwrap_or_else(|| "-".to_string());
+ let log_message = format!(
+ "HTTP request | method={} | path={} | status={} | duration_ms={} | remote={} | user_id={}",
+ method,
+ full_path,
+ status_code,
+ elapsed.as_millis(),
+ remote,
+ user_id_str
+ );
+
+ match status_code {
+ 200..=299 => slog_info!(&log, "{}", log_message),
+ 400..=499 => slog_warn!(&log, "{}", log_message),
+ _ => slog_error!(&log, "{}", log_message),
+ }
+ }
+ Ok(res)
+ })
+ }
+}
diff --git a/apps/app/src/main.rs b/apps/app/src/main.rs
new file mode 100644
index 0000000..4087380
--- /dev/null
+++ b/apps/app/src/main.rs
@@ -0,0 +1,210 @@
+use actix_cors::Cors;
+use actix_web::cookie::time::Duration;
+use actix_web::middleware::Logger;
+use actix_web::{App, HttpResponse, HttpServer, cookie::Key, web};
+use clap::Parser;
+use db::cache::AppCache;
+use db::database::AppDatabase;
+use sea_orm::ConnectionTrait;
+use service::AppService;
+use session::SessionMiddleware;
+use session::config::{PersistentSession, SessionLifecycle, TtlExtensionPolicy};
+use session::storage::RedisClusterSessionStore;
+use slog::Drain;
+
+mod args;
+mod logging;
+
+use args::ServerArgs;
+use config::AppConfig;
+use migrate::{Migrator, MigratorTrait};
+
+#[derive(Clone)]
+pub struct AppState {
+ pub db: AppDatabase,
+ pub cache: AppCache,
+}
+
+fn build_slog_logger(level: &str) -> slog::Logger {
+ let level_filter = match level {
+ "trace" => 0usize,
+ "debug" => 1usize,
+ "info" => 2usize,
+ "warn" => 3usize,
+ "error" => 4usize,
+ _ => 2usize,
+ };
+
+ struct StderrDrain(usize);
+
+ impl Drain for StderrDrain {
+ type Ok = ();
+ type Err = ();
+ #[inline]
+ fn log(&self, record: &slog::Record, _logger: &slog::OwnedKVList) -> Result<(), ()> {
+ let slog_level = match record.level() {
+ slog::Level::Trace => 0,
+ slog::Level::Debug => 1,
+ slog::Level::Info => 2,
+ slog::Level::Warning => 3,
+ slog::Level::Error => 4,
+ slog::Level::Critical => 5,
+ };
+ if slog_level < self.0 {
+ return Ok(());
+ }
+ let _ = eprintln!(
+ "{} [{}] {}:{} - {}",
+ chrono::Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ"),
+ record.level().to_string(),
+ record
+ .file()
+ .rsplit_once('/')
+ .map(|(_, s)| s)
+ .unwrap_or(record.file()),
+ record.line(),
+ record.msg(),
+ );
+ Ok(())
+ }
+ }
+
+ let drain = StderrDrain(level_filter);
+ let drain = std::sync::Mutex::new(drain);
+ let drain = slog::Fuse::new(drain);
+ slog::Logger::root(drain, slog::o!())
+}
+
+fn build_session_key(cfg: &AppConfig) -> anyhow::Result {
+ if let Some(secret) = cfg.env.get("APP_SESSION_SECRET") {
+ let bytes: Vec = secret.as_bytes().iter().cycle().take(64).copied().collect();
+ return Ok(Key::from(&bytes));
+ }
+ Ok(Key::generate())
+}
+
+#[tokio::main]
+async fn main() -> anyhow::Result<()> {
+ let cfg = AppConfig::load();
+ let log_level = cfg.log_level().unwrap_or_else(|_| "info".to_string());
+ let log = build_slog_logger(&log_level);
+ slog::info!(
+ log,
+ "Starting {} {}",
+ cfg.app_name().unwrap_or_default(),
+ cfg.app_version().unwrap_or_default()
+ );
+ let db = AppDatabase::init(&cfg).await?;
+ slog::info!(log, "Database connected");
+ let redis_urls = cfg.redis_urls()?;
+ let store: RedisClusterSessionStore = RedisClusterSessionStore::new(redis_urls).await?;
+ slog::info!(log, "Redis connected");
+ let cache = AppCache::init(&cfg).await?;
+ slog::info!(log, "Cache initialized");
+ run_migrations(&db, &log).await?;
+ let session_key = build_session_key(&cfg)?;
+ let args = ServerArgs::parse();
+ let service = AppService::new(cfg.clone()).await?;
+ slog::info!(log, "AppService initialized");
+
+ let (shutdown_tx, shutdown_rx) = tokio::sync::broadcast::channel::<()>(1);
+ let worker_service = service.clone();
+ let log_for_http = log.clone();
+ let log_for_worker = log.clone();
+ let worker_handle = tokio::spawn(async move {
+ worker_service
+ .start_room_workers(shutdown_rx, log_for_worker)
+ .await
+ });
+
+ let bind_addr = args.bind.unwrap_or_else(|| "127.0.0.1:8080".to_string());
+ slog::info!(log, "Listening on {}", bind_addr);
+ HttpServer::new(move || {
+ let cors = Cors::default()
+ .allow_any_origin()
+ .allow_any_method()
+ .allow_any_header()
+ .supports_credentials()
+ .max_age(3600);
+
+ let session_mw = SessionMiddleware::builder(store.clone(), session_key.clone())
+ .cookie_name("id".to_string())
+ .cookie_path("/".to_string())
+ .cookie_secure(false)
+ .cookie_http_only(true)
+ .session_lifecycle(SessionLifecycle::PersistentSession(
+ PersistentSession::default()
+ .session_ttl(Duration::days(30))
+ .session_ttl_extension_policy(TtlExtensionPolicy::OnEveryRequest),
+ ))
+ .build();
+
+ App::new()
+ .wrap(cors)
+ .wrap(session_mw)
+ .wrap(Logger::default().exclude("/health"))
+ .app_data(web::Data::new(AppState {
+ db: db.clone(),
+ cache: cache.clone(),
+ }))
+ .app_data(web::Data::new(service.clone()))
+ .app_data(web::Data::new(cfg.clone()))
+ .app_data(web::Data::new(db.clone()))
+ .app_data(web::Data::new(cache.clone()))
+ .wrap(logging::RequestLogger::new(log_for_http.clone()))
+ .route("/health", web::get().to(health_check))
+ .configure(api::route::init_routes)
+ })
+ .bind(&bind_addr)?
+ .run()
+ .await?;
+
+ slog::info!(log, "Server stopped, shutting down room workers");
+ let _ = shutdown_tx.send(());
+ let _ = worker_handle.await;
+ slog::info!(log, "Room workers stopped");
+
+ Ok(())
+}
+
+async fn run_migrations(db: &AppDatabase, log: &slog::Logger) -> anyhow::Result<()> {
+ slog::info!(log, "Running database migrations...");
+ Migrator::up(db.writer(), None)
+ .await
+ .map_err(|e| anyhow::anyhow!("Migration failed: {:?}", e))?;
+ slog::info!(log, "Migrations completed");
+ Ok(())
+}
+
+async fn health_check(state: web::Data) -> HttpResponse {
+ let db_ok = db_ping(&state.db).await;
+ let cache_ok = cache_ping(&state.cache).await;
+
+ let healthy = db_ok && cache_ok;
+ if healthy {
+ HttpResponse::Ok().json(serde_json::json!({
+ "status": "ok",
+ "db": "ok",
+ "cache": "ok",
+ }))
+ } else {
+ HttpResponse::ServiceUnavailable().json(serde_json::json!({
+ "status": "unhealthy",
+ "db": if db_ok { "ok" } else { "error" },
+ "cache": if cache_ok { "ok" } else { "error" },
+ }))
+ }
+}
+
+async fn db_ping(db: &AppDatabase) -> bool {
+ db.query_one_raw(sea_orm::Statement::from_string(
+ sea_orm::DbBackend::Postgres,
+ "SELECT 1",
+ ))
+ .await
+ .is_ok()
+}
+
+async fn cache_ping(cache: &AppCache) -> bool {
+ cache.conn().await.is_ok()
+}
diff --git a/apps/email/Cargo.toml b/apps/email/Cargo.toml
new file mode 100644
index 0000000..ad888ad
--- /dev/null
+++ b/apps/email/Cargo.toml
@@ -0,0 +1,30 @@
+[package]
+name = "email-server"
+version.workspace = true
+edition.workspace = true
+authors.workspace = true
+description.workspace = true
+repository.workspace = true
+readme.workspace = true
+homepage.workspace = true
+license.workspace = true
+keywords.workspace = true
+categories.workspace = true
+documentation.workspace = true
+
+[[bin]]
+name = "email-worker"
+path = "src/main.rs"
+
+[dependencies]
+tokio = { workspace = true, features = ["full"] }
+service = { workspace = true }
+db = { workspace = true }
+config = { workspace = true }
+slog = { workspace = true }
+anyhow = { workspace = true }
+clap = { workspace = true, features = ["derive"] }
+chrono = { workspace = true, features = ["serde"] }
+
+[lints]
+workspace = true
diff --git a/apps/email/src/main.rs b/apps/email/src/main.rs
new file mode 100644
index 0000000..d08a6b1
--- /dev/null
+++ b/apps/email/src/main.rs
@@ -0,0 +1,84 @@
+use clap::Parser;
+use config::AppConfig;
+use service::AppService;
+use slog::{Drain, OwnedKVList, Record};
+
+#[derive(Parser, Debug)]
+#[command(name = "email-worker")]
+#[command(version)]
+struct Args {
+ #[arg(long, default_value = "info")]
+ log_level: String,
+}
+
+#[tokio::main]
+async fn main() -> anyhow::Result<()> {
+ let args = Args::parse();
+ let cfg = AppConfig::load();
+ let log = build_logger(&args.log_level);
+
+ slog::info!(log, "Starting email worker");
+ let service = AppService::new(cfg).await?;
+
+ let (shutdown_tx, shutdown_rx) = tokio::sync::broadcast::channel::<()>(1);
+ let log_for_signal = log.clone();
+ tokio::spawn(async move {
+ tokio::signal::ctrl_c().await.ok();
+ slog::info!(log_for_signal, "shutting down email worker");
+ let _ = shutdown_tx.send(());
+ });
+
+ service.start_email_workers(shutdown_rx).await?;
+ slog::info!(log, "email worker stopped");
+ Ok(())
+}
+
+fn build_logger(level: &str) -> slog::Logger {
+ let level_filter = match level {
+ "trace" => 0usize,
+ "debug" => 1usize,
+ "info" => 2usize,
+ "warn" => 3usize,
+ "error" => 4usize,
+ _ => 2usize,
+ };
+
+ struct StderrDrain(usize);
+
+ impl Drain for StderrDrain {
+ type Ok = ();
+ type Err = ();
+ #[inline]
+ fn log(&self, record: &Record, _logger: &OwnedKVList) -> Result<(), ()> {
+ let slog_level = match record.level() {
+ slog::Level::Trace => 0,
+ slog::Level::Debug => 1,
+ slog::Level::Info => 2,
+ slog::Level::Warning => 3,
+ slog::Level::Error => 4,
+ slog::Level::Critical => 5,
+ };
+ if slog_level < self.0 {
+ return Ok(());
+ }
+ let _ = eprintln!(
+ "{} [{}] {}:{} - {}",
+ chrono::Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ"),
+ record.level().to_string(),
+ record
+ .file()
+ .rsplit_once('/')
+ .map(|(_, s)| s)
+ .unwrap_or(record.file()),
+ record.line(),
+ record.msg(),
+ );
+ Ok(())
+ }
+ }
+
+ let drain = StderrDrain(level_filter);
+ let drain = std::sync::Mutex::new(drain);
+ let drain = slog::Fuse::new(drain);
+ slog::Logger::root(drain, slog::o!())
+}
diff --git a/apps/git-hook/Cargo.toml b/apps/git-hook/Cargo.toml
new file mode 100644
index 0000000..2cef8bb
--- /dev/null
+++ b/apps/git-hook/Cargo.toml
@@ -0,0 +1,27 @@
+[package]
+name = "git-hook"
+version.workspace = true
+edition.workspace = true
+authors.workspace = true
+description.workspace = true
+repository.workspace = true
+readme.workspace = true
+homepage.workspace = true
+license.workspace = true
+keywords.workspace = true
+categories.workspace = true
+documentation.workspace = true
+
+[dependencies]
+tokio = { workspace = true, features = ["full"] }
+git = { workspace = true }
+db = { workspace = true }
+config = { workspace = true }
+tracing = { workspace = true }
+tracing-subscriber = { workspace = true, features = ["json"] }
+anyhow = { workspace = true }
+slog = { workspace = true }
+clap = { workspace = true, features = ["derive"] }
+tokio-util = { workspace = true }
+chrono = { workspace = true, features = ["serde"] }
+reqwest = { workspace = true }
diff --git a/apps/git-hook/src/args.rs b/apps/git-hook/src/args.rs
new file mode 100644
index 0000000..d6dd9b9
--- /dev/null
+++ b/apps/git-hook/src/args.rs
@@ -0,0 +1,10 @@
+use clap::Parser;
+
+#[derive(Parser, Debug)]
+#[command(name = "git-hook")]
+#[command(version)]
+pub struct HookArgs {
+ /// Worker ID for this instance. Defaults to the HOOK_POOL_WORKER_ID env var or a generated UUID.
+ #[arg(long)]
+ pub worker_id: Option,
+}
diff --git a/apps/git-hook/src/main.rs b/apps/git-hook/src/main.rs
new file mode 100644
index 0000000..aaf762f
--- /dev/null
+++ b/apps/git-hook/src/main.rs
@@ -0,0 +1,142 @@
+use clap::Parser;
+use config::AppConfig;
+use db::cache::AppCache;
+use db::database::AppDatabase;
+use git::hook::GitServiceHooks;
+use slog::{Drain, OwnedKVList, Record};
+use tokio::signal;
+use tokio_util::sync::CancellationToken;
+
+mod args;
+
+use args::HookArgs;
+
+#[tokio::main]
+async fn main() -> anyhow::Result<()> {
+ // 1. Load configuration
+ let cfg = AppConfig::load();
+
+ // 2. Init slog logging
+ let log_level = cfg.log_level().unwrap_or_else(|_| "info".to_string());
+ let log = build_slog_logger(&log_level);
+
+ // 3. Connect to database
+ let db = AppDatabase::init(&cfg).await?;
+ slog::info!(log, "database connected");
+
+ // 4. Connect to Redis cache (also provides the cluster pool for hook queue)
+ let cache = AppCache::init(&cfg).await?;
+ slog::info!(log, "cache connected");
+
+ // 5. Parse CLI args
+ let args = HookArgs::parse();
+
+ slog::info!(log, "git-hook worker starting";
+ "worker_id" => %args.worker_id.unwrap_or_else(|| "default".to_string())
+ );
+
+ // 5. Build HTTP client for webhook delivery
+ let http = reqwest::Client::builder()
+ .user_agent("Code-Git-Hook/1.0")
+ .build()
+ .unwrap_or_else(|_| reqwest::Client::new());
+
+ // 6. Build and run git hook service
+ let hooks = GitServiceHooks::new(
+ db,
+ cache.clone(),
+ cache.redis_pool().clone(),
+ log.clone(),
+ cfg,
+ std::sync::Arc::new(http),
+ );
+
+ let cancel = CancellationToken::new();
+ let cancel_clone = cancel.clone();
+
+ // Spawn signal handler
+ let log_clone = log.clone();
+ tokio::spawn(async move {
+ let ctrl_c = async {
+ signal::ctrl_c()
+ .await
+ .expect("failed to install CTRL+C handler");
+ };
+
+ #[cfg(unix)]
+ let term = async {
+ use tokio::signal::unix::{SignalKind, signal};
+ let mut sig =
+ signal(SignalKind::terminate()).expect("failed to install SIGTERM handler");
+ sig.recv().await;
+ };
+
+ #[cfg(not(unix))]
+ let term = std::future::pending::<()>();
+
+ tokio::select! {
+ _ = ctrl_c => {
+ slog::info!(log_clone, "received SIGINT, initiating shutdown");
+ }
+ _ = term => {
+ slog::info!(log_clone, "received SIGTERM, initiating shutdown");
+ }
+ }
+ cancel_clone.cancel();
+ });
+
+ hooks.run(cancel).await?;
+
+ slog::info!(log, "git-hook worker stopped");
+ Ok(())
+}
+
+fn build_slog_logger(level: &str) -> slog::Logger {
+ let level_filter = match level {
+ "trace" => 0usize,
+ "debug" => 1usize,
+ "info" => 2usize,
+ "warn" => 3usize,
+ "error" => 4usize,
+ _ => 2usize,
+ };
+
+ struct StderrDrain(usize);
+
+ impl Drain for StderrDrain {
+ type Ok = ();
+ type Err = ();
+ #[inline]
+ fn log(&self, record: &Record, _logger: &OwnedKVList) -> Result<(), ()> {
+ let slog_level = match record.level() {
+ slog::Level::Trace => 0,
+ slog::Level::Debug => 1,
+ slog::Level::Info => 2,
+ slog::Level::Warning => 3,
+ slog::Level::Error => 4,
+ slog::Level::Critical => 5,
+ };
+ if slog_level < self.0 {
+ return Ok(());
+ }
+ let _ = eprintln!(
+ "{} [{}] {}:{} - {}",
+ chrono::Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ"),
+ record.level().to_string(),
+ record
+ .file()
+ .rsplit_once('/')
+ .map(|(_, s)| s)
+ .unwrap_or(record.file()),
+ record.line(),
+ record.msg(),
+ );
+ Ok(())
+ }
+ }
+
+ let drain = StderrDrain(level_filter);
+ let drain = std::sync::Mutex::new(drain);
+ let drain = slog::Fuse::new(drain);
+ slog::Logger::root(drain, slog::o!())
+}
diff --git a/apps/gitserver/Cargo.toml b/apps/gitserver/Cargo.toml
new file mode 100644
index 0000000..5b11284
--- /dev/null
+++ b/apps/gitserver/Cargo.toml
@@ -0,0 +1,30 @@
+[package]
+name = "gitserver"
+version.workspace = true
+edition.workspace = true
+authors.workspace = true
+description.workspace = true
+repository.workspace = true
+readme.workspace = true
+homepage.workspace = true
+license.workspace = true
+keywords.workspace = true
+categories.workspace = true
+documentation.workspace = true
+
+[[bin]]
+name = "gitserver"
+path = "src/main.rs"
+
+[dependencies]
+tokio = { workspace = true, features = ["full"] }
+git = { workspace = true }
+db = { workspace = true }
+config = { workspace = true }
+slog = { workspace = true }
+anyhow = { workspace = true }
+clap = { workspace = true, features = ["derive"] }
+chrono = { workspace = true, features = ["serde"] }
+
+[lints]
+workspace = true
diff --git a/apps/gitserver/src/main.rs b/apps/gitserver/src/main.rs
new file mode 100644
index 0000000..17c077f
--- /dev/null
+++ b/apps/gitserver/src/main.rs
@@ -0,0 +1,94 @@
+use clap::Parser;
+use config::AppConfig;
+use slog::{Drain, OwnedKVList, Record};
+
+#[derive(Parser, Debug)]
+#[command(name = "gitserver")]
+#[command(version)]
+struct Args {
+ #[arg(long, default_value = "info")]
+ log_level: String,
+}
+
+#[tokio::main]
+async fn main() -> anyhow::Result<()> {
+ let args = Args::parse();
+ let cfg = AppConfig::load();
+ let log = build_logger(&args.log_level);
+
+ let http_handle = tokio::spawn(git::http::run_http(cfg.clone(), log.clone()));
+ let ssh_handle = tokio::spawn(git::ssh::run_ssh(cfg, log.clone()));
+
+ tokio::select! {
+ result = http_handle => {
+ match result {
+ Ok(Ok(())) => slog::info!(log, "HTTP server stopped"),
+ Ok(Err(e)) => slog::error!(log, "HTTP server error: {}", e),
+ Err(e) => slog::error!(log, "HTTP server task panicked: {}", e),
+ }
+ }
+ result = ssh_handle => {
+ match result {
+ Ok(Ok(())) => slog::info!(log, "SSH server stopped"),
+ Ok(Err(e)) => slog::error!(log, "SSH server error: {}", e),
+ Err(e) => slog::error!(log, "SSH server task panicked: {}", e),
+ }
+ }
+ _ = tokio::signal::ctrl_c() => {
+ slog::info!(log, "received shutdown signal");
+ }
+ }
+
+ slog::info!(log, "shutting down");
+ Ok(())
+}
+
+fn build_logger(level: &str) -> slog::Logger {
+ let level_filter = match level {
+ "trace" => 0usize,
+ "debug" => 1usize,
+ "info" => 2usize,
+ "warn" => 3usize,
+ "error" => 4usize,
+ _ => 2usize,
+ };
+
+ struct StderrDrain(usize);
+
+ impl Drain for StderrDrain {
+ type Ok = ();
+ type Err = ();
+ #[inline]
+ fn log(&self, record: &Record, _logger: &OwnedKVList) -> Result<(), ()> {
+ let slog_level = match record.level() {
+ slog::Level::Trace => 0,
+ slog::Level::Debug => 1,
+ slog::Level::Info => 2,
+ slog::Level::Warning => 3,
+ slog::Level::Error => 4,
+ slog::Level::Critical => 5,
+ };
+ if slog_level < self.0 {
+ return Ok(());
+ }
+ let _ = eprintln!(
+ "{} [{}] {}:{} - {}",
+ chrono::Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ"),
+ record.level().to_string(),
+ record
+ .file()
+ .rsplit_once('/')
+ .map(|(_, s)| s)
+ .unwrap_or(record.file()),
+ record.line(),
+ record.msg(),
+ );
+ Ok(())
+ }
+ }
+
+ let drain = StderrDrain(level_filter);
+ let drain = std::sync::Mutex::new(drain);
+ let drain = slog::Fuse::new(drain);
+ slog::Logger::root(drain, slog::o!())
+}
diff --git a/apps/migrate/Cargo.toml b/apps/migrate/Cargo.toml
new file mode 100644
index 0000000..3fe1bbe
--- /dev/null
+++ b/apps/migrate/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "migrate-cli"
+version.workspace = true
+edition.workspace = true
+
+[dependencies]
+migrate.workspace = true
+sea-orm = { workspace = true, features = ["sqlx-all", "runtime-tokio"] }
+tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
+anyhow.workspace = true
+clap.workspace = true
+dotenvy.workspace = true
+config = { workspace = true }
\ No newline at end of file
diff --git a/apps/migrate/src/main.rs b/apps/migrate/src/main.rs
new file mode 100644
index 0000000..126c359
--- /dev/null
+++ b/apps/migrate/src/main.rs
@@ -0,0 +1,102 @@
+use anyhow::Context;
+use clap::Command;
+use migrate::MigratorTrait;
+use sea_orm::{Database, DatabaseConnection};
+
+#[tokio::main]
+async fn main() -> anyhow::Result<()> {
+ dotenvy::dotenv().ok();
+ config::AppConfig::load();
+
+ let cmd = Command::new("migrate")
+ .about("Database migration CLI")
+ .arg(
+ clap::Arg::new("steps")
+ .help("Number of migrations (for up/down)")
+ .required(false)
+ .index(1),
+ )
+ .subcommand(Command::new("up").about("Apply pending migrations"))
+ .subcommand(Command::new("down").about("Revert applied migrations"))
+ .subcommand(Command::new("fresh").about("Drop all tables and re-apply"))
+ .subcommand(Command::new("refresh").about("Revert all then re-apply"))
+ .subcommand(Command::new("reset").about("Revert all applied migrations"))
+ .subcommand(Command::new("status").about("Show migration status"))
+ .try_get_matches()
+ .map_err(|e| anyhow::anyhow!("{}", e))?;
+
+ let db_url = config::AppConfig::load().database_url()?;
+
+ let db: DatabaseConnection = Database::connect(&db_url).await?;
+
+ match cmd.subcommand_name() {
+ Some("up") => {
+ let steps = cmd
+ .get_one::("steps")
+ .and_then(|s| s.parse().ok())
+ .unwrap_or(0);
+ run_up(&db, steps).await?;
+ }
+ Some("down") => {
+ let steps = cmd
+ .get_one::("steps")
+ .and_then(|s| s.parse().ok())
+ .unwrap_or(1);
+ run_down(&db, steps).await?;
+ }
+ Some("fresh") => run_fresh(&db).await?,
+ Some("refresh") => run_refresh(&db).await?,
+ Some("reset") => run_reset(&db).await?,
+ Some("status") => run_status(&db).await?,
+ _ => {
+ eprintln!(
+ "Usage: migrate \nCommands: up, down, fresh, refresh, reset, status"
+ );
+ std::process::exit(1);
+ }
+ }
+
+ Ok(())
+}
+
+async fn run_up(db: &DatabaseConnection, steps: u32) -> anyhow::Result<()> {
+ migrate::Migrator::up(db, if steps == 0 { None } else { Some(steps) })
+ .await
+ .context("failed to run migrations up")?;
+ Ok(())
+}
+
+async fn run_down(db: &DatabaseConnection, steps: u32) -> anyhow::Result<()> {
+ migrate::Migrator::down(db, Some(steps))
+ .await
+ .context("failed to run migrations down")?;
+ Ok(())
+}
+
+async fn run_fresh(db: &DatabaseConnection) -> anyhow::Result<()> {
+ migrate::Migrator::fresh(db)
+ .await
+ .context("failed to run migrations fresh")?;
+ Ok(())
+}
+
+async fn run_refresh(db: &DatabaseConnection) -> anyhow::Result<()> {
+ migrate::Migrator::refresh(db)
+ .await
+ .context("failed to run migrations refresh")?;
+ Ok(())
+}
+
+async fn run_reset(db: &DatabaseConnection) -> anyhow::Result<()> {
+ migrate::Migrator::reset(db)
+ .await
+ .context("failed to run migrations reset")?;
+ Ok(())
+}
+
+async fn run_status(db: &DatabaseConnection) -> anyhow::Result<()> {
+ migrate::Migrator::status(db)
+ .await
+ .context("failed to get migration status")?;
+ Ok(())
+}
diff --git a/apps/operator/Cargo.toml b/apps/operator/Cargo.toml
new file mode 100644
index 0000000..211f759
--- /dev/null
+++ b/apps/operator/Cargo.toml
@@ -0,0 +1,30 @@
+[package]
+name = "operator"
+version.workspace = true
+edition.workspace = true
+authors.workspace = true
+description.workspace = true
+repository.workspace = true
+readme.workspace = true
+homepage.workspace = true
+license.workspace = true
+keywords.workspace = true
+categories.workspace = true
+documentation.workspace = true
+
+[dependencies]
+kube = { workspace = true }
+k8s-openapi = { workspace = true }
+serde = { workspace = true }
+serde_json.workspace = true
+serde_yaml = { workspace = true }
+tokio = { workspace = true, features = ["rt-multi-thread", "macros", "sync"] }
+anyhow.workspace = true
+futures.workspace = true
+tracing.workspace = true
+tracing-subscriber.workspace = true
+chrono = { workspace = true }
+uuid = { workspace = true, features = ["v4"] }
+
+[lints]
+workspace = true
diff --git a/apps/operator/src/context.rs b/apps/operator/src/context.rs
new file mode 100644
index 0000000..ca782a1
--- /dev/null
+++ b/apps/operator/src/context.rs
@@ -0,0 +1,44 @@
+//! Shared reconcile context.
+
+use kube::Client;
+
+/// Context passed to every reconcile call.
+#[derive(Clone)]
+pub struct ReconcileCtx {
+ pub client: Client,
+ /// Default image registry prefix (e.g. "myapp/").
+ pub image_prefix: String,
+ /// Operator's own namespace.
+ pub operator_namespace: String,
+}
+
+impl ReconcileCtx {
+ pub async fn from_env() -> anyhow::Result {
+ let client = Client::try_default().await?;
+ let ns = std::env::var("POD_NAMESPACE").unwrap_or_else(|_| "default".to_string());
+ let prefix =
+ std::env::var("OPERATOR_IMAGE_PREFIX").unwrap_or_else(|_| "myapp/".to_string());
+
+ Ok(Self {
+ client,
+ image_prefix: prefix,
+ operator_namespace: ns,
+ })
+ }
+
+ /// Prepend image_prefix to an unqualified image name.
+ /// E.g. "app:latest" → "myapp/app:latest"
+ pub fn resolve_image(&self, image: &str) -> String {
+ // If it already has a registry/domain component, leave it alone.
+ if image.contains('/') && !image.starts_with(&self.image_prefix) {
+ image.to_string()
+ } else if image.starts_with(&self.image_prefix) {
+ image.to_string()
+ } else {
+ // Unqualified name: prepend prefix.
+ format!("{}{}", self.image_prefix, image)
+ }
+ }
+}
+
+pub type ReconcileState = ReconcileCtx;
diff --git a/apps/operator/src/controller/app.rs b/apps/operator/src/controller/app.rs
new file mode 100644
index 0000000..a1d2731
--- /dev/null
+++ b/apps/operator/src/controller/app.rs
@@ -0,0 +1,221 @@
+//! Controller for the `App` CRD — manages Deployment + Service.
+
+use crate::context::ReconcileState;
+use crate::controller::helpers::{
+ child_meta, env_var_to_json, merge_env, owner_ref, query_deployment_status, std_labels,
+};
+use crate::crd::{App, AppSpec};
+use serde_json::{Value, json};
+use std::sync::Arc;
+use tracing::info;
+
+/// Reconcile an App resource: create/update Deployment + Service.
+pub async fn reconcile(app: Arc, ctx: Arc) -> Result<(), kube::Error> {
+ let ns = app.metadata.namespace.as_deref().unwrap_or("default");
+ let name = app.metadata.name.as_deref().unwrap_or("");
+ let spec = &app.spec;
+ let client = &ctx.client;
+
+ let or = owner_ref(&app.metadata, &app.api_version, &app.kind);
+ let labels = std_labels();
+
+ // ---- Deployment ----
+ let deployment = build_deployment(ns, name, spec, &or, &labels);
+ apply_deployment(client, ns, name, &deployment).await?;
+
+ // ---- Service ----
+ let service = build_service(ns, name, &or, &labels);
+ apply_service(client, ns, name, &service).await?;
+
+ // ---- Status patch ----
+ let (ready_replicas, phase) = query_deployment_status(client, ns, name).await?;
+ let status = json!({
+ "status": {
+ "readyReplicas": ready_replicas,
+ "phase": phase
+ }
+ });
+ patch_status::(client, ns, name, &status).await?;
+
+ Ok(())
+}
+
+fn build_deployment(
+ ns: &str,
+ name: &str,
+ spec: &AppSpec,
+ or: &crate::crd::OwnerReference,
+ labels: &std::collections::BTreeMap,
+) -> Value {
+ let env = merge_env(&[], &spec.env);
+ let image = if spec.image.is_empty() {
+ "myapp/app:latest".to_string()
+ } else {
+ spec.image.clone()
+ };
+ let pull = if spec.image_pull_policy.is_empty() {
+ "IfNotPresent".to_string()
+ } else {
+ spec.image_pull_policy.clone()
+ };
+
+ let resources = build_resources(&spec.resources);
+ let liveness = spec.liveness_probe.as_ref().map(|p| {
+ json!({
+ "httpGet": { "path": p.path, "port": p.port },
+ "initialDelaySeconds": p.initial_delay_seconds,
+ "periodSeconds": 10,
+ })
+ });
+ let readiness = spec.readiness_probe.as_ref().map(|p| {
+ json!({
+ "httpGet": { "path": p.path, "port": p.port },
+ "initialDelaySeconds": p.initial_delay_seconds,
+ "periodSeconds": 5,
+ })
+ });
+
+ json!({
+ "metadata": child_meta(name, ns, or, labels.clone()),
+ "spec": {
+ "replicas": spec.replicas,
+ "selector": { "matchLabels": labels },
+ "strategy": {
+ "type": "RollingUpdate",
+ "rollingUpdate": { "maxSurge": 1, "maxUnavailable": 0 }
+ },
+ "template": {
+ "metadata": { "labels": labels.clone() },
+ "spec": {
+ "containers": [{
+ "name": "app",
+ "image": image,
+ "ports": [{ "containerPort": 8080 }],
+ "env": env.iter().map(env_var_to_json).collect::>(),
+ "imagePullPolicy": pull,
+ "resources": resources,
+ "livenessProbe": liveness,
+ "readinessProbe": readiness,
+ }]
+ }
+ }
+ }
+ })
+}
+
+fn build_service(
+ ns: &str,
+ name: &str,
+ or: &crate::crd::OwnerReference,
+ labels: &std::collections::BTreeMap,
+) -> Value {
+ json!({
+ "metadata": child_meta(name, ns, or, labels.clone()),
+ "spec": {
+ "ports": [{ "port": 80, "targetPort": 8080, "name": "http" }],
+ "selector": labels.clone(),
+ "type": "ClusterIP"
+ }
+ })
+}
+
+pub(crate) fn build_resources(res: &Option) -> Value {
+ match res {
+ Some(r) => {
+ let mut out = serde_json::Map::new();
+ if let Some(ref req) = r.requests {
+ let mut req_map = serde_json::Map::new();
+ if let Some(ref cpu) = req.cpu {
+ req_map.insert("cpu".to_string(), json!(cpu));
+ }
+ if let Some(ref mem) = req.memory {
+ req_map.insert("memory".to_string(), json!(mem));
+ }
+ if !req_map.is_empty() {
+ out.insert("requests".to_string(), Value::Object(req_map));
+ }
+ }
+ if let Some(ref lim) = r.limits {
+ let mut lim_map = serde_json::Map::new();
+ if let Some(ref cpu) = lim.cpu {
+ lim_map.insert("cpu".to_string(), json!(cpu));
+ }
+ if let Some(ref mem) = lim.memory {
+ lim_map.insert("memory".to_string(), json!(mem));
+ }
+ if !lim_map.is_empty() {
+ out.insert("limits".to_string(), Value::Object(lim_map));
+ }
+ }
+ if out.is_empty() {
+ json!({})
+ } else {
+ Value::Object(out)
+ }
+ }
+ None => json!({}),
+ }
+}
+
+pub(crate) async fn apply_deployment(
+ client: &kube::Client,
+ ns: &str,
+ name: &str,
+ body: &Value,
+) -> Result<(), kube::Error> {
+ let api: kube::Api = kube::Api::namespaced(client.clone(), ns);
+ let jr = crate::crd::JsonResource::new(Default::default(), body.clone());
+ match api.get(name).await {
+ Ok(_) => {
+ info!(name, ns, "updating app deployment");
+ let _ = api
+ .replace(name, &kube::api::PostParams::default(), &jr)
+ .await?;
+ }
+ Err(kube::Error::Api(e)) if e.code == 404 => {
+ info!(name, ns, "creating app deployment");
+ let _ = api.create(&kube::api::PostParams::default(), &jr).await?;
+ }
+ Err(e) => return Err(e),
+ }
+ Ok(())
+}
+
+pub(crate) async fn apply_service(
+ client: &kube::Client,
+ ns: &str,
+ name: &str,
+ body: &Value,
+) -> Result<(), kube::Error> {
+ let api: kube::Api = kube::Api::namespaced(client.clone(), ns);
+ let jr = crate::crd::JsonResource::new(Default::default(), body.clone());
+ match api.get(name).await {
+ Ok(_) => {
+ let _ = api
+ .replace(name, &kube::api::PostParams::default(), &jr)
+ .await?;
+ }
+ Err(kube::Error::Api(e)) if e.code == 404 => {
+ let _ = api.create(&kube::api::PostParams::default(), &jr).await?;
+ }
+ Err(e) => return Err(e),
+ }
+ Ok(())
+}
+
+pub(crate) async fn patch_status(
+ client: &kube::Client,
+ ns: &str,
+ name: &str,
+ body: &Value,
+) -> Result<(), kube::Error> {
+ let api: kube::Api = kube::Api::namespaced(client.clone(), ns);
+ let _ = api
+ .patch_status(
+ name,
+ &kube::api::PatchParams::default(),
+ &kube::api::Patch::Merge(body),
+ )
+ .await?;
+ Ok(())
+}
diff --git a/apps/operator/src/controller/email_worker.rs b/apps/operator/src/controller/email_worker.rs
new file mode 100644
index 0000000..e05a8e2
--- /dev/null
+++ b/apps/operator/src/controller/email_worker.rs
@@ -0,0 +1,68 @@
+//! Controller for the `EmailWorker` CRD — Deployment only.
+
+use crate::context::ReconcileState;
+use crate::controller::app::{apply_deployment, patch_status};
+use crate::controller::helpers::{child_meta, env_var_to_json, merge_env, owner_ref, query_deployment_status, std_labels};
+use crate::crd::{EmailWorker, EmailWorkerSpec};
+use serde_json::{Value, json};
+use std::sync::Arc;
+
+pub async fn reconcile(ew: Arc, ctx: Arc) -> Result<(), kube::Error> {
+ let ns = ew.metadata.namespace.as_deref().unwrap_or("default");
+ let name = ew.metadata.name.as_deref().unwrap_or("");
+ let spec = &ew.spec;
+ let client = &ctx.client;
+
+ let or = owner_ref(&ew.metadata, &ew.api_version, &ew.kind);
+ let labels = std_labels();
+
+ let deployment = build_deployment(ns, name, spec, &or, &labels);
+ apply_deployment(client, ns, name, &deployment).await?;
+
+ let (ready_replicas, phase) = query_deployment_status(client, ns, name).await?;
+ let status = json!({ "status": { "readyReplicas": ready_replicas, "phase": phase } });
+ patch_status::(client, ns, name, &status).await?;
+
+ Ok(())
+}
+
+fn build_deployment(
+ ns: &str,
+ name: &str,
+ spec: &EmailWorkerSpec,
+ or: &crate::crd::OwnerReference,
+ labels: &std::collections::BTreeMap,
+) -> Value {
+ let env = merge_env(&[], &spec.env);
+ let image = if spec.image.is_empty() {
+ "myapp/email-worker:latest".to_string()
+ } else {
+ spec.image.clone()
+ };
+ let pull = if spec.image_pull_policy.is_empty() {
+ "IfNotPresent".to_string()
+ } else {
+ spec.image_pull_policy.clone()
+ };
+ let resources = super::app::build_resources(&spec.resources);
+
+ json!({
+ "metadata": child_meta(name, ns, or, labels.clone()),
+ "spec": {
+ "replicas": 1,
+ "selector": { "matchLabels": labels },
+ "template": {
+ "metadata": { "labels": labels.clone() },
+ "spec": {
+ "containers": [{
+ "name": "email-worker",
+ "image": image,
+ "env": env.iter().map(env_var_to_json).collect::>(),
+ "imagePullPolicy": pull,
+ "resources": resources,
+ }]
+ }
+ }
+ }
+ })
+}
diff --git a/apps/operator/src/controller/git_hook.rs b/apps/operator/src/controller/git_hook.rs
new file mode 100644
index 0000000..312a59a
--- /dev/null
+++ b/apps/operator/src/controller/git_hook.rs
@@ -0,0 +1,137 @@
+//! Controller for the `GitHook` CRD — Deployment + ConfigMap.
+
+use crate::context::ReconcileState;
+use crate::controller::app::{apply_deployment, patch_status};
+use crate::controller::helpers::{child_meta, env_var_to_json, merge_env, owner_ref, query_deployment_status, std_labels};
+use crate::crd::{GitHook, GitHookSpec, JsonResource};
+use serde_json::{Value, json};
+use std::sync::Arc;
+use tracing::info;
+
+pub async fn reconcile(gh: Arc, ctx: Arc) -> Result<(), kube::Error> {
+ let ns = gh.metadata.namespace.as_deref().unwrap_or("default");
+ let name = gh.metadata.name.as_deref().unwrap_or("");
+ let spec = &gh.spec;
+ let client = &ctx.client;
+
+ let or = owner_ref(&gh.metadata, &gh.api_version, &gh.kind);
+ let labels = std_labels();
+ let cm_name = format!("{}-config", name);
+
+ // ---- ConfigMap ----
+ let configmap = build_configmap(ns, &cm_name, &or, &labels);
+ apply_configmap(client, ns, &cm_name, &configmap).await?;
+
+ // ---- Deployment ----
+ let deployment = build_deployment(ns, name, &cm_name, spec, &or, &labels);
+ apply_deployment(client, ns, name, &deployment).await?;
+
+ let (ready_replicas, phase) = query_deployment_status(client, ns, name).await?;
+ let status = json!({ "status": { "readyReplicas": ready_replicas, "phase": phase } });
+ patch_status::(client, ns, name, &status).await?;
+
+ Ok(())
+}
+
+fn build_configmap(
+ ns: &str,
+ cm_name: &str,
+ or: &crate::crd::OwnerReference,
+ labels: &std::collections::BTreeMap,
+) -> Value {
+ let pool_config = serde_yaml::to_string(&serde_json::json!({
+ "max_concurrent": 8,
+ "cpu_threshold": 80.0,
+ "redis_list_prefix": "{hook}",
+ "redis_log_channel": "hook:logs",
+ "redis_block_timeout_secs": 5,
+ "redis_max_retries": 3,
+ }))
+ .unwrap_or_default();
+
+ json!({
+ "metadata": child_meta(cm_name, ns, or, labels.clone()),
+ "data": {
+ "pool.yaml": pool_config
+ }
+ })
+}
+
+fn build_deployment(
+ ns: &str,
+ name: &str,
+ cm_name: &str,
+ spec: &GitHookSpec,
+ or: &crate::crd::OwnerReference,
+ labels: &std::collections::BTreeMap,
+) -> Value {
+ let env = merge_env(&[], &spec.env);
+ let image = if spec.image.is_empty() {
+ "myapp/git-hook:latest".to_string()
+ } else {
+ spec.image.clone()
+ };
+ let pull = if spec.image_pull_policy.is_empty() {
+ "IfNotPresent".to_string()
+ } else {
+ spec.image_pull_policy.clone()
+ };
+ let resources = super::app::build_resources(&spec.resources);
+
+ // Add WORKER_ID env
+ let worker_id = spec
+ .worker_id
+ .clone()
+ .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
+ let mut env_vars: Vec = env.iter().map(env_var_to_json).collect();
+ env_vars.push(json!({ "name": "HOOK_POOL_WORKER_ID", "value": worker_id }));
+
+ json!({
+ "metadata": child_meta(name, ns, or, labels.clone()),
+ "spec": {
+ "replicas": 1,
+ "selector": { "matchLabels": labels },
+ "template": {
+ "metadata": { "labels": labels.clone() },
+ "spec": {
+ "containers": [{
+ "name": "git-hook",
+ "image": image,
+ "env": env_vars,
+ "imagePullPolicy": pull,
+ "resources": resources,
+ "volumeMounts": [{ "name": "hook-config", "mountPath": "/config" }]
+ }],
+ "volumes": [{
+ "name": "hook-config",
+ "configMap": { "name": cm_name }
+ }]
+ }
+ }
+ }
+ })
+}
+
+async fn apply_configmap(
+ client: &kube::Client,
+ ns: &str,
+ name: &str,
+ body: &Value,
+) -> Result<(), kube::Error> {
+ let api: kube::Api = kube::Api::namespaced(client.clone(), ns);
+ let jr = JsonResource::new(Default::default(), body.clone());
+ match api.get(name).await {
+ Ok(_) => {
+ let _ = api
+ .replace(name, &kube::api::PostParams::default(), &jr)
+ .await?;
+ Ok(())
+ }
+ Err(kube::Error::Api(e)) if e.code == 404 => {
+ info!(name, ns, "creating git-hook configmap");
+ let _ = api.create(&kube::api::PostParams::default(), &jr).await?;
+ Ok(())
+ }
+ Err(e) => Err(e),
+ }
+}
diff --git a/apps/operator/src/controller/gitserver.rs b/apps/operator/src/controller/gitserver.rs
new file mode 100644
index 0000000..976bab8
--- /dev/null
+++ b/apps/operator/src/controller/gitserver.rs
@@ -0,0 +1,164 @@
+//! Controller for the `GitServer` CRD — Deployment + HTTP Svc + SSH Svc + PVC.
+
+use crate::context::ReconcileState;
+use crate::controller::app::{apply_deployment, apply_service, patch_status};
+use crate::controller::helpers::{child_meta, env_var_to_json, merge_env, owner_ref, query_deployment_status, std_labels};
+use crate::crd::{GitServer, GitServerSpec};
+use serde_json::{Value, json};
+use std::sync::Arc;
+use tracing::info;
+
+pub async fn reconcile(gs: Arc, ctx: Arc) -> Result<(), kube::Error> {
+ let ns = gs.metadata.namespace.as_deref().unwrap_or("default");
+ let name = gs.metadata.name.as_deref().unwrap_or("");
+ let spec = &gs.spec;
+ let client = &ctx.client;
+
+ let or = owner_ref(&gs.metadata, &gs.api_version, &gs.kind);
+ let labels = std_labels();
+
+ // ---- PVC ----
+ let pvc = build_pvc(ns, name, spec, &or, &labels);
+ apply_pvc(client, ns, &format!("{}-repos", name), &pvc).await?;
+
+ // ---- Deployment ----
+ let deployment = build_deployment(ns, name, spec, &or, &labels);
+ apply_deployment(client, ns, name, &deployment).await?;
+
+ // ---- HTTP Service ----
+ let http_svc = build_http_service(ns, name, spec, &or, &labels);
+ apply_service(client, ns, &format!("{}-http", name), &http_svc).await?;
+
+ // ---- SSH Service ----
+ let ssh_svc = build_ssh_service(ns, name, spec, &or, &labels);
+ apply_service(client, ns, &format!("{}-ssh", name), &ssh_svc).await?;
+
+ // ---- Status ----
+ let (ready_replicas, phase) = query_deployment_status(client, ns, name).await?;
+ let status = json!({ "status": { "readyReplicas": ready_replicas, "phase": phase } });
+ patch_status::(client, ns, name, &status).await?;
+
+ Ok(())
+}
+
+fn build_deployment(
+ ns: &str,
+ name: &str,
+ spec: &GitServerSpec,
+ or: &crate::crd::OwnerReference,
+ labels: &std::collections::BTreeMap,
+) -> Value {
+ let env = merge_env(&[], &spec.env);
+ let image = if spec.image.is_empty() {
+ "myapp/gitserver:latest".to_string()
+ } else {
+ spec.image.clone()
+ };
+ let pull = if spec.image_pull_policy.is_empty() {
+ "IfNotPresent".to_string()
+ } else {
+ spec.image_pull_policy.clone()
+ };
+ let resources = super::app::build_resources(&spec.resources);
+
+ json!({
+ "metadata": child_meta(name, ns, or, labels.clone()),
+ "spec": {
+ "replicas": 1,
+ "selector": { "matchLabels": labels },
+ "template": {
+ "metadata": { "labels": labels.clone() },
+ "spec": {
+ "containers": [{
+ "name": "gitserver",
+ "image": image,
+ "ports": [
+ { "name": "http", "containerPort": spec.http_port },
+ { "name": "ssh", "containerPort": spec.ssh_port }
+ ],
+ "env": env.iter().map(env_var_to_json).collect::>(),
+ "imagePullPolicy": pull,
+ "resources": resources,
+ "volumeMounts": [{ "name": "git-repos", "mountPath": "/data/repos" }]
+ }],
+ "volumes": [{
+ "name": "git-repos",
+ "persistentVolumeClaim": { "claimName": format!("{}-repos", name) }
+ }]
+ }
+ }
+ }
+ })
+}
+
+fn build_http_service(
+ ns: &str,
+ name: &str,
+ spec: &GitServerSpec,
+ or: &crate::crd::OwnerReference,
+ labels: &std::collections::BTreeMap,
+) -> Value {
+ json!({
+ "metadata": child_meta(&format!("{}-http", name), ns, or, labels.clone()),
+ "spec": {
+ "ports": [{ "port": spec.http_port, "targetPort": spec.http_port, "name": "http" }],
+ "selector": labels.clone(),
+ "type": "ClusterIP"
+ }
+ })
+}
+
+fn build_ssh_service(
+ ns: &str,
+ name: &str,
+ spec: &GitServerSpec,
+ or: &crate::crd::OwnerReference,
+ labels: &std::collections::BTreeMap,
+) -> Value {
+ json!({
+ "metadata": child_meta(&format!("{}-ssh", name), ns, or, labels.clone()),
+ "spec": {
+ "ports": [{ "port": spec.ssh_port, "targetPort": spec.ssh_port, "name": "ssh" }],
+ "selector": labels.clone(),
+ "type": spec.ssh_service_type
+ }
+ })
+}
+
+fn build_pvc(
+ ns: &str,
+ name: &str,
+ spec: &GitServerSpec,
+ or: &crate::crd::OwnerReference,
+ labels: &std::collections::BTreeMap,
+) -> Value {
+ json!({
+ "metadata": child_meta(&format!("{}-repos", name), ns, or, labels.clone()),
+ "spec": {
+ "accessModes": ["ReadWriteOnce"],
+ "resources": { "requests": { "storage": spec.storage_size } }
+ }
+ })
+}
+
+async fn apply_pvc(
+ client: &kube::Client,
+ ns: &str,
+ name: &str,
+ body: &Value,
+) -> Result<(), kube::Error> {
+ let api: kube::Api = kube::Api::namespaced(client.clone(), ns);
+ let jr = crate::crd::JsonResource::new(Default::default(), body.clone());
+ match api.get(name).await {
+ Ok(_) => {
+ /* already exists, don't replace PVC */
+ Ok(())
+ }
+ Err(kube::Error::Api(e)) if e.code == 404 => {
+ info!(name, ns, "creating gitserver pvc");
+ let _ = api.create(&kube::api::PostParams::default(), &jr).await?;
+ Ok(())
+ }
+ Err(e) => Err(e),
+ }
+}
diff --git a/apps/operator/src/controller/helpers.rs b/apps/operator/src/controller/helpers.rs
new file mode 100644
index 0000000..2ac6d6a
--- /dev/null
+++ b/apps/operator/src/controller/helpers.rs
@@ -0,0 +1,96 @@
+//! Shared helpers for building Kubernetes child resources as JSON objects.
+
+use crate::crd::{EnvVar, K8sObjectMeta, OwnerReference};
+
+/// Query a Deployment's actual status and derive the CR's phase.
+pub async fn query_deployment_status(
+ client: &kube::Client,
+ ns: &str,
+ name: &str,
+) -> Result<(i32, String), kube::Error> {
+ use k8s_openapi::api::apps::v1::Deployment;
+
+ let api: kube::Api = kube::Api::namespaced(client.clone(), ns);
+ match api.get(name).await {
+ Ok(d) => {
+ let ready = d.status.as_ref().and_then(|s| s.ready_replicas).unwrap_or(0);
+ let phase = if ready > 0 { "Running" } else { "Pending" };
+ Ok((ready, phase.to_string()))
+ }
+ Err(kube::Error::Api(e)) if e.code == 404 => Ok((0, "Pending".to_string())),
+ Err(e) => Err(e),
+ }
+}
+
+/// Labels applied to every child resource.
+pub fn std_labels() -> std::collections::BTreeMap {
+ let mut m = std::collections::BTreeMap::new();
+ m.insert(
+ "app.kubernetes.io/managed-by".to_string(),
+ "code-operator".to_string(),
+ );
+ m.insert(
+ "app.kubernetes.io/part-of".to_string(),
+ "code-system".to_string(),
+ );
+ m
+}
+
+pub fn child_meta(
+ name: &str,
+ namespace: &str,
+ owner: &OwnerReference,
+ labels: std::collections::BTreeMap,
+) -> K8sObjectMeta {
+ K8sObjectMeta {
+ name: Some(name.to_string()),
+ namespace: Some(namespace.to_string()),
+ labels: Some(labels),
+ owner_references: Some(vec![owner.clone().into()]),
+ ..Default::default()
+ }
+}
+
+pub fn owner_ref(parent: &K8sObjectMeta, api_version: &str, kind: &str) -> OwnerReference {
+ OwnerReference {
+ api_version: api_version.to_string(),
+ kind: kind.to_string(),
+ name: parent.name.clone().unwrap_or_default(),
+ uid: parent.uid.clone().unwrap_or_default(),
+ controller: Some(true),
+ block_owner_deletion: Some(true),
+ }
+}
+
+/// Merge env vars (global first, then local overrides).
+pub fn merge_env(global: &[EnvVar], local: &[EnvVar]) -> Vec {
+ use std::collections::BTreeMap;
+ let mut map: BTreeMap = global
+ .iter()
+ .cloned()
+ .map(|e| (e.name.clone(), e))
+ .collect();
+ for e in local {
+ map.insert(e.name.clone(), e.clone());
+ }
+ map.into_values().collect()
+}
+
+pub fn env_var_to_json(e: &EnvVar) -> serde_json::Value {
+ use serde_json::json;
+ let mut m = json!({ "name": e.name });
+ if let Some(ref v) = e.value {
+ m["value"] = json!(v);
+ }
+ if let Some(ref src) = e.value_from {
+ if let Some(ref sr) = src.secret_ref {
+ m["valueFrom"] = json!({
+ "secretRef": {
+ "name": sr.secret_name,
+ "key": sr.secret_key,
+ }
+ });
+ }
+ }
+ m
+}
diff --git a/apps/operator/src/controller/migrate.rs b/apps/operator/src/controller/migrate.rs
new file mode 100644
index 0000000..47ce299
--- /dev/null
+++ b/apps/operator/src/controller/migrate.rs
@@ -0,0 +1,171 @@
+//! Controller for the `Migrate` CRD — creates a one-shot Job on reconcile.
+//!
+//! The Job is re-created on every reconcile (idempotent). Once the Job
+//! succeeds, the Migrate status is patched to "Completed".
+
+use crate::context::ReconcileState;
+use crate::controller::helpers::{child_meta, env_var_to_json, merge_env, owner_ref, std_labels};
+use crate::crd::{JsonResource, K8sObjectMeta, Migrate, MigrateSpec};
+use chrono::Utc;
+use serde_json::{Value, json};
+use std::sync::Arc;
+use tracing::info;
+
+pub async fn reconcile(mig: Arc, ctx: Arc) -> Result<(), kube::Error> {
+ let ns = mig.metadata.namespace.as_deref().unwrap_or("default");
+ let name = mig.metadata.name.as_deref().unwrap_or("");
+ let spec = &mig.spec;
+ let client = &ctx.client;
+
+ let or = owner_ref(&mig.metadata, &mig.api_version, &mig.kind);
+ let labels = std_labels();
+
+ let job_meta = child_meta(name, ns, &or, labels.clone());
+ let job = build_job(spec, job_meta, &labels);
+
+ // Use JsonResource for Job create/replace (spec part)
+ let jobs_api: kube::Api = kube::Api::namespaced(client.clone(), ns);
+ match jobs_api.get(name).await {
+ Ok(_) => {
+ info!(name, ns, "replacing migrate job");
+ let _ = jobs_api
+ .replace(name, &kube::api::PostParams::default(), &job)
+ .await?;
+ }
+ Err(kube::Error::Api(e)) if e.code == 404 => {
+ info!(name, ns, "creating migrate job");
+ let _ = jobs_api.create(&kube::api::PostParams::default(), &job).await?;
+ }
+ Err(e) => return Err(e),
+ }
+
+ // Query real Job status via k8s-openapi (reads status subresource)
+ let job_status = query_job_status(client, ns, name).await?;
+ patch_migrate_status_from_job(client, ns, name, &job_status).await?;
+
+ Ok(())
+}
+
+/// Query actual Job status and derive Migrate phase + timestamps.
+async fn query_job_status(
+ client: &kube::Client,
+ ns: &str,
+ name: &str,
+) -> Result {
+ use k8s_openapi::api::batch::v1::Job;
+ let api: kube::Api = kube::Api::namespaced(client.clone(), ns);
+ match api.get(name).await {
+ Ok(job) => {
+ let status = job.status.as_ref();
+ let succeeded = status.and_then(|s| s.succeeded).unwrap_or(0);
+ let failed = status.and_then(|s| s.failed).unwrap_or(0);
+ let active = status.and_then(|s| s.active).unwrap_or(0);
+
+ let phase = if succeeded > 0 {
+ "Completed"
+ } else if failed > 0 {
+ "Failed"
+ } else if active > 0 {
+ "Running"
+ } else {
+ "Pending"
+ };
+
+ let start_time = status.and_then(|s| s.start_time.as_ref()).map(|t| t.to_string());
+ let completion_time = status.and_then(|s| s.completion_time.as_ref()).map(|t| t.to_string());
+
+ Ok(JobStatusResult { phase, start_time, completion_time })
+ }
+ Err(kube::Error::Api(e)) if e.code == 404 => {
+ Ok(JobStatusResult { phase: "Pending".to_string(), start_time: None, completion_time: None })
+ }
+ Err(e) => Err(e),
+ }
+}
+
+struct JobStatusResult {
+ phase: String,
+ start_time: Option,
+ completion_time: Option,
+}
+
+async fn patch_migrate_status_from_job(
+ client: &kube::Client,
+ ns: &str,
+ name: &str,
+ job: &JobStatusResult,
+) -> Result<(), kube::Error> {
+ let api: kube::Api = kube::Api::namespaced(client.clone(), ns);
+ let mut status_obj = json!({ "phase": job.phase });
+ if let Some(ref st) = job.start_time {
+ status_obj["startTime"] = json!(st);
+ }
+ if let Some(ref ct) = job.completion_time {
+ status_obj["completionTime"] = json!(ct);
+ }
+ let patch = json!({ "status": status_obj });
+ let _ = api
+ .patch_status(
+ name,
+ &kube::api::PatchParams::default(),
+ &kube::api::Patch::Merge(&patch),
+ )
+ .await?;
+ Ok(())
+}
+
+fn build_job(
+ spec: &MigrateSpec,
+ meta: K8sObjectMeta,
+ labels: &std::collections::BTreeMap,
+) -> JsonResource {
+ let image = if spec.image.is_empty() {
+ "myapp/migrate:latest".to_string()
+ } else {
+ spec.image.clone()
+ };
+
+ let env = merge_env(&[], &spec.env);
+ let env_vars: Vec = env.iter().map(env_var_to_json).collect();
+
+ let cmd_parts: Vec<&str> = spec.command.split_whitespace().collect();
+ let cmd: Vec<&str> = if cmd_parts.is_empty() {
+ vec!["up"]
+ } else {
+ cmd_parts
+ };
+
+ let now = Utc::now().to_rfc3339();
+
+ let mut meta_with_anno = meta.clone();
+ meta_with_anno.annotations = Some(std::collections::BTreeMap::from([(
+ "code.dev/last-migrate".to_string(),
+ now,
+ )]));
+
+ let body = json!({
+ "metadata": meta_with_anno,
+ "spec": {
+ "backoffLimit": spec.backoff_limit,
+ "ttlSecondsAfterFinished": 300,
+ "template": {
+ "metadata": {
+ "labels": labels.clone()
+ },
+ "spec": {
+ "restartPolicy": "Never",
+ "containers": [{
+ "name": "migrate",
+ "image": image,
+ "command": ["/app/migrate"],
+ "args": cmd,
+ "env": env_vars,
+ "imagePullPolicy": "IfNotPresent"
+ }]
+ }
+ }
+ }
+ });
+
+ JsonResource::new(meta, body)
+}
diff --git a/apps/operator/src/controller/mod.rs b/apps/operator/src/controller/mod.rs
new file mode 100644
index 0000000..bac5dd6
--- /dev/null
+++ b/apps/operator/src/controller/mod.rs
@@ -0,0 +1,188 @@
+//! Kubernetes Controllers — one per CRD type.
+
+pub mod app;
+pub mod email_worker;
+pub mod git_hook;
+pub mod gitserver;
+pub mod helpers;
+pub mod migrate;
+
+use crate::context::ReconcileCtx;
+use crate::crd::{App, EmailWorker, GitHook, GitServer, Migrate};
+use futures::StreamExt;
+use kube::runtime::{Controller, controller::Action};
+use std::sync::Arc;
+
+fn error_policy(
+ obj: Arc,
+ err: &kube::Error,
+ _: Arc,
+) -> Action {
+ tracing::error!(?obj, %err, "reconcile error");
+ Action::await_change()
+}
+
+/// Start the App controller.
+pub async fn start_app(client: kube::Client, ctx: Arc) -> anyhow::Result<()> {
+ Controller::new(kube::Api::::all(client.clone()), Default::default())
+ .owns::(
+ kube::Api::all(client.clone()),
+ Default::default(),
+ )
+ .owns::(
+ kube::Api::all(client.clone()),
+ Default::default(),
+ )
+ .run(
+ |o, c| {
+ let c = c.clone();
+ async move {
+ app::reconcile(o, c).await?;
+ Ok::<_, kube::Error>(Action::await_change())
+ }
+ },
+ error_policy,
+ ctx.clone(),
+ )
+ .for_each(|r| async move {
+ if let Err(e) = r {
+ tracing::error!(%e, "app controller stream error");
+ }
+ })
+ .await;
+
+ Ok(())
+}
+
+/// Start the GitServer controller.
+pub async fn start_gitserver(client: kube::Client, ctx: Arc) -> anyhow::Result<()> {
+ Controller::new(
+ kube::Api::::all(client.clone()),
+ Default::default(),
+ )
+ .owns::(
+ kube::Api::all(client.clone()),
+ Default::default(),
+ )
+ .owns::(kube::Api::all(client.clone()), Default::default())
+ .owns::(
+ kube::Api::all(client.clone()),
+ Default::default(),
+ )
+ .run(
+ |o, c| {
+ let c = c.clone();
+ async move {
+ gitserver::reconcile(o, c).await?;
+ Ok::<_, kube::Error>(Action::await_change())
+ }
+ },
+ error_policy,
+ ctx.clone(),
+ )
+ .for_each(|r| async move {
+ if let Err(e) = r {
+ tracing::error!(%e, "gitserver controller stream error");
+ }
+ })
+ .await;
+
+ Ok(())
+}
+
+/// Start the EmailWorker controller.
+pub async fn start_email_worker(
+ client: kube::Client,
+ ctx: Arc,
+) -> anyhow::Result<()> {
+ Controller::new(
+ kube::Api::::all(client.clone()),
+ Default::default(),
+ )
+ .owns::(
+ kube::Api::all(client.clone()),
+ Default::default(),
+ )
+ .run(
+ |o, c| {
+ let c = c.clone();
+ async move {
+ email_worker::reconcile(o, c).await?;
+ Ok::<_, kube::Error>(Action::await_change())
+ }
+ },
+ error_policy,
+ ctx.clone(),
+ )
+ .for_each(|r| async move {
+ if let Err(e) = r {
+ tracing::error!(%e, "email_worker controller stream error");
+ }
+ })
+ .await;
+
+ Ok(())
+}
+
+/// Start the GitHook controller.
+pub async fn start_git_hook(client: kube::Client, ctx: Arc) -> anyhow::Result<()> {
+ Controller::new(
+ kube::Api::::all(client.clone()),
+ Default::default(),
+ )
+ .owns::(
+ kube::Api::all(client.clone()),
+ Default::default(),
+ )
+ .owns::(
+ kube::Api::all(client.clone()),
+ Default::default(),
+ )
+ .run(
+ |o, c| {
+ let c = c.clone();
+ async move {
+ git_hook::reconcile(o, c).await?;
+ Ok::<_, kube::Error>(Action::await_change())
+ }
+ },
+ error_policy,
+ ctx.clone(),
+ )
+ .for_each(|r| async move {
+ if let Err(e) = r {
+ tracing::error!(%e, "git_hook controller stream error");
+ }
+ })
+ .await;
+
+ Ok(())
+}
+
+/// Start the Migrate controller.
+pub async fn start_migrate(client: kube::Client, ctx: Arc) -> anyhow::Result<()> {
+ Controller::new(
+ kube::Api::::all(client.clone()),
+ Default::default(),
+ )
+ .owns::(kube::Api::all(client.clone()), Default::default())
+ .run(
+ |o, c| {
+ let c = c.clone();
+ async move {
+ migrate::reconcile(o, c).await?;
+ Ok::<_, kube::Error>(Action::await_change())
+ }
+ },
+ error_policy,
+ ctx.clone(),
+ )
+ .for_each(|r| async move {
+ if let Err(e) = r {
+ tracing::error!(%e, "migrate controller stream error");
+ }
+ })
+ .await;
+
+ Ok(())
+}
diff --git a/apps/operator/src/crd.rs b/apps/operator/src/crd.rs
new file mode 100644
index 0000000..518b3d9
--- /dev/null
+++ b/apps/operator/src/crd.rs
@@ -0,0 +1,581 @@
+//! Custom Resource Definitions (CRDs) — plain serde types.
+//!
+//! API Group: `code.dev`
+//!
+//! The operator watches these resources using `kube::Api::::all(client)`.
+//! Reconcile is triggered on every change to any instance of these types.
+
+use k8s_openapi::apimachinery::pkg::apis::meta::v1::{
+ ObjectMeta, OwnerReference as K8sOwnerReference,
+};
+use kube::Resource;
+use serde::{Deserialize, Serialize};
+use std::borrow::Cow;
+
+// ---------------------------------------------------------------------------
+// A dynamic Resource impl for serde_json::Value — lets us use kube::Api
+// ---------------------------------------------------------------------------
+
+/// JsonResource wraps serde_json::Value and implements Resource so we can use
+/// `kube::Api` for arbitrary child-resource API calls.
+/// The metadata field is kept separate to satisfy the Resource::meta() bound.
+#[derive(Clone, Debug, Default)]
+pub struct JsonResource {
+ meta: ObjectMeta,
+ body: serde_json::Value,
+}
+
+impl JsonResource {
+ pub fn new(meta: ObjectMeta, body: serde_json::Value) -> Self {
+ JsonResource { meta, body }
+ }
+}
+
+impl std::ops::Deref for JsonResource {
+ type Target = serde_json::Value;
+ fn deref(&self) -> &serde_json::Value {
+ &self.body
+ }
+}
+
+impl serde::Serialize for JsonResource {
+ fn serialize(&self, s: S) -> Result {
+ self.body.serialize(s)
+ }
+}
+
+impl<'de> serde::Deserialize<'de> for JsonResource {
+ fn deserialize>(d: D) -> Result {
+ let body = serde_json::Value::deserialize(d)?;
+ let meta = body
+ .get("metadata")
+ .and_then(|m| serde_json::from_value(m.clone()).ok())
+ .unwrap_or_default();
+ Ok(JsonResource { meta, body })
+ }
+}
+
+impl Resource for JsonResource {
+ type DynamicType = ();
+ type Scope = k8s_openapi::NamespaceResourceScope;
+ fn kind(_: &()) -> Cow<'_, str> {
+ Cow::Borrowed("Object")
+ }
+ fn group(_: &()) -> Cow<'_, str> {
+ Cow::Borrowed("")
+ }
+ fn version(_: &()) -> Cow<'_, str> {
+ Cow::Borrowed("v1")
+ }
+ fn plural(_: &()) -> Cow<'_, str> {
+ Cow::Borrowed("objects")
+ }
+ fn meta(&self) -> &ObjectMeta {
+ &self.meta
+ }
+ fn meta_mut(&mut self) -> &mut ObjectMeta {
+ &mut self.meta
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Shared types
+// ---------------------------------------------------------------------------
+
+/// EnvVar with optional secret reference.
+#[derive(Debug, Clone, Serialize, Deserialize, Default)]
+pub struct EnvVar {
+ pub name: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub value: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub value_from: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Default)]
+pub struct EnvVarSource {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub secret_ref: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Default)]
+pub struct SecretEnvVar {
+ pub name: String,
+ pub secret_name: String,
+ pub secret_key: String,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Default)]
+pub struct ResourceRequirements {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub requests: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub limits: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Default)]
+pub struct ResourceList {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub cpu: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub memory: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Default)]
+pub struct Probe {
+ #[serde(default = "default_port")]
+ pub port: i32,
+ #[serde(default = "default_path")]
+ pub path: String,
+ #[serde(default = "default_initial_delay")]
+ pub initial_delay_seconds: i32,
+}
+
+fn default_port() -> i32 {
+ 8080
+}
+fn default_path() -> String {
+ "/health".to_string()
+}
+fn default_initial_delay() -> i32 {
+ 5
+}
+
+// ---------------------------------------------------------------------------
+// App CRD
+// ---------------------------------------------------------------------------
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct AppSpec {
+ #[serde(default = "default_app_image")]
+ pub image: String,
+ #[serde(default = "default_replicas")]
+ pub replicas: i32,
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
+ pub env: Vec,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub resources: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub liveness_probe: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub readiness_probe: Option,
+ #[serde(default)]
+ pub image_pull_policy: String,
+}
+
+fn default_app_image() -> String {
+ "myapp/app:latest".to_string()
+}
+fn default_replicas() -> i32 {
+ 3
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Default)]
+pub struct AppStatus {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub ready_replicas: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub phase: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct App {
+ pub api_version: String,
+ pub kind: String,
+ pub metadata: K8sObjectMeta,
+ pub spec: AppSpec,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub status: Option,
+}
+
+impl App {
+ pub fn api_group() -> &'static str {
+ "code.dev"
+ }
+ pub fn version() -> &'static str {
+ "v1"
+ }
+ pub fn plural() -> &'static str {
+ "apps"
+ }
+}
+
+impl Resource for App {
+ type DynamicType = ();
+ type Scope = k8s_openapi::NamespaceResourceScope;
+ fn kind(_: &Self::DynamicType) -> Cow<'_, str> {
+ Cow::Borrowed("App")
+ }
+ fn group(_: &Self::DynamicType) -> Cow<'_, str> {
+ Cow::Borrowed("code.dev")
+ }
+ fn version(_: &Self::DynamicType) -> Cow<'_, str> {
+ Cow::Borrowed("v1")
+ }
+ fn plural(_: &Self::DynamicType) -> Cow<'_, str> {
+ Cow::Borrowed("apps")
+ }
+ fn meta(&self) -> &ObjectMeta {
+ &self.metadata
+ }
+ fn meta_mut(&mut self) -> &mut ObjectMeta {
+ &mut self.metadata
+ }
+}
+
+// ---------------------------------------------------------------------------
+// GitServer CRD
+// ---------------------------------------------------------------------------
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct GitServerSpec {
+ #[serde(default = "default_gitserver_image")]
+ pub image: String,
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
+ pub env: Vec,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub resources: Option,
+ #[serde(default = "default_ssh_service_type")]
+ pub ssh_service_type: String,
+ #[serde(default = "default_storage_size")]
+ pub storage_size: String,
+ #[serde(default)]
+ pub image_pull_policy: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub ssh_domain: Option,
+ #[serde(default = "default_ssh_port")]
+ pub ssh_port: i32,
+ #[serde(default = "default_http_port")]
+ pub http_port: i32,
+}
+
+fn default_gitserver_image() -> String {
+ "myapp/gitserver:latest".to_string()
+}
+fn default_ssh_service_type() -> String {
+ "NodePort".to_string()
+}
+fn default_storage_size() -> String {
+ "10Gi".to_string()
+}
+fn default_ssh_port() -> i32 {
+ 22
+}
+fn default_http_port() -> i32 {
+ 8022
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Default)]
+pub struct GitServerStatus {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub ready_replicas: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub phase: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct GitServer {
+ pub api_version: String,
+ pub kind: String,
+ pub metadata: K8sObjectMeta,
+ pub spec: GitServerSpec,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub status: Option,
+}
+
+impl GitServer {
+ pub fn api_group() -> &'static str {
+ "code.dev"
+ }
+ pub fn version() -> &'static str {
+ "v1"
+ }
+ pub fn plural() -> &'static str {
+ "gitservers"
+ }
+}
+
+impl Resource for GitServer {
+ type DynamicType = ();
+ type Scope = k8s_openapi::NamespaceResourceScope;
+ fn kind(_: &Self::DynamicType) -> Cow<'_, str> {
+ Cow::Borrowed("GitServer")
+ }
+ fn group(_: &Self::DynamicType) -> Cow<'_, str> {
+ Cow::Borrowed("code.dev")
+ }
+ fn version(_: &Self::DynamicType) -> Cow<'_, str> {
+ Cow::Borrowed("v1")
+ }
+ fn plural(_: &Self::DynamicType) -> Cow<'_, str> {
+ Cow::Borrowed("gitservers")
+ }
+ fn meta(&self) -> &ObjectMeta {
+ &self.metadata
+ }
+ fn meta_mut(&mut self) -> &mut ObjectMeta {
+ &mut self.metadata
+ }
+}
+
+// ---------------------------------------------------------------------------
+// EmailWorker CRD
+// ---------------------------------------------------------------------------
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct EmailWorkerSpec {
+ #[serde(default = "default_email_image")]
+ pub image: String,
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
+ pub env: Vec,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub resources: Option,
+ #[serde(default)]
+ pub image_pull_policy: String,
+}
+
+fn default_email_image() -> String {
+ "myapp/email-worker:latest".to_string()
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Default)]
+pub struct EmailWorkerStatus {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub ready_replicas: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub phase: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct EmailWorker {
+ pub api_version: String,
+ pub kind: String,
+ pub metadata: K8sObjectMeta,
+ pub spec: EmailWorkerSpec,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub status: Option,
+}
+
+impl EmailWorker {
+ pub fn api_group() -> &'static str {
+ "code.dev"
+ }
+ pub fn version() -> &'static str {
+ "v1"
+ }
+ pub fn plural() -> &'static str {
+ "emailworkers"
+ }
+}
+
+impl Resource for EmailWorker {
+ type DynamicType = ();
+ type Scope = k8s_openapi::NamespaceResourceScope;
+ fn kind(_: &Self::DynamicType) -> Cow<'_, str> {
+ Cow::Borrowed("EmailWorker")
+ }
+ fn group(_: &Self::DynamicType) -> Cow<'_, str> {
+ Cow::Borrowed("code.dev")
+ }
+ fn version(_: &Self::DynamicType) -> Cow<'_, str> {
+ Cow::Borrowed("v1")
+ }
+ fn plural(_: &Self::DynamicType) -> Cow<'_, str> {
+ Cow::Borrowed("emailworkers")
+ }
+ fn meta(&self) -> &ObjectMeta {
+ &self.metadata
+ }
+ fn meta_mut(&mut self) -> &mut ObjectMeta {
+ &mut self.metadata
+ }
+}
+
+// ---------------------------------------------------------------------------
+// GitHook CRD
+// ---------------------------------------------------------------------------
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct GitHookSpec {
+ #[serde(default = "default_githook_image")]
+ pub image: String,
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
+ pub env: Vec,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub resources: Option,
+ #[serde(default)]
+ pub image_pull_policy: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub worker_id: Option,
+}
+
+fn default_githook_image() -> String {
+ "myapp/git-hook:latest".to_string()
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Default)]
+pub struct GitHookStatus {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub ready_replicas: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub phase: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct GitHook {
+ pub api_version: String,
+ pub kind: String,
+ pub metadata: K8sObjectMeta,
+ pub spec: GitHookSpec,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub status: Option,
+}
+
+impl GitHook {
+ pub fn api_group() -> &'static str {
+ "code.dev"
+ }
+ pub fn version() -> &'static str {
+ "v1"
+ }
+ pub fn plural() -> &'static str {
+ "githooks"
+ }
+}
+
+impl Resource for GitHook {
+ type DynamicType = ();
+ type Scope = k8s_openapi::NamespaceResourceScope;
+ fn kind(_: &Self::DynamicType) -> Cow<'_, str> {
+ Cow::Borrowed("GitHook")
+ }
+ fn group(_: &Self::DynamicType) -> Cow<'_, str> {
+ Cow::Borrowed("code.dev")
+ }
+ fn version(_: &Self::DynamicType) -> Cow<'_, str> {
+ Cow::Borrowed("v1")
+ }
+ fn plural(_: &Self::DynamicType) -> Cow<'_, str> {
+ Cow::Borrowed("githooks")
+ }
+ fn meta(&self) -> &ObjectMeta {
+ &self.metadata
+ }
+ fn meta_mut(&mut self) -> &mut ObjectMeta {
+ &mut self.metadata
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Migrate CRD
+// ---------------------------------------------------------------------------
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct MigrateSpec {
+ #[serde(default = "default_migrate_image")]
+ pub image: String,
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
+ pub env: Vec,
+ #[serde(default = "default_migrate_cmd")]
+ pub command: String,
+ #[serde(default = "default_backoff_limit")]
+ pub backoff_limit: i32,
+}
+
+fn default_migrate_image() -> String {
+ "myapp/migrate:latest".to_string()
+}
+fn default_migrate_cmd() -> String {
+ "up".to_string()
+}
+fn default_backoff_limit() -> i32 {
+ 3
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, Default)]
+pub struct MigrateStatus {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub phase: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub start_time: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub completion_time: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct Migrate {
+ pub api_version: String,
+ pub kind: String,
+ pub metadata: K8sObjectMeta,
+ pub spec: MigrateSpec,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub status: Option,
+}
+
+impl Migrate {
+ pub fn api_group() -> &'static str {
+ "code.dev"
+ }
+ pub fn version() -> &'static str {
+ "v1"
+ }
+ pub fn plural() -> &'static str {
+ "migrates"
+ }
+}
+
+impl Resource for Migrate {
+ type DynamicType = ();
+ type Scope = k8s_openapi::NamespaceResourceScope;
+ fn kind(_: &Self::DynamicType) -> Cow<'_, str> {
+ Cow::Borrowed("Migrate")
+ }
+ fn group(_: &Self::DynamicType) -> Cow<'_, str> {
+ Cow::Borrowed("code.dev")
+ }
+ fn version(_: &Self::DynamicType) -> Cow<'_, str> {
+ Cow::Borrowed("v1")
+ }
+ fn plural(_: &Self::DynamicType) -> Cow<'_, str> {
+ Cow::Borrowed("migrates")
+ }
+ fn meta(&self) -> &ObjectMeta {
+ &self.metadata
+ }
+ fn meta_mut(&mut self) -> &mut ObjectMeta {
+ &mut self.metadata
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Shared K8s types — aligned with k8s-openapi for Resource trait compatibility
+// ---------------------------------------------------------------------------
+
+/// Type alias so K8sObjectMeta satisfies Resource::meta() -> &k8s_openapi::...::ObjectMeta.
+pub type K8sObjectMeta = ObjectMeta;
+
+/// OwnerReference compatible with k8s-openapi.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct OwnerReference {
+ pub api_version: String,
+ pub kind: String,
+ pub name: String,
+ pub uid: String,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub controller: Option,
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub block_owner_deletion: Option,
+}
+
+impl From for K8sOwnerReference {
+ fn from(o: OwnerReference) -> Self {
+ K8sOwnerReference {
+ api_version: o.api_version,
+ kind: o.kind,
+ name: o.name,
+ uid: o.uid,
+ controller: o.controller,
+ block_owner_deletion: o.block_owner_deletion,
+ }
+ }
+}
diff --git a/apps/operator/src/lib.rs b/apps/operator/src/lib.rs
new file mode 100644
index 0000000..2282475
--- /dev/null
+++ b/apps/operator/src/lib.rs
@@ -0,0 +1,3 @@
+pub mod context;
+pub mod controller;
+pub mod crd;
diff --git a/apps/operator/src/main.rs b/apps/operator/src/main.rs
new file mode 100644
index 0000000..8f6eae0
--- /dev/null
+++ b/apps/operator/src/main.rs
@@ -0,0 +1,100 @@
+//! Code System Kubernetes Operator
+//!
+//! Manages the lifecycle of: App, GitServer, EmailWorker, GitHook, Migrate CRDs.
+
+use operator::context::ReconcileCtx;
+use std::sync::Arc;
+use tracing::{Level, error, info};
+use tracing_subscriber::FmtSubscriber;
+
+#[tokio::main]
+async fn main() -> anyhow::Result<()> {
+ // ---- Logging ----
+ let log_level = std::env::var("OPERATOR_LOG_LEVEL").unwrap_or_else(|_| "info".to_string());
+ let level = match log_level.to_lowercase().as_str() {
+ "trace" => Level::TRACE,
+ "debug" => Level::DEBUG,
+ "info" => Level::INFO,
+ "warn" => Level::WARN,
+ "error" => Level::ERROR,
+ _ => Level::INFO,
+ };
+ FmtSubscriber::builder()
+ .with_max_level(level)
+ .with_target(false)
+ .with_thread_ids(false)
+ .with_file(true)
+ .with_line_number(true)
+ .compact()
+ .init();
+
+ let ctx = Arc::new(ReconcileCtx::from_env().await?);
+ info!(
+ namespace = ctx.operator_namespace,
+ image_prefix = ctx.image_prefix,
+ "code-operator starting"
+ );
+
+ // ---- Spawn all 5 controllers ----
+ let app_handle = tokio::spawn({
+ let ctx = ctx.clone();
+ let client = ctx.client.clone();
+ async move {
+ use operator::controller;
+ if let Err(e) = controller::start_app(client, ctx).await {
+ error!(%e, "app controller stopped");
+ }
+ }
+ });
+
+ let gs_handle = tokio::spawn({
+ let ctx = ctx.clone();
+ let client = ctx.client.clone();
+ async move {
+ use operator::controller;
+ if let Err(e) = controller::start_gitserver(client, ctx).await {
+ error!(%e, "gitserver controller stopped");
+ }
+ }
+ });
+
+ let ew_handle = tokio::spawn({
+ let ctx = ctx.clone();
+ let client = ctx.client.clone();
+ async move {
+ use operator::controller;
+ if let Err(e) = controller::start_email_worker(client, ctx).await {
+ error!(%e, "email_worker controller stopped");
+ }
+ }
+ });
+
+ let gh_handle = tokio::spawn({
+ let ctx = ctx.clone();
+ let client = ctx.client.clone();
+ async move {
+ use operator::controller;
+ if let Err(e) = controller::start_git_hook(client, ctx).await {
+ error!(%e, "git_hook controller stopped");
+ }
+ }
+ });
+
+ let mig_handle = tokio::spawn({
+ let ctx = ctx.clone();
+ let client = ctx.client.clone();
+ async move {
+ use operator::controller;
+ if let Err(e) = controller::start_migrate(client, ctx).await {
+ error!(%e, "migrate controller stopped");
+ }
+ }
+ });
+
+ // ---- Graceful shutdown on SIGINT / SIGTERM ----
+ tokio::signal::ctrl_c().await.ok();
+
+ info!("code-operator stopped");
+ let _ = tokio::join!(app_handle, gs_handle, ew_handle, gh_handle, mig_handle,);
+ Ok(())
+}
diff --git a/components.json b/components.json
new file mode 100644
index 0000000..15addee
--- /dev/null
+++ b/components.json
@@ -0,0 +1,25 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "base-nova",
+ "rsc": false,
+ "tsx": true,
+ "tailwind": {
+ "config": "",
+ "css": "src/index.css",
+ "baseColor": "neutral",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "iconLibrary": "lucide",
+ "rtl": false,
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui",
+ "lib": "@/lib",
+ "hooks": "@/hooks"
+ },
+ "menuColor": "default",
+ "menuAccent": "subtle",
+ "registries": {}
+}
diff --git a/deploy/Chart.yaml b/deploy/Chart.yaml
new file mode 100644
index 0000000..3f14379
--- /dev/null
+++ b/deploy/Chart.yaml
@@ -0,0 +1,13 @@
+apiVersion: v2
+name: c-----code
+description: Self-hosted GitHub + Slack alternative platform
+type: application
+version: 0.1.0
+appVersion: "0.1.0"
+keywords:
+ - git
+ - collaboration
+ - self-hosted
+maintainers:
+ - name: C-----code Team
+ email: team@c.dev
diff --git a/deploy/templates/NOTES.txt b/deploy/templates/NOTES.txt
new file mode 100644
index 0000000..c044cee
--- /dev/null
+++ b/deploy/templates/NOTES.txt
@@ -0,0 +1,35 @@
+{{/* Helm NOTES.txt – shown after install/upgrade */}}
+{{- if .Release.IsInstall }}
+🎉 {{ .Chart.Name }} {{ .Chart.Version }} installed in namespace {{ .Release.Namespace }}.
+
+⚠️ Prerequisites you must fulfil before the app starts:
+
+ 1. PostgreSQL database is reachable.
+ 2. Redis is reachable.
+ 3. (Optional) NATS if HOOK_POOL is enabled.
+ 4. (Optional) Qdrant if AI embeddings are used.
+
+📋 Required Secret "{{ .Release.Name }}-secrets" (create manually or via external secrets):
+
+ apiVersion: v1
+ kind: Secret
+ metadata:
+ name: {{ .Release.Name }}-secrets
+ namespace: {{ .Release.Namespace }}
+ type: Opaque
+ stringData:
+ APP_DATABASE_URL: postgresql://user:password@postgres:5432/db
+ APP_REDIS_URL: redis://redis:6379
+ # APP_SMTP_PASSWORD: ...
+ # APP_QDRANT_API_KEY: ...
+
+ Or set .Values.secrets in values.yaml.
+
+🔄 To run database migrations:
+ helm upgrade {{ .Release.Name }} ./c-----code -n {{ .Release.Namespace }} \
+ --set migrate.enabled=true
+
+📖 Useful commands:
+ kubectl get pods -n {{ .Release.Namespace }}
+ kubectl logs -n {{ .Release.Namespace }} -l app.kubernetes.io/name={{ .Chart.Name }}
+{{- end }}
diff --git a/deploy/templates/_helpers.tpl b/deploy/templates/_helpers.tpl
new file mode 100644
index 0000000..4f7e72b
--- /dev/null
+++ b/deploy/templates/_helpers.tpl
@@ -0,0 +1,44 @@
+{{/* =============================================================================
+ Common helpers
+ ============================================================================= */}}
+
+{{- define "c-----code.fullname" -}}
+{{- .Release.Name -}}
+{{- end -}}
+
+{{- define "c-----code.namespace" -}}
+{{- .Values.namespace | default .Release.Namespace -}}
+{{- end -}}
+
+{{- define "c-----code.image" -}}
+{{- $registry := .Values.image.registry -}}
+{{- $pullPolicy := .Values.image.pullPolicy -}}
+{{- printf "%s/%s:%s" $registry .image.repository .image.tag -}}
+{{- end -}}
+
+{{/* Inject image pull policy into sub-chart image dict */}}
+{{- define "c-----code.mergeImage" -}}
+{{- $merged := dict "pullPolicy" $.Values.image.pullPolicy -}}
+{{- $merged = merge $merged .image -}}
+{{- printf "%s/%s:%s" $.Values.image.registry $merged.repository $merged.tag -}}
+{{- end -}}
+
+{{/* Build a key-value env var list, optionally reading from a Secret */}}
+{{- define "c-----code.envFromSecret" -}}
+{{- $secretName := .existingSecret -}}
+{{- $keys := .secretKeys -}}
+{{- $result := list -}}
+{{- range $envName, $secretKey := $keys -}}
+{{- $item := dict "name" $envName "valueFrom" (dict "secretKeyRef" (dict "name" $secretName "key" $secretKey)) -}}
+{{- $result = append $result $item -}}
+{{- end -}}
+{{- $result | toJson | fromJson -}}
+{{- end -}}
+
+{{/* Merge two env lists (extra env over auto-injected) */}}
+{{- define "c-----code.mergeEnv" -}}
+{{- $auto := .auto -}}
+{{- $extra := .extra | default list -}}
+{{- $merged := append $auto $extra | toJson | fromJson -}}
+{{- $merged | toYaml -}}
+{{- end -}}
diff --git a/deploy/templates/app-deployment.yaml b/deploy/templates/app-deployment.yaml
new file mode 100644
index 0000000..d2eb191
--- /dev/null
+++ b/deploy/templates/app-deployment.yaml
@@ -0,0 +1,111 @@
+{{- if .Values.app.enabled -}}
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: {{ include "c-----code.fullname" . }}-app
+ namespace: {{ include "c-----code.namespace" . }}
+ labels:
+ app.kubernetes.io/name: {{ include "c-----code.fullname" . }}-app
+ app.kubernetes.io/instance: {{ .Release.Name }}
+ app.kubernetes.io/version: {{ .Chart.AppVersion }}
+spec:
+ replicas: {{ .Values.app.replicaCount }}
+ selector:
+ matchLabels:
+ app.kubernetes.io/name: {{ include "c-----code.fullname" . }}-app
+ app.kubernetes.io/instance: {{ .Release.Name }}
+ template:
+ metadata:
+ labels:
+ app.kubernetes.io/name: {{ include "c-----code.fullname" . }}-app
+ app.kubernetes.io/instance: {{ .Release.Name }}
+ spec:
+ containers:
+ - name: app
+ image: "{{ .Values.image.registry }}/{{ .Values.app.image.repository }}:{{ .Values.app.image.tag }}"
+ imagePullPolicy: {{ .Values.app.image.pullPolicy | default .Values.image.pullPolicy }}
+ ports:
+ - name: http
+ containerPort: {{ .Values.app.service.port }}
+ protocol: TCP
+ env:
+ - name: APP_DATABASE_URL
+ valueFrom:
+ secretKeyRef:
+ name: {{ .Values.database.existingSecret | default (printf "%s-secrets" (include "c-----code.fullname" .)) }}
+ key: {{ .Values.database.secretKeys.url }}
+ optional: true
+ - name: APP_REDIS_URL
+ valueFrom:
+ secretKeyRef:
+ name: {{ .Values.redis.existingSecret | default (printf "%s-secrets" (include "c-----code.fullname" .)) }}
+ key: {{ .Values.redis.secretKeys.url }}
+ optional: true
+ {{- if .Values.nats.enabled }}
+ - name: HOOK_POOL_REDIS_LIST_PREFIX
+ value: "{hook}"
+ - name: HOOK_POOL_REDIS_LOG_CHANNEL
+ value: "hook:logs"
+ {{- end }}
+ {{- if .Values.qdrant.enabled }}
+ - name: APP_QDRANT_URL
+ value: {{ .Values.qdrant.url }}
+ {{- if and .Values.qdrant.existingSecret .Values.qdrant.secretKeys.apiKey }}
+ - name: APP_QDRANT_API_KEY
+ valueFrom:
+ secretKeyRef:
+ name: {{ .Values.qdrant.existingSecret }}
+ key: {{ .Values.qdrant.secretKeys.apiKey }}
+ optional: true
+ {{- end }}
+ {{- end }}
+ {{- range .Values.app.env }}
+ - name: {{ .name }}
+ value: {{ .value | quote }}
+ {{- end }}
+ livenessProbe:
+ httpGet:
+ path: {{ .Values.app.livenessProbe.path }}
+ port: {{ .Values.app.livenessProbe.port }}
+ initialDelaySeconds: {{ .Values.app.livenessProbe.initialDelaySeconds }}
+ periodSeconds: {{ .Values.app.livenessProbe.periodSeconds }}
+ readinessProbe:
+ httpGet:
+ path: {{ .Values.app.readinessProbe.path }}
+ port: {{ .Values.app.readinessProbe.port }}
+ initialDelaySeconds: {{ .Values.app.readinessProbe.initialDelaySeconds }}
+ periodSeconds: {{ .Values.app.readinessProbe.periodSeconds }}
+ resources:
+ {{- toYaml .Values.app.resources | nindent 10 }}
+ {{- with .Values.app.nodeSelector }}
+ nodeSelector:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ {{- with .Values.app.affinity }}
+ affinity:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ {{- with .Values.app.tolerations }}
+ tolerations:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: {{ include "c-----code.fullname" . }}-app
+ namespace: {{ include "c-----code.namespace" . }}
+ labels:
+ app.kubernetes.io/name: {{ include "c-----code.fullname" . }}-app
+ app.kubernetes.io/instance: {{ .Release.Name }}
+spec:
+ type: {{ .Values.app.service.type }}
+ ports:
+ - port: {{ .Values.app.service.port }}
+ targetPort: http
+ protocol: TCP
+ name: http
+ selector:
+ app.kubernetes.io/name: {{ include "c-----code.fullname" . }}-app
+ app.kubernetes.io/instance: {{ .Release.Name }}
+{{- end }}
diff --git a/deploy/templates/configmap.yaml b/deploy/templates/configmap.yaml
new file mode 100644
index 0000000..02add79
--- /dev/null
+++ b/deploy/templates/configmap.yaml
@@ -0,0 +1,15 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: {{ include "c-----code.fullname" . }}-config
+ namespace: {{ include "c-----code.namespace" . }}
+ labels:
+ app.kubernetes.io/name: {{ .Chart.Name }}
+ app.kubernetes.io/instance: {{ .Release.Name }}
+ app.kubernetes.io/version: {{ .Chart.AppVersion }}
+data:
+{{- if .Values.app.config }}
+{{- range $key, $value := .Values.app.config }}
+ {{ $key }}: {{ $value | quote }}
+{{- end }}
+{{- end }}
diff --git a/deploy/templates/email-worker-deployment.yaml b/deploy/templates/email-worker-deployment.yaml
new file mode 100644
index 0000000..173c4a0
--- /dev/null
+++ b/deploy/templates/email-worker-deployment.yaml
@@ -0,0 +1,58 @@
+{{- if .Values.emailWorker.enabled -}}
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: {{ include "c-----code.fullname" . }}-email-worker
+ namespace: {{ include "c-----code.namespace" . }}
+ labels:
+ app.kubernetes.io/name: {{ include "c-----code.fullname" . }}-email-worker
+ app.kubernetes.io/instance: {{ .Release.Name }}
+ app.kubernetes.io/version: {{ .Chart.AppVersion }}
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app.kubernetes.io/name: {{ include "c-----code.fullname" . }}-email-worker
+ app.kubernetes.io/instance: {{ .Release.Name }}
+ template:
+ metadata:
+ labels:
+ app.kubernetes.io/name: {{ include "c-----code.fullname" . }}-email-worker
+ app.kubernetes.io/instance: {{ .Release.Name }}
+ spec:
+ containers:
+ - name: email-worker
+ image: "{{ .Values.image.registry }}/{{ .Values.emailWorker.image.repository }}:{{ .Values.emailWorker.image.tag }}"
+ imagePullPolicy: {{ .Values.emailWorker.image.pullPolicy | default .Values.image.pullPolicy }}
+ env:
+ - name: APP_DATABASE_URL
+ valueFrom:
+ secretKeyRef:
+ name: {{ .Values.database.existingSecret | default (printf "%s-secrets" (include "c-----code.fullname" .)) }}
+ key: {{ .Values.database.secretKeys.url }}
+ optional: true
+ - name: APP_REDIS_URL
+ valueFrom:
+ secretKeyRef:
+ name: {{ .Values.redis.existingSecret | default (printf "%s-secrets" (include "c-----code.fullname" .)) }}
+ key: {{ .Values.redis.secretKeys.url }}
+ optional: true
+ {{- range .Values.emailWorker.env }}
+ - name: {{ .name }}
+ value: {{ .value | quote }}
+ {{- end }}
+ resources:
+ {{- toYaml .Values.emailWorker.resources | nindent 10 }}
+ {{- with .Values.emailWorker.nodeSelector }}
+ nodeSelector:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ {{- with .Values.emailWorker.affinity }}
+ affinity:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ {{- with .Values.emailWorker.tolerations }}
+ tolerations:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+{{- end }}
diff --git a/deploy/templates/git-hook-deployment.yaml b/deploy/templates/git-hook-deployment.yaml
new file mode 100644
index 0000000..17107ce
--- /dev/null
+++ b/deploy/templates/git-hook-deployment.yaml
@@ -0,0 +1,64 @@
+{{- if .Values.gitHook.enabled -}}
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: {{ include "c-----code.fullname" . }}-git-hook
+ namespace: {{ include "c-----code.namespace" . }}
+ labels:
+ app.kubernetes.io/name: {{ include "c-----code.fullname" . }}-git-hook
+ app.kubernetes.io/instance: {{ .Release.Name }}
+ app.kubernetes.io/version: {{ .Chart.AppVersion }}
+spec:
+ replicas: {{ .Values.gitHook.replicaCount | default 2 }}
+ selector:
+ matchLabels:
+ app.kubernetes.io/name: {{ include "c-----code.fullname" . }}-git-hook
+ app.kubernetes.io/instance: {{ .Release.Name }}
+ template:
+ metadata:
+ labels:
+ app.kubernetes.io/name: {{ include "c-----code.fullname" . }}-git-hook
+ app.kubernetes.io/instance: {{ .Release.Name }}
+ spec:
+ containers:
+ - name: git-hook
+ image: "{{ .Values.image.registry }}/{{ .Values.gitHook.image.repository }}:{{ .Values.gitHook.image.tag }}"
+ imagePullPolicy: {{ .Values.gitHook.image.pullPolicy | default .Values.image.pullPolicy }}
+ env:
+ - name: APP_DATABASE_URL
+ valueFrom:
+ secretKeyRef:
+ name: {{ .Values.database.existingSecret | default (printf "%s-secrets" (include "c-----code.fullname" .)) }}
+ key: {{ .Values.database.secretKeys.url }}
+ optional: true
+ - name: APP_REDIS_URL
+ valueFrom:
+ secretKeyRef:
+ name: {{ .Values.redis.existingSecret | default (printf "%s-secrets" (include "c-----code.fullname" .)) }}
+ key: {{ .Values.redis.secretKeys.url }}
+ optional: true
+ {{- if .Values.nats.enabled }}
+ - name: HOOK_POOL_REDIS_LIST_PREFIX
+ value: "{hook}"
+ - name: HOOK_POOL_REDIS_LOG_CHANNEL
+ value: "hook:logs"
+ {{- end }}
+ {{- range .Values.gitHook.env }}
+ - name: {{ .name }}
+ value: {{ .value | quote }}
+ {{- end }}
+ resources:
+ {{- toYaml .Values.gitHook.resources | nindent 10 }}
+ {{- with .Values.gitHook.nodeSelector }}
+ nodeSelector:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ {{- with .Values.gitHook.affinity }}
+ affinity:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ {{- with .Values.gitHook.tolerations }}
+ tolerations:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+{{- end }}
diff --git a/deploy/templates/gitserver-deployment.yaml b/deploy/templates/gitserver-deployment.yaml
new file mode 100644
index 0000000..388f381
--- /dev/null
+++ b/deploy/templates/gitserver-deployment.yaml
@@ -0,0 +1,162 @@
+{{- if .Values.gitserver.enabled -}}
+{{- $fullName := include "c-----code.fullname" . -}}
+{{- $ns := include "c-----code.namespace" . -}}
+{{- $svc := .Values.gitserver -}}
+
+{{/* PersistentVolumeClaim for git repositories */}}
+{{- if $svc.persistence.enabled }}
+---
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: {{ $fullName }}-repos
+ namespace: {{ $ns }}
+ labels:
+ app.kubernetes.io/name: {{ $fullName }}-gitserver
+ app.kubernetes.io/instance: {{ $.Release.Name }}
+spec:
+ accessModes:
+ - {{ $svc.persistence.accessMode | default "ReadWriteOnce" }}
+ resources:
+ requests:
+ storage: {{ $svc.persistence.size }}
+ {{- if $svc.persistence.storageClass }}
+ storageClassName: {{ $svc.persistence.storageClass }}
+ {{- end }}
+{{- end }}
+
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: {{ $fullName }}-gitserver
+ namespace: {{ $ns }}
+ labels:
+ app.kubernetes.io/name: {{ $fullName }}-gitserver
+ app.kubernetes.io/instance: {{ $.Release.Name }}
+ app.kubernetes.io/version: {{ $.Chart.AppVersion }}
+spec:
+ replicas: {{ $svc.replicaCount }}
+ selector:
+ matchLabels:
+ app.kubernetes.io/name: {{ $fullName }}-gitserver
+ app.kubernetes.io/instance: {{ $.Release.Name }}
+ template:
+ metadata:
+ labels:
+ app.kubernetes.io/name: {{ $fullName }}-gitserver
+ app.kubernetes.io/instance: {{ $.Release.Name }}
+ spec:
+ containers:
+ - name: gitserver
+ image: "{{ $.Values.image.registry }}/{{ $svc.image.repository }}:{{ $svc.image.tag }}"
+ imagePullPolicy: {{ $svc.image.pullPolicy | default $.Values.image.pullPolicy }}
+ ports:
+ - name: http
+ containerPort: {{ $svc.service.http.port }}
+ protocol: TCP
+ - name: ssh
+ containerPort: {{ $svc.ssh.port }}
+ protocol: TCP
+ env:
+ - name: APP_REPOS_ROOT
+ value: /data/repos
+ - name: APP_DATABASE_URL
+ valueFrom:
+ secretKeyRef:
+ name: {{ $.Values.database.existingSecret | default (printf "%s-secrets" $fullName) }}
+ key: {{ $.Values.database.secretKeys.url }}
+ optional: true
+ - name: APP_REDIS_URL
+ valueFrom:
+ secretKeyRef:
+ name: {{ $.Values.redis.existingSecret | default (printf "%s-secrets" $fullName) }}
+ key: {{ $.Values.redis.secretKeys.url }}
+ optional: true
+ {{- if $svc.ssh.domain }}
+ - name: APP_SSH_DOMAIN
+ value: {{ $svc.ssh.domain }}
+ {{- end }}
+ {{- if $svc.ssh.port }}
+ - name: APP_SSH_PORT
+ value: {{ $svc.ssh.port | quote }}
+ {{- end }}
+ {{- range $svc.env }}
+ - name: {{ .name }}
+ value: {{ .value | quote }}
+ {{- end }}
+ resources:
+ {{- toYaml $svc.resources | nindent 10 }}
+ volumeMounts:
+ {{- if $svc.persistence.enabled }}
+ - name: repos
+ mountPath: /data/repos
+ {{- end }}
+ volumes:
+ {{- if $svc.persistence.enabled }}
+ - name: repos
+ persistentVolumeClaim:
+ claimName: {{ $fullName }}-repos
+ {{- end }}
+ {{- with $svc.nodeSelector }}
+ nodeSelector:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ {{- with $svc.affinity }}
+ affinity:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ {{- with $svc.tolerations }}
+ tolerations:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+
+---
+# HTTP service (git smart HTTP)
+apiVersion: v1
+kind: Service
+metadata:
+ name: {{ $fullName }}-gitserver-http
+ namespace: {{ $ns }}
+ labels:
+ app.kubernetes.io/name: {{ $fullName }}-gitserver
+ app.kubernetes.io/instance: {{ $.Release.Name }}
+spec:
+ type: {{ $svc.service.http.type }}
+ ports:
+ - name: http
+ port: {{ $svc.service.http.port }}
+ targetPort: http
+ protocol: TCP
+ selector:
+ app.kubernetes.io/name: {{ $fullName }}-gitserver
+ app.kubernetes.io/instance: {{ $.Release.Name }}
+
+---
+# SSH service (git over SSH)
+apiVersion: v1
+kind: Service
+metadata:
+ name: {{ $fullName }}-gitserver-ssh
+ namespace: {{ $ns }}
+ labels:
+ app.kubernetes.io/name: {{ $fullName }}-gitserver
+ app.kubernetes.io/instance: {{ $.Release.Name }}
+spec:
+ type: {{ $svc.service.ssh.type }}
+ {{- if eq $svc.service.ssh.type "NodePort" }}
+ ports:
+ - name: ssh
+ port: {{ $svc.ssh.port }}
+ targetPort: ssh
+ nodePort: {{ $svc.service.ssh.nodePort }}
+ {{- else }}
+ ports:
+ - name: ssh
+ port: {{ $svc.ssh.port }}
+ targetPort: ssh
+ {{- end }}
+ selector:
+ app.kubernetes.io/name: {{ $fullName }}-gitserver
+ app.kubernetes.io/instance: {{ $.Release.Name }}
+{{- end }}
diff --git a/deploy/templates/ingress.yaml b/deploy/templates/ingress.yaml
new file mode 100644
index 0000000..6031323
--- /dev/null
+++ b/deploy/templates/ingress.yaml
@@ -0,0 +1,46 @@
+{{- if .Values.app.ingress.enabled -}}
+{{- $svcName := printf "%s-app" (include "c-----code.fullname" .) -}}
+{{- $ns := include "c-----code.namespace" . -}}
+{{- $ing := .Values.app.ingress -}}
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: {{ include "c-----code.fullname" . }}-ingress
+ namespace: {{ $ns }}
+ labels:
+ app.kubernetes.io/name: {{ include "c-----code.fullname" . }}-app
+ app.kubernetes.io/instance: {{ .Release.Name }}
+ {{- with $ing.annotations }}
+ annotations:
+ {{- toYaml . | nindent 4 }}
+ {{- end }}
+spec:
+ {{- if $ing.className }}
+ ingressClassName: {{ $ing.className }}
+ {{- end }}
+ {{- if $ing.tls }}
+ tls:
+ {{- range $ing.tls }}
+ - hosts:
+ {{- range .hosts }}
+ - {{ . | quote }}
+ {{- end }}
+ secretName: {{ .secretName }}
+ {{- end }}
+ {{- end }}
+ rules:
+ {{- range $ing.hosts }}
+ - host: {{ .host | quote }}
+ http:
+ paths:
+ {{- range .paths }}
+ - path: {{ .path }}
+ pathType: {{ .pathType | default "Prefix" }}
+ backend:
+ service:
+ name: {{ $svcName }}
+ port:
+ number: {{ $.Values.app.service.port }}
+ {{- end }}
+ {{- end }}
+{{- end }}
diff --git a/deploy/templates/migrate-job.yaml b/deploy/templates/migrate-job.yaml
new file mode 100644
index 0000000..4023783
--- /dev/null
+++ b/deploy/templates/migrate-job.yaml
@@ -0,0 +1,42 @@
+{{- if .Values.migrate.enabled -}}
+apiVersion: batch/v1
+kind: Job
+metadata:
+ name: {{ include "c-----code.fullname" . }}-migrate
+ namespace: {{ include "c-----code.namespace" . }}
+ labels:
+ app.kubernetes.io/name: {{ include "c-----code.fullname" . }}-migrate
+ app.kubernetes.io/instance: {{ .Release.Name }}
+ app.kubernetes.io/version: {{ .Chart.AppVersion }}
+ helm.sh/hook: post-install,post-upgrade
+ helm.sh/hook-delete-policy: before-hook-creation
+spec:
+ backoffLimit: {{ .Values.migrate.backoffLimit }}
+ template:
+ metadata:
+ labels:
+ app.kubernetes.io/name: {{ include "c-----code.fullname" . }}-migrate
+ app.kubernetes.io/instance: {{ .Release.Name }}
+ spec:
+ restartPolicy: OnFailure
+ containers:
+ - name: migrate
+ image: "{{ .Values.image.registry }}/{{ .Values.migrate.image.repository }}:{{ .Values.migrate.image.tag }}"
+ imagePullPolicy: {{ .Values.migrate.image.pullPolicy | default .Values.image.pullPolicy }}
+ command:
+ {{- if .Values.migrate.command }}
+ - {{ .Values.migrate.command }}
+ {{- else }}
+ - up
+ {{- end }}
+ env:
+ - name: APP_DATABASE_URL
+ valueFrom:
+ secretKeyRef:
+ name: {{ .Values.database.existingSecret | default (printf "%s-secrets" (include "c-----code.fullname" .)) }}
+ key: {{ .Values.database.secretKeys.url }}
+ {{- range .Values.migrate.env }}
+ - name: {{ .name }}
+ value: {{ .value | quote }}
+ {{- end }}
+{{- end }}
diff --git a/deploy/templates/operator-deployment.yaml b/deploy/templates/operator-deployment.yaml
new file mode 100644
index 0000000..19968a5
--- /dev/null
+++ b/deploy/templates/operator-deployment.yaml
@@ -0,0 +1,52 @@
+{{- if .Values.operator.enabled -}}
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: {{ include "c-----code.fullname" . }}-operator
+ namespace: {{ include "c-----code.namespace" . }}
+ labels:
+ app.kubernetes.io/name: {{ include "c-----code.fullname" . }}-operator
+ app.kubernetes.io/instance: {{ .Release.Name }}
+ app.kubernetes.io/version: {{ .Chart.AppVersion }}
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app.kubernetes.io/name: {{ include "c-----code.fullname" . }}-operator
+ app.kubernetes.io/instance: {{ .Release.Name }}
+ template:
+ metadata:
+ labels:
+ app.kubernetes.io/name: {{ include "c-----code.fullname" . }}-operator
+ app.kubernetes.io/instance: {{ .Release.Name }}
+ spec:
+ serviceAccountName: {{ include "c-----code.fullname" . }}-operator
+ containers:
+ - name: operator
+ image: "{{ .Values.image.registry }}/{{ .Values.operator.image.repository }}:{{ .Values.operator.image.tag }}"
+ imagePullPolicy: {{ .Values.operator.image.pullPolicy | default .Values.image.pullPolicy }}
+ resources:
+ {{- toYaml .Values.operator.resources | nindent 10 }}
+ {{- with .Values.operator.nodeSelector }}
+ nodeSelector:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ {{- with .Values.operator.affinity }}
+ affinity:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+ {{- with .Values.operator.tolerations }}
+ tolerations:
+ {{- toYaml . | nindent 8 }}
+ {{- end }}
+
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: {{ include "c-----code.fullname" . }}-operator
+ namespace: {{ include "c-----code.namespace" . }}
+ labels:
+ app.kubernetes.io/name: {{ include "c-----code.fullname" . }}-operator
+ app.kubernetes.io/instance: {{ .Release.Name }}
+{{- end }}
diff --git a/deploy/templates/secret.yaml b/deploy/templates/secret.yaml
new file mode 100644
index 0000000..b1b354e
--- /dev/null
+++ b/deploy/templates/secret.yaml
@@ -0,0 +1,17 @@
+{{- /* Template for bootstrap secrets – replace with external secret manager in prod */ -}}
+{{- if .Values.secrets }}
+apiVersion: v1
+kind: Secret
+metadata:
+ name: {{ include "c-----code.fullname" . }}-secrets
+ namespace: {{ include "c-----code.namespace" . }}
+ labels:
+ app.kubernetes.io/name: {{ .Chart.Name }}
+ app.kubernetes.io/instance: {{ .Release.Name }}
+ app.kubernetes.io/version: {{ .Chart.AppVersion }}
+type: Opaque
+stringData:
+{{- range $key, $value := .Values.secrets }}
+ {{ $key }}: {{ $value | quote }}
+{{- end }}
+{{- end }}
diff --git a/deploy/values.yaml b/deploy/values.yaml
new file mode 100644
index 0000000..441064d
--- /dev/null
+++ b/deploy/values.yaml
@@ -0,0 +1,262 @@
+# =============================================================================
+# Global / common settings
+# =============================================================================
+namespace: c-----code
+releaseName: c-----code
+
+image:
+ registry: harbor.gitdata.me/gta_team
+ pullPolicy: IfNotPresent
+
+# PostgreSQL (required) – set connection string via secret or values
+database:
+ existingSecret: ""
+ secretKeys:
+ url: APP_DATABASE_URL
+
+# Redis (required)
+redis:
+ existingSecret: ""
+ secretKeys:
+ url: APP_REDIS_URL
+
+# NATS (optional – required only if HOOK_POOL is enabled)
+nats:
+ enabled: false
+ url: nats://nats:4222
+
+# Qdrant (optional – required only if AI embeddings are used)
+qdrant:
+ enabled: false
+ url: http://qdrant:6333
+ existingSecret: ""
+ secretKeys:
+ apiKey: APP_QDRANT_API_KEY
+
+# =============================================================================
+# App – main web/API service
+# =============================================================================
+app:
+ enabled: true
+ replicaCount: 3
+
+ image:
+ repository: app
+ tag: latest
+
+ service:
+ type: ClusterIP
+ port: 8080
+
+ ingress:
+ enabled: false
+ className: cilium # Cilium Ingress (or envoy for EnvoyGateway)
+ annotations: {}
+ hosts:
+ - host: c-----.local
+ paths:
+ - path: /
+ pathType: Prefix
+ tls: []
+
+ resources:
+ requests:
+ cpu: 100m
+ memory: 256Mi
+ limits:
+ cpu: 1000m
+ memory: 1Gi
+
+ livenessProbe:
+ path: /health
+ port: 8080
+ initialDelaySeconds: 5
+ periodSeconds: 10
+
+ readinessProbe:
+ path: /health
+ port: 8080
+ initialDelaySeconds: 5
+ periodSeconds: 5
+
+ # Extra env vars (merge with auto-injected ones)
+ env: []
+
+ nodeSelector: {}
+ tolerations: []
+ affinity: {}
+
+# =============================================================================
+# Gitserver – git daemon / SSH + HTTP server
+# =============================================================================
+gitserver:
+ enabled: true
+ replicaCount: 1
+
+ image:
+ repository: gitserver
+ tag: latest
+
+ service:
+ http:
+ type: ClusterIP
+ port: 8022
+ ssh:
+ type: NodePort
+ nodePort: 30222
+
+ resources:
+ requests:
+ cpu: 100m
+ memory: 128Mi
+ limits:
+ cpu: 500m
+ memory: 512Mi
+
+ # Storage for git repos
+ persistence:
+ enabled: true
+ storageClass: ""
+ size: 50Gi
+ accessMode: ReadWriteOnce
+
+ ssh:
+ domain: ""
+ port: 22
+
+ env: []
+
+ nodeSelector: {}
+ tolerations: []
+ affinity: {}
+
+# =============================================================================
+# Email worker – processes outgoing email queue
+# =============================================================================
+emailWorker:
+ enabled: true
+
+ image:
+ repository: email-worker
+ tag: latest
+
+ resources:
+ requests:
+ cpu: 50m
+ memory: 64Mi
+ limits:
+ cpu: 200m
+ memory: 256Mi
+
+ env: []
+
+ nodeSelector: {}
+ tolerations: []
+ affinity: {}
+
+# =============================================================================
+# Git hook pool – handles pre-receive / post-receive hooks
+# =============================================================================
+gitHook:
+ enabled: true
+
+ image:
+ repository: git-hook
+ tag: latest
+
+ replicaCount: 2
+
+ resources:
+ requests:
+ cpu: 50m
+ memory: 64Mi
+ limits:
+ cpu: 200m
+ memory: 256Mi
+
+ env: []
+
+ nodeSelector: {}
+ tolerations: []
+ affinity: {}
+
+# =============================================================================
+# Migrate – database migration Job (runOnce)
+# =============================================================================
+migrate:
+ enabled: false # Set true to run migrations on upgrade
+
+ image:
+ repository: migrate
+ tag: latest
+
+ command: up
+ backoffLimit: 3
+
+ env: []
+
+# =============================================================================
+# Operator – Kubernetes operator (manages custom App/GitServer CRDs)
+# =============================================================================
+operator:
+ enabled: false # Enable only if running the custom operator
+
+ image:
+ repository: operator
+ tag: latest
+
+ resources:
+ requests:
+ cpu: 50m
+ memory: 64Mi
+ limits:
+ cpu: 200m
+ memory: 256Mi
+
+ nodeSelector: {}
+ tolerations: []
+ affinity: {}
+
+# =============================================================================
+# Act Runner – Gitea Actions self-hosted runner
+# =============================================================================
+actRunner:
+ enabled: false
+
+ image:
+ repository: act-runner
+ tag: latest
+
+ replicaCount: 2
+
+ # Concurrency per runner instance
+ capacity: 2
+
+ # Runner labels (must match workflow `runs-on`)
+ labels:
+ - gitea
+ - docker
+
+ logLevel: info
+
+ cache:
+ enabled: true
+ dir: /tmp/actions-cache
+
+ resources:
+ requests:
+ cpu: 500m
+ memory: 1Gi
+ limits:
+ cpu: 2000m
+ memory: 4Gi
+
+ env: []
+
+ nodeSelector: {}
+ tolerations:
+ - key: "runner"
+ operator: "Equal"
+ value: "true"
+ effect: "NoSchedule"
+ affinity: {}
diff --git a/docker/app.Dockerfile b/docker/app.Dockerfile
new file mode 100644
index 0000000..9a446b8
--- /dev/null
+++ b/docker/app.Dockerfile
@@ -0,0 +1,41 @@
+# ---- Stage 1: Build ----
+FROM rust:1.94-bookworm AS builder
+
+ARG BUILD_TARGET=x86_64-unknown-linux-gnu
+ENV TARGET=${BUILD_TARGET}
+
+# Build dependencies: OpenSSL, libgit2, zlib, clang for sea-orm codegen
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ pkg-config libssl-dev libclang-dev \
+ gcc g++ make \
+ && rm -rf /var/lib/apt/lists/*
+
+WORKDIR /build
+
+# Copy workspace manifests
+COPY Cargo.toml Cargo.lock ./
+COPY libs/ libs/
+COPY apps/app/ apps/app/
+
+# Pre-build dependencies only
+RUN cargo fetch
+
+# Build the binary
+RUN --mount=type=cache,target=/usr/local/cargo/registry \
+ --mount=type=cache,target=/usr/local/cargo/git \
+ --mount=type=cache,target=target \
+ cargo build --release --package app --target ${TARGET}
+
+# ---- Stage 2: Runtime ----
+FROM debian:bookworm-slim AS runtime
+
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ ca-certificates libssl3 \
+ && rm -rf /var/lib/apt/lists/*
+
+WORKDIR /app
+COPY --from=builder /build/target/${TARGET}/release/app /app/app
+
+# All config via environment variables (APP_* prefix)
+ENV APP_LOG_LEVEL=info
+ENTRYPOINT ["/app/app"]
diff --git a/docker/build.md b/docker/build.md
new file mode 100644
index 0000000..ff9b0de
--- /dev/null
+++ b/docker/build.md
@@ -0,0 +1,171 @@
+# Docker 构建指南
+
+## 前提条件
+
+- Docker 20.10+
+- Cargo.lock 已存在(`cargo generate-lockfile`)
+- 网络能够访问 crates.io
+
+## 快速开始
+
+```bash
+# 构建全部镜像(默认 registry=myapp, tag=latest)
+./docker/build.sh
+
+# 构建指定镜像
+./docker/build.sh app
+./docker/build.sh gitserver email-worker
+
+# 指定 registry 和 tag
+REGISTRY=myregistry TAG=v1.0.0 ./docker/build.sh
+```
+
+## 镜像列表
+
+| 镜像 | Dockerfile | 二进制 | 实例类型 | 说明 |
+|---|---|---|---|---|
+| `myapp/app:latest` | `app.Dockerfile` | `app` | 多实例 | 主 Web 服务(API + WS) |
+| `myapp/gitserver:latest` | `gitserver.Dockerfile` | `gitserver` | 单实例 | Git HTTP + SSH 服务 |
+| `myapp/email-worker:latest` | `email-worker.Dockerfile` | `email-worker` | 单实例 | 邮件发送 Worker |
+| `myapp/git-hook:latest` | `git-hook.Dockerfile` | `git-hook` | 单实例 | Git Hook 事件处理 |
+| `myapp/migrate:latest` | `migrate.Dockerfile` | `migrate` | Job/InitContainer | 数据库迁移 CLI |
+
+## 部署架构
+
+```
+ ┌─ NATS ─┐
+ │ │
+┌─────────┐ ┌──────────────┐ ┌─────────────────┐
+│ LB/ │───▶│ app (×N) │ │ git-hook │
+│ nginx │ │ (stateless) │ │ (单实例) │
+└─────────┘ └──────────────┘ └─────────────────┘
+ ┌──────────────┐
+ │ gitserver │
+ │ (单实例) │ ┌─────────────────┐
+ │ HTTP :8022 │───▶│ email-worker │
+ │ SSH :2222 │ │ (单实例) │
+ └──────────────┘ └─────────────────┘
+```
+
+## 环境变量
+
+所有配置通过环境变量注入,无需修改镜像:
+
+| 变量 | 示例 | 说明 |
+|---|---|---|
+| `APP_DATABASE_URL` | `postgres://user:pass@host:5432/db` | 数据库连接 |
+| `APP_REDIS_URLS` | `redis://host:6379` | Redis(多实例用逗号分隔) |
+| `APP_SMTP_HOST` | `smtp.example.com` | SMTP 服务器 |
+| `APP_SMTP_USERNAME` | `noreply@example.com` | SMTP 用户名 |
+| `APP_SMTP_PASSWORD` | `xxx` | SMTP 密码 |
+| `APP_SMTP_FROM` | `noreply@example.com` | 发件人地址 |
+| `APP_AI_BASIC_URL` | `https://api.openai.com/v1` | AI API 地址 |
+| `APP_AI_API_KEY` | `sk-xxx` | AI API Key |
+| `APP_DOMAIN_URL` | `https://example.com` | 主域名 |
+| `APP_LOG_LEVEL` | `info` | 日志级别: trace/debug/info/warn/error |
+| `APP_SSH_DOMAIN` | `git.example.com` | Git SSH 域名 |
+| `APP_REPOS_ROOT` | `/data/repos` | Git 仓库存储路径 |
+| `NATS_URL` | `nats://localhost:4222` | NATS 服务器地址 |
+
+## 数据库迁移
+
+镜像启动前先运行迁移:
+
+```bash
+# 方式一:直接运行
+docker run --rm \
+ --env-file .env \
+ myapp/migrate:latest up
+
+# 方式二:Kubernetes InitContainer
+# 见下方 K8s 示例
+```
+
+## Kubernetes 部署示例
+
+```yaml
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: app
+spec:
+ replicas: 3
+ template:
+ spec:
+ containers:
+ - name: app
+ image: myapp/app:latest
+ envFrom:
+ - secretRef:
+ name: app-secrets
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: gitserver
+spec:
+ replicas: 1
+ template:
+ spec:
+ containers:
+ - name: gitserver
+ image: myapp/gitserver:latest
+ ports:
+ - containerPort: 8022 # HTTP
+ - containerPort: 2222 # SSH
+ envFrom:
+ - secretRef:
+ name: app-secrets
+ volumeMounts:
+ - name: repos
+ mountPath: /data/repos
+ volumes:
+ - name: repos
+ persistentVolumeClaim:
+ claimName: git-repos
+---
+apiVersion: batch/v1
+kind: Job
+metadata:
+ name: migrate
+spec:
+ template:
+ spec:
+ containers:
+ - name: migrate
+ image: myapp/migrate:latest
+ envFrom:
+ - secretRef:
+ name: app-secrets
+ args: ["up"]
+ restartPolicy: Never
+```
+
+## 构建缓存
+
+使用 Docker BuildKit 的构建缓存:
+- `--mount=type=cache,target=/usr/local/cargo/registry` — crates.io 依赖
+- `--mount=type=cache,target=/usr/local/cargo/git` — git 依赖
+- `--mount=type=cache,target=target` — 编译产物
+
+建议挂载持久化缓存卷以加速增量构建:
+
+```bash
+docker buildx create --use
+docker buildx build \
+ --cache-from=type=local,src=/tmp/cargo-cache \
+ --cache-to=type=local,dest=/tmp/cargo-cache \
+ -f docker/app.Dockerfile -t myapp/app .
+```
+
+## 跨平台构建
+
+默认构建 x86_64 Linux 可执行文件。构建其他平台:
+
+```bash
+# ARM64
+BUILD_TARGET=aarch64-unknown-linux-gnu ./docker/build.sh
+
+# 需先安装对应 target:
+rustup target add aarch64-unknown-linux-gnu
+```
diff --git a/docker/build.sh b/docker/build.sh
new file mode 100644
index 0000000..fedf3ef
--- /dev/null
+++ b/docker/build.sh
@@ -0,0 +1,52 @@
+#!/bin/bash
+set -e
+
+REGISTRY="${REGISTRY:-harbor.gitdata.me/gta_team}"
+TAG="${TAG:-latest}"
+BUILD_TARGET="${BUILD_TARGET:-x86_64-unknown-linux-gnu}"
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+cd "$SCRIPT_DIR/.."
+
+# All images: (dockerfile, image-name)
+declare -A ALL_IMAGES=(
+ [app]="docker/app.Dockerfile"
+ [gitserver]="docker/gitserver.Dockerfile"
+ [email-worker]="docker/email-worker.Dockerfile"
+ [git-hook]="docker/git-hook.Dockerfile"
+ [migrate]="docker/migrate.Dockerfile"
+ [operator]="docker/operator.Dockerfile"
+)
+
+# Filter by first argument if provided
+TARGETS=("$@")
+if [[ ${#TARGETS[@]} -eq 0 ]] || [[ "${TARGETS[0]}" == "all" ]]; then
+ TARGETS=("${!ALL_IMAGES[@]}")
+fi
+
+for name in "${TARGETS[@]}"; do
+ df="${ALL_IMAGES[$name]}"
+ if [[ -z "$df" ]]; then
+ echo "ERROR: unknown image '$name'"
+ echo "Available: ${!ALL_IMAGES[@]}"
+ exit 1
+ fi
+ if [[ ! -f "$df" ]]; then
+ echo "ERROR: $df not found"
+ exit 1
+ fi
+ image="${REGISTRY}/${name}:${TAG}"
+ echo "==> Building $image"
+ docker build \
+ --build-arg BUILD_TARGET="${BUILD_TARGET}" \
+ -f "$df" \
+ -t "$image" \
+ .
+ echo "==> $image done"
+ echo ""
+done
+
+echo "==> All images built:"
+for name in "${TARGETS[@]}"; do
+ echo " ${REGISTRY}/${name}:${TAG}"
+done
diff --git a/docker/crd/app-crd.yaml b/docker/crd/app-crd.yaml
new file mode 100644
index 0000000..94addce
--- /dev/null
+++ b/docker/crd/app-crd.yaml
@@ -0,0 +1,127 @@
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ name: apps.code.dev
+ annotations:
+ controller-gen.kubebuilder.io/version: v0.16.0
+spec:
+ group: code.dev
+ names:
+ kind: App
+ listKind: AppList
+ plural: apps
+ singular: app
+ shortNames:
+ - app
+ scope: Namespaced
+ versions:
+ - name: v1
+ served: true
+ storage: true
+ subresources:
+ status: {}
+ additionalPrinterColumns:
+ - name: Replicas
+ jsonPath: .spec.replicas
+ type: integer
+ - name: Ready
+ jsonPath: .status.phase
+ type: string
+ - name: Age
+ jsonPath: .metadata.creationTimestamp
+ type: date
+ schema:
+ openAPIV3Schema:
+ type: object
+ required: [spec]
+ properties:
+ apiVersion:
+ type: string
+ kind:
+ type: string
+ metadata:
+ type: object
+ spec:
+ type: object
+ required: []
+ properties:
+ image:
+ type: string
+ default: myapp/app:latest
+ replicas:
+ type: integer
+ default: 3
+ env:
+ type: array
+ items:
+ type: object
+ required: [name]
+ properties:
+ name:
+ type: string
+ value:
+ type: string
+ valueFrom:
+ type: object
+ properties:
+ secretRef:
+ type: object
+ required: [name, secretName, secretKey]
+ properties:
+ name:
+ type: string
+ secretName:
+ type: string
+ secretKey:
+ type: string
+ resources:
+ type: object
+ properties:
+ requests:
+ type: object
+ properties:
+ cpu:
+ type: string
+ memory:
+ type: string
+ limits:
+ type: object
+ properties:
+ cpu:
+ type: string
+ memory:
+ type: string
+ livenessProbe:
+ type: object
+ properties:
+ port:
+ type: integer
+ default: 8080
+ path:
+ type: string
+ default: /health
+ initialDelaySeconds:
+ type: integer
+ default: 5
+ readinessProbe:
+ type: object
+ properties:
+ port:
+ type: integer
+ default: 8080
+ path:
+ type: string
+ default: /health
+ initialDelaySeconds:
+ type: integer
+ default: 5
+ imagePullPolicy:
+ type: string
+ default: IfNotPresent
+ status:
+ type: object
+ properties:
+ readyReplicas:
+ type: integer
+ phase:
+ type: string
diff --git a/docker/crd/email-worker-crd.yaml b/docker/crd/email-worker-crd.yaml
new file mode 100644
index 0000000..ba22be2
--- /dev/null
+++ b/docker/crd/email-worker-crd.yaml
@@ -0,0 +1,94 @@
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ name: emailworkers.code.dev
+ annotations:
+ controller-gen.kubebuilder.io/version: v0.16.0
+spec:
+ group: code.dev
+ names:
+ kind: EmailWorker
+ listKind: EmailWorkerList
+ plural: emailworkers
+ singular: emailworker
+ shortNames:
+ - ew
+ scope: Namespaced
+ versions:
+ - name: v1
+ served: true
+ storage: true
+ subresources:
+ status: {}
+ additionalPrinterColumns:
+ - name: Age
+ jsonPath: .metadata.creationTimestamp
+ type: date
+ schema:
+ openAPIV3Schema:
+ type: object
+ required: [spec]
+ properties:
+ apiVersion:
+ type: string
+ kind:
+ type: string
+ metadata:
+ type: object
+ spec:
+ type: object
+ required: []
+ properties:
+ image:
+ type: string
+ default: myapp/email-worker:latest
+ env:
+ type: array
+ items:
+ type: object
+ required: [name]
+ properties:
+ name:
+ type: string
+ value:
+ type: string
+ valueFrom:
+ type: object
+ properties:
+ secretRef:
+ type: object
+ required: [name, secretName, secretKey]
+ properties:
+ name:
+ type: string
+ secretName:
+ type: string
+ secretKey:
+ type: string
+ resources:
+ type: object
+ properties:
+ requests:
+ type: object
+ properties:
+ cpu:
+ type: string
+ memory:
+ type: string
+ limits:
+ type: object
+ properties:
+ cpu:
+ type: string
+ memory:
+ type: string
+ imagePullPolicy:
+ type: string
+ default: IfNotPresent
+ status:
+ type: object
+ properties:
+ readyReplicas:
+ type: integer
+ phase:
+ type: string
diff --git a/docker/crd/git-hook-crd.yaml b/docker/crd/git-hook-crd.yaml
new file mode 100644
index 0000000..7123c85
--- /dev/null
+++ b/docker/crd/git-hook-crd.yaml
@@ -0,0 +1,96 @@
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ name: githooks.code.dev
+ annotations:
+ controller-gen.kubebuilder.io/version: v0.16.0
+spec:
+ group: code.dev
+ names:
+ kind: GitHook
+ listKind: GitHookList
+ plural: githooks
+ singular: githook
+ shortNames:
+ - ghk
+ scope: Namespaced
+ versions:
+ - name: v1
+ served: true
+ storage: true
+ subresources:
+ status: {}
+ additionalPrinterColumns:
+ - name: Age
+ jsonPath: .metadata.creationTimestamp
+ type: date
+ schema:
+ openAPIV3Schema:
+ type: object
+ required: [spec]
+ properties:
+ apiVersion:
+ type: string
+ kind:
+ type: string
+ metadata:
+ type: object
+ spec:
+ type: object
+ required: []
+ properties:
+ image:
+ type: string
+ default: myapp/git-hook:latest
+ env:
+ type: array
+ items:
+ type: object
+ required: [name]
+ properties:
+ name:
+ type: string
+ value:
+ type: string
+ valueFrom:
+ type: object
+ properties:
+ secretRef:
+ type: object
+ required: [name, secretName, secretKey]
+ properties:
+ name:
+ type: string
+ secretName:
+ type: string
+ secretKey:
+ type: string
+ resources:
+ type: object
+ properties:
+ requests:
+ type: object
+ properties:
+ cpu:
+ type: string
+ memory:
+ type: string
+ limits:
+ type: object
+ properties:
+ cpu:
+ type: string
+ memory:
+ type: string
+ imagePullPolicy:
+ type: string
+ default: IfNotPresent
+ workerId:
+ type: string
+ status:
+ type: object
+ properties:
+ readyReplicas:
+ type: integer
+ phase:
+ type: string
diff --git a/docker/crd/gitserver-crd.yaml b/docker/crd/gitserver-crd.yaml
new file mode 100644
index 0000000..fa60874
--- /dev/null
+++ b/docker/crd/gitserver-crd.yaml
@@ -0,0 +1,108 @@
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ name: gitservers.code.dev
+ annotations:
+ controller-gen.kubebuilder.io/version: v0.16.0
+spec:
+ group: code.dev
+ names:
+ kind: GitServer
+ listKind: GitServerList
+ plural: gitservers
+ singular: gitserver
+ shortNames:
+ - gs
+ scope: Namespaced
+ versions:
+ - name: v1
+ served: true
+ storage: true
+ subresources:
+ status: {}
+ additionalPrinterColumns:
+ - name: Age
+ jsonPath: .metadata.creationTimestamp
+ type: date
+ schema:
+ openAPIV3Schema:
+ type: object
+ required: [spec]
+ properties:
+ apiVersion:
+ type: string
+ kind:
+ type: string
+ metadata:
+ type: object
+ spec:
+ type: object
+ required: []
+ properties:
+ image:
+ type: string
+ default: myapp/gitserver:latest
+ env:
+ type: array
+ items:
+ type: object
+ required: [name]
+ properties:
+ name:
+ type: string
+ value:
+ type: string
+ valueFrom:
+ type: object
+ properties:
+ secretRef:
+ type: object
+ required: [name, secretName, secretKey]
+ properties:
+ name:
+ type: string
+ secretName:
+ type: string
+ secretKey:
+ type: string
+ resources:
+ type: object
+ properties:
+ requests:
+ type: object
+ properties:
+ cpu:
+ type: string
+ memory:
+ type: string
+ limits:
+ type: object
+ properties:
+ cpu:
+ type: string
+ memory:
+ type: string
+ sshServiceType:
+ type: string
+ default: NodePort
+ storageSize:
+ type: string
+ default: 10Gi
+ imagePullPolicy:
+ type: string
+ default: IfNotPresent
+ sshDomain:
+ type: string
+ sshPort:
+ type: integer
+ default: 22
+ httpPort:
+ type: integer
+ default: 8022
+ status:
+ type: object
+ properties:
+ readyReplicas:
+ type: integer
+ phase:
+ type: string
diff --git a/docker/crd/migrate-crd.yaml b/docker/crd/migrate-crd.yaml
new file mode 100644
index 0000000..255d150
--- /dev/null
+++ b/docker/crd/migrate-crd.yaml
@@ -0,0 +1,87 @@
+apiVersion: apiextensions.k8s.io/v1
+kind: CustomResourceDefinition
+metadata:
+ name: migrates.code.dev
+ annotations:
+ controller-gen.kubebuilder.io/version: v0.16.0
+spec:
+ group: code.dev
+ names:
+ kind: Migrate
+ listKind: MigrateList
+ plural: migrates
+ singular: migrate
+ shortNames:
+ - mig
+ scope: Namespaced
+ versions:
+ - name: v1
+ served: true
+ storage: true
+ subresources:
+ status: {}
+ additionalPrinterColumns:
+ - name: Status
+ jsonPath: .status.phase
+ type: string
+ - name: Age
+ jsonPath: .metadata.creationTimestamp
+ type: date
+ schema:
+ openAPIV3Schema:
+ type: object
+ required: [spec]
+ properties:
+ apiVersion:
+ type: string
+ kind:
+ type: string
+ metadata:
+ type: object
+ spec:
+ type: object
+ required: []
+ properties:
+ image:
+ type: string
+ default: myapp/migrate:latest
+ env:
+ type: array
+ description: "Must include APP_DATABASE_URL"
+ items:
+ type: object
+ required: [name]
+ properties:
+ name:
+ type: string
+ value:
+ type: string
+ valueFrom:
+ type: object
+ properties:
+ secretRef:
+ type: object
+ required: [name, secretName, secretKey]
+ properties:
+ name:
+ type: string
+ secretName:
+ type: string
+ secretKey:
+ type: string
+ command:
+ type: string
+ default: up
+ description: "Migration command: up, down, fresh, refresh, reset"
+ backoffLimit:
+ type: integer
+ default: 3
+ status:
+ type: object
+ properties:
+ phase:
+ type: string
+ startTime:
+ type: string
+ completionTime:
+ type: string
diff --git a/docker/email-worker.Dockerfile b/docker/email-worker.Dockerfile
new file mode 100644
index 0000000..62f8de0
--- /dev/null
+++ b/docker/email-worker.Dockerfile
@@ -0,0 +1,36 @@
+# ---- Stage 1: Build ----
+FROM rust:1.94-bookworm AS builder
+
+ARG BUILD_TARGET=x86_64-unknown-linux-gnu
+ENV TARGET=${BUILD_TARGET}
+
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ pkg-config libssl-dev libclang-dev \
+ gcc g++ make \
+ && rm -rf /var/lib/apt/lists/*
+
+WORKDIR /build
+
+COPY Cargo.toml Cargo.lock ./
+COPY libs/ libs/
+COPY apps/email/ apps/email/
+
+RUN cargo fetch
+
+RUN --mount=type=cache,target=/usr/local/cargo/registry \
+ --mount=type=cache,target=/usr/local/cargo/git \
+ --mount=type=cache,target=target \
+ cargo build --release --package email-server --target ${TARGET}
+
+# ---- Stage 2: Runtime ----
+FROM debian:bookworm-slim
+
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ ca-certificates libssl3 \
+ && rm -rf /var/lib/apt/lists/*
+
+WORKDIR /app
+COPY --from=builder /build/target/${TARGET}/release/email-server /app/email-worker
+
+ENV APP_LOG_LEVEL=info
+ENTRYPOINT ["/app/email-worker"]
diff --git a/docker/git-hook.Dockerfile b/docker/git-hook.Dockerfile
new file mode 100644
index 0000000..2dc885a
--- /dev/null
+++ b/docker/git-hook.Dockerfile
@@ -0,0 +1,36 @@
+# ---- Stage 1: Build ----
+FROM rust:1.94-bookworm AS builder
+
+ARG BUILD_TARGET=x86_64-unknown-linux-gnu
+ENV TARGET=${BUILD_TARGET}
+
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ pkg-config libssl-dev libgit2-dev zlib1g-dev libclang-dev \
+ gcc g++ make \
+ && rm -rf /var/lib/apt/lists/*
+
+WORKDIR /build
+
+COPY Cargo.toml Cargo.lock ./
+COPY libs/ libs/
+COPY apps/git-hook/ apps/git-hook/
+
+RUN cargo fetch
+
+RUN --mount=type=cache,target=/usr/local/cargo/registry \
+ --mount=type=cache,target=/usr/local/cargo/git \
+ --mount=type=cache,target=target \
+ cargo build --release --package git-hook --target ${TARGET}
+
+# ---- Stage 2: Runtime ----
+FROM debian:bookworm-slim
+
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ ca-certificates libssl3 openssh-client \
+ && rm -rf /var/lib/apt/lists/*
+
+WORKDIR /app
+COPY --from=builder /build/target/${TARGET}/release/git-hook /app/git-hook
+
+ENV APP_LOG_LEVEL=info
+ENTRYPOINT ["/app/git-hook"]
diff --git a/docker/gitserver.Dockerfile b/docker/gitserver.Dockerfile
new file mode 100644
index 0000000..9ef0e71
--- /dev/null
+++ b/docker/gitserver.Dockerfile
@@ -0,0 +1,41 @@
+# ---- Stage 1: Build ----
+FROM rust:1.94-bookworm AS builder
+
+ARG BUILD_TARGET=x86_64-unknown-linux-gnu
+ENV TARGET=${BUILD_TARGET}
+
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ pkg-config libssl-dev libgit2-dev zlib1g-dev libclang-dev \
+ gcc g++ make \
+ && rm -rf /var/lib/apt/lists/*
+
+WORKDIR /build
+
+COPY Cargo.toml Cargo.lock ./
+COPY libs/ libs/
+COPY apps/gitserver/ apps/gitserver/
+
+RUN cargo fetch
+
+RUN --mount=type=cache,target=/usr/local/cargo/registry \
+ --mount=type=cache,target=/usr/local/cargo/git \
+ --mount=type=cache,target=target \
+ cargo build --release --package gitserver --target ${TARGET}
+
+# ---- Stage 2: Runtime ----
+FROM debian:bookworm-slim AS runtime
+
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ ca-certificates libssl3 openssh-server \
+ && rm -rf /var/lib/apt/lists/*
+
+# SSH requires host keys and proper permissions
+RUN mkdir -p /run/sshd && \
+ ssh-keygen -A && \
+ chmod 755 /etc/ssh
+
+WORKDIR /app
+COPY --from=builder /build/target/${TARGET}/release/gitserver /app/gitserver
+
+ENV APP_LOG_LEVEL=info
+ENTRYPOINT ["/app/gitserver"]
diff --git a/docker/migrate.Dockerfile b/docker/migrate.Dockerfile
new file mode 100644
index 0000000..03da474
--- /dev/null
+++ b/docker/migrate.Dockerfile
@@ -0,0 +1,36 @@
+# ---- Stage 1: Build ----
+FROM rust:1.94-bookworm AS builder
+
+ARG BUILD_TARGET=x86_64-unknown-linux-gnu
+ENV TARGET=${BUILD_TARGET}
+
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ pkg-config libssl-dev libclang-dev \
+ gcc g++ make \
+ && rm -rf /var/lib/apt/lists/*
+
+WORKDIR /build
+
+COPY Cargo.toml Cargo.lock ./
+COPY libs/ libs/
+COPY apps/migrate/ apps/migrate/
+
+RUN cargo fetch
+
+RUN --mount=type=cache,target=/usr/local/cargo/registry \
+ --mount=type=cache,target=/usr/local/cargo/git \
+ --mount=type=cache,target=target \
+ cargo build --release --package migrate-cli --target ${TARGET}
+
+# ---- Stage 2: Runtime ----
+FROM debian:bookworm-slim
+
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ ca-certificates libssl3 \
+ && rm -rf /var/lib/apt/lists/*
+
+WORKDIR /app
+COPY --from=builder /build/target/${TARGET}/release/migrate /app/migrate
+
+# Run migrations via: docker run --rm myapp/migrate up
+ENTRYPOINT ["/app/migrate"]
diff --git a/docker/operator.Dockerfile b/docker/operator.Dockerfile
new file mode 100644
index 0000000..6e0b898
--- /dev/null
+++ b/docker/operator.Dockerfile
@@ -0,0 +1,39 @@
+# ---- Stage 1: Build ----
+FROM rust:1.94-bookworm AS builder
+
+ARG BUILD_TARGET=x86_64-unknown-linux-gnu
+ENV TARGET=${BUILD_TARGET}
+
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ pkg-config libssl-dev libclang-dev \
+ gcc g++ make \
+ && rm -rf /var/lib/apt/lists/*
+
+WORKDIR /build
+
+COPY Cargo.toml Cargo.lock ./
+COPY libs/config/ libs/config/
+COPY apps/operator/ apps/operator/
+
+RUN cargo fetch
+
+RUN --mount=type=cache,target=/usr/local/cargo/registry \
+ --mount=type=cache,target=/usr/local/cargo/git \
+ --mount=type=cache,target=target \
+ cargo build --release --package operator --target ${TARGET}
+
+# ---- Stage 2: Runtime ----
+FROM debian:bookworm-slim
+
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ ca-certificates libssl3 \
+ && rm -rf /var/lib/apt/lists/*
+
+WORKDIR /app
+COPY --from=builder /build/target/${TARGET}/release/operator /app/operator
+
+# The operator reads POD_NAMESPACE and OPERATOR_IMAGE_PREFIX from env.
+# It connects to the in-cluster Kubernetes API via the service account token.
+# All child resources are created in the operator's own namespace.
+ENV OPERATOR_LOG_LEVEL=info
+ENTRYPOINT ["/app/operator"]
diff --git a/docker/operator/deployment.yaml b/docker/operator/deployment.yaml
new file mode 100644
index 0000000..aed54d1
--- /dev/null
+++ b/docker/operator/deployment.yaml
@@ -0,0 +1,128 @@
+# ---- Namespace ----
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: code-system
+---
+# ---- ServiceAccount ----
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: code-operator
+ namespace: code-system
+---
+# ---- RBAC: Role ----
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+ name: code-operator
+ namespace: code-system
+rules:
+ # CRDs we manage
+ - apiGroups: ["code.dev"]
+ resources: ["apps", "gitservers", "emailworkers", "githooks", "migrates"]
+ verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
+
+ # Status subresources
+ - apiGroups: ["code.dev"]
+ resources: ["apps/status", "gitservers/status", "emailworkers/status", "githooks/status", "migrates/status"]
+ verbs: ["get", "patch", "update"]
+
+ # Child resources managed by App
+ - apiGroups: ["apps"]
+ resources: ["deployments"]
+ verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
+ - apiGroups: [""]
+ resources: ["services"]
+ verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
+
+ # Child resources managed by GitServer
+ - apiGroups: [""]
+ resources: ["persistentvolumeclaims"]
+ verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
+
+ # Child resources managed by GitHook
+ - apiGroups: [""]
+ resources: ["configmaps"]
+ verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
+
+ # Child resources managed by Migrate
+ - apiGroups: ["batch"]
+ resources: ["jobs"]
+ verbs: ["get", "list", "watch", "create", "update", "patch", "delete", "deletecollection"]
+
+ # Secrets (read-only for env var resolution)
+ - apiGroups: [""]
+ resources: ["secrets"]
+ verbs: ["get", "list", "watch"]
+---
+# ---- RBAC: RoleBinding ----
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+ name: code-operator
+ namespace: code-system
+subjects:
+ - kind: ServiceAccount
+ name: code-operator
+ namespace: code-system
+roleRef:
+ kind: Role
+ name: code-operator
+ apiGroup: rbac.authorization.k8s.io
+---
+# ---- Deployment ----
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: code-operator
+ namespace: code-system
+ labels:
+ app.kubernetes.io/name: code-operator
+ app.kubernetes.io/managed-by: code-operator
+ app.kubernetes.io/part-of: code-system
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app.kubernetes.io/name: code-operator
+ template:
+ metadata:
+ labels:
+ app.kubernetes.io/name: code-operator
+ app.kubernetes.io/managed-by: code-operator
+ app.kubernetes.io/part-of: code-system
+ spec:
+ serviceAccountName: code-operator
+ terminationGracePeriodSeconds: 10
+ volumes:
+ - name: tmp
+ emptyDir: {}
+ containers:
+ - name: operator
+ image: myapp/operator:latest
+ imagePullPolicy: IfNotPresent
+ env:
+ - name: OPERATOR_IMAGE_PREFIX
+ value: "myapp/"
+ - name: OPERATOR_LOG_LEVEL
+ value: "info"
+ - name: POD_NAMESPACE
+ valueFrom:
+ fieldRef:
+ fieldPath: metadata.namespace
+ resources:
+ requests:
+ cpu: 10m
+ memory: 64Mi
+ limits:
+ memory: 256Mi
+ volumeMounts:
+ - name: tmp
+ mountPath: /tmp
+ securityContext:
+ allowPrivilegeEscalation: false
+ readOnlyRootFilesystem: true
+ capabilities:
+ drop:
+ - ALL
diff --git a/docker/operator/example/code-system.yaml b/docker/operator/example/code-system.yaml
new file mode 100644
index 0000000..006d5ca
--- /dev/null
+++ b/docker/operator/example/code-system.yaml
@@ -0,0 +1,280 @@
+# Example: deploying the full code system into `code-system` namespace.
+#
+# Prerequisites:
+# 1. Install CRDs: kubectl apply -f ../crd/
+# 2. Install Operator: kubectl apply -f ../operator/deployment.yaml
+#
+# Then apply this file:
+# kubectl apply -f example/code-system.yaml
+
+apiVersion: v1
+kind: Secret
+metadata:
+ name: app-secrets
+ namespace: code-system
+type: Opaque
+stringData:
+ APP_DATABASE_URL: "postgres://user:password@postgres:5432/codedb?sslmode=disable"
+ APP_REDIS_URLS: "redis://redis:6379"
+ APP_SMTP_HOST: "smtp.example.com"
+ APP_SMTP_PORT: "587"
+ APP_SMTP_USERNAME: "noreply@example.com"
+ APP_SMTP_PASSWORD: "change-me"
+ APP_SMTP_FROM: "noreply@example.com"
+ APP_AI_BASIC_URL: "https://api.openai.com/v1"
+ APP_AI_API_KEY: "sk-change-me"
+ APP_SSH_SERVER_PRIVATE_KEY: |
+ -----BEGIN OPENSSH PRIVATE KEY-----
+ ... paste your SSH private key here ...
+ -----END OPENSSH PRIVATE KEY-----
+ APP_SSH_SERVER_PUBLIC_KEY: "ssh-ed25519 AAAAC3... your-pub-key"
+---
+# ---- App (main web service, 3 replicas) ----
+apiVersion: code.dev/v1
+kind: App
+metadata:
+ name: app
+ namespace: code-system
+spec:
+ image: myapp/app:latest
+ replicas: 3
+ imagePullPolicy: IfNotPresent
+ env:
+ - name: APP_DATABASE_URL
+ valueFrom:
+ secretRef:
+ name: app-secrets
+ secretName: app-secrets
+ secretKey: APP_DATABASE_URL
+ - name: APP_REDIS_URLS
+ valueFrom:
+ secretRef:
+ name: app-secrets
+ secretName: app-secrets
+ secretKey: APP_REDIS_URLS
+ - name: APP_SMTP_HOST
+ valueFrom:
+ secretRef:
+ name: app-secrets
+ secretName: app-secrets
+ secretKey: APP_SMTP_HOST
+ - name: APP_SMTP_USERNAME
+ valueFrom:
+ secretRef:
+ name: app-secrets
+ secretName: app-secrets
+ secretKey: APP_SMTP_USERNAME
+ - name: APP_SMTP_PASSWORD
+ valueFrom:
+ secretRef:
+ name: app-secrets
+ secretName: app-secrets
+ secretKey: APP_SMTP_PASSWORD
+ - name: APP_SMTP_FROM
+ valueFrom:
+ secretRef:
+ name: app-secrets
+ secretName: app-secrets
+ secretKey: APP_SMTP_FROM
+ - name: APP_AI_BASIC_URL
+ valueFrom:
+ secretRef:
+ name: app-secrets
+ secretName: app-secrets
+ secretKey: APP_AI_BASIC_URL
+ - name: APP_AI_API_KEY
+ valueFrom:
+ secretRef:
+ name: app-secrets
+ secretName: app-secrets
+ secretKey: APP_AI_API_KEY
+ - name: APP_DOMAIN_URL
+ value: "https://example.com"
+ - name: APP_LOG_LEVEL
+ value: "info"
+ resources:
+ requests:
+ cpu: 100m
+ memory: 128Mi
+ limits:
+ cpu: 500m
+ memory: 512Mi
+ livenessProbe:
+ port: 8080
+ path: /health
+ initialDelaySeconds: 10
+ readinessProbe:
+ port: 8080
+ path: /health
+ initialDelaySeconds: 5
+---
+# ---- GitServer (git HTTP + SSH, single instance) ----
+apiVersion: code.dev/v1
+kind: GitServer
+metadata:
+ name: gitserver
+ namespace: code-system
+spec:
+ image: myapp/gitserver:latest
+ imagePullPolicy: IfNotPresent
+ env:
+ - name: APP_DATABASE_URL
+ valueFrom:
+ secretRef:
+ name: app-secrets
+ secretName: app-secrets
+ secretKey: APP_DATABASE_URL
+ - name: APP_REDIS_URLS
+ valueFrom:
+ secretRef:
+ name: app-secrets
+ secretName: app-secrets
+ secretKey: APP_REDIS_URLS
+ - name: APP_SSH_SERVER_PRIVATE_KEY
+ valueFrom:
+ secretRef:
+ name: app-secrets
+ secretName: app-secrets
+ secretKey: APP_SSH_SERVER_PRIVATE_KEY
+ - name: APP_SSH_SERVER_PUBLIC_KEY
+ valueFrom:
+ secretRef:
+ name: app-secrets
+ secretName: app-secrets
+ secretKey: APP_SSH_SERVER_PUBLIC_KEY
+ - name: APP_SSH_DOMAIN
+ value: "git.example.com"
+ - name: APP_REPOS_ROOT
+ value: "/data/repos"
+ resources:
+ requests:
+ cpu: 100m
+ memory: 128Mi
+ limits:
+ cpu: 1000m
+ memory: 1Gi
+ sshServiceType: NodePort # Use LoadBalancer in production
+ sshPort: 22
+ httpPort: 8022
+ storageSize: 50Gi
+---
+# ---- EmailWorker (single instance) ----
+apiVersion: code.dev/v1
+kind: EmailWorker
+metadata:
+ name: email-worker
+ namespace: code-system
+spec:
+ image: myapp/email-worker:latest
+ imagePullPolicy: IfNotPresent
+ env:
+ - name: APP_DATABASE_URL
+ valueFrom:
+ secretRef:
+ name: app-secrets
+ secretName: app-secrets
+ secretKey: APP_DATABASE_URL
+ - name: APP_REDIS_URLS
+ valueFrom:
+ secretRef:
+ name: app-secrets
+ secretName: app-secrets
+ secretKey: APP_REDIS_URLS
+ - name: APP_SMTP_HOST
+ valueFrom:
+ secretRef:
+ name: app-secrets
+ secretName: app-secrets
+ secretKey: APP_SMTP_HOST
+ - name: APP_SMTP_USERNAME
+ valueFrom:
+ secretRef:
+ name: app-secrets
+ secretName: app-secrets
+ secretKey: APP_SMTP_USERNAME
+ - name: APP_SMTP_PASSWORD
+ valueFrom:
+ secretRef:
+ name: app-secrets
+ secretName: app-secrets
+ secretKey: APP_SMTP_PASSWORD
+ - name: APP_SMTP_FROM
+ valueFrom:
+ secretRef:
+ name: app-secrets
+ secretName: app-secrets
+ secretKey: APP_SMTP_FROM
+ resources:
+ requests:
+ cpu: 50m
+ memory: 64Mi
+ limits:
+ memory: 256Mi
+---
+# ---- GitHook (single instance) ----
+apiVersion: code.dev/v1
+kind: GitHook
+metadata:
+ name: git-hook
+ namespace: code-system
+spec:
+ image: myapp/git-hook:latest
+ imagePullPolicy: IfNotPresent
+ env:
+ - name: APP_DATABASE_URL
+ valueFrom:
+ secretRef:
+ name: app-secrets
+ secretName: app-secrets
+ secretKey: APP_DATABASE_URL
+ - name: APP_REDIS_URLS
+ valueFrom:
+ secretRef:
+ name: app-secrets
+ secretName: app-secrets
+ secretKey: APP_REDIS_URLS
+ resources:
+ requests:
+ cpu: 50m
+ memory: 64Mi
+ limits:
+ memory: 256Mi
+---
+# ---- Migrate (auto-triggered on apply) ----
+apiVersion: code.dev/v1
+kind: Migrate
+metadata:
+ name: migrate
+ namespace: code-system
+spec:
+ image: myapp/migrate:latest
+ command: up
+ backoffLimit: 3
+ env:
+ - name: APP_DATABASE_URL
+ valueFrom:
+ secretRef:
+ name: app-secrets
+ secretName: app-secrets
+ secretKey: APP_DATABASE_URL
+---
+# ---- Ingress (example for App) ----
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: app-ingress
+ namespace: code-system
+ annotations:
+ nginx.ingress.kubernetes.io/proxy-body-size: "100m"
+spec:
+ rules:
+ - host: example.com
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: app
+ port:
+ number: 80
diff --git a/docs/ARCHITECTURE-LAYERS.md b/docs/ARCHITECTURE-LAYERS.md
new file mode 100644
index 0000000..2332600
--- /dev/null
+++ b/docs/ARCHITECTURE-LAYERS.md
@@ -0,0 +1,903 @@
+# Code 项目架构分层图
+
+> 一个现代化的代码协作与团队沟通平台
+>
+> 技术栈:Rust (后端) + TypeScript/React (前端) + Kubernetes (部署)
+
+---
+
+## 系统全景架构
+
+```
+┌─────────────────────────────────────────────────────────────────────────────────────────────────────┐
+│ 用 户 层 │
+│ │
+│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
+│ │ Web 浏览器 │ │ Git 客户端 │ │ 外部 CI/CD │ │
+│ │ (React SPA) │ │ (git/SSH) │ │ (GitHub/GitLab) │ │
+│ └────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘ │
+└──────────────────┼────────────────────────────────┼────────────────────────────────┼────────────────┘
+ │ │ │
+ │ HTTP/WS │ Git Protocol │ Webhook
+ │ │ │
+┌──────────────────▼────────────────────────────────▼────────────────────────────────▼────────────────┐
+│ 接入层 (Ingress/LB) │
+│ │
+│ ┌──────────────────────────────────────────────────────────────────────────────────┐ │
+│ │ Load Balancer / K8s Ingress (:80/:443) │ │
+│ └──────────────────────┬──────────────────────┬──────────────────────┬─────────────┘ │
+└────────────────────────────────┼──────────────────────┼──────────────────────┼──────────────────────┘
+ │ │ │
+ │ REST API │ Git Ops │ Webhook
+ │ │ │
+┌────────────────────────────────▼──────────────────────▼──────────────────────▼──────────────────────┐
+│ 应 用 服 务 层 (apps/) │
+│ │
+│ ┌────────────────────┐ ┌────────────────────┐ ┌────────────────────┐ ┌────────────────────┐ │
+│ │ apps/app │ │ apps/gitserver │ │ apps/git-hook │ │ apps/email │ │
+│ │ 主 Web API 服务 │ │ Git HTTP/SSH 服务 │ │ Git Hook 处理器 │ │ 邮件发送 Worker │ │
+│ │ :8080 │ │ :8021/:2222 │ │ Worker │ │ Worker │ │
+│ │ HTTP + WebSocket │ │ HTTP + SSH │ │ 异步任务 │ │ 队列消费 │ │
+│ │ 多实例部署 │ │ 单实例 │ │ 单实例 │ │ 单实例 │ │
+│ └─────────┬──────────┘ └─────────┬──────────┘ └─────────┬──────────┘ └─────────┬──────────┘ │
+└─────────────┼───────────────────────┼───────────────────────┼───────────────────────┼───────────────┘
+ │ │ │ │
+ │ │ │ │
+┌─────────────▼───────────────────────▼───────────────────────▼───────────────────────▼───────────────┐
+│ 应 用 编 排 层 (apps/operator) │
+│ │
+│ ┌───────────────────────────────────────────────────────────────────────────────────────┐ │
+│ │ apps/operator (Kubernetes Operator) │ │
+│ │ │ │
+│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
+│ │ │ App CRD │ │GitSrv CRD│ │Email CRD │ │Hook CRD │ │Mig CRD │ │ │
+│ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │
+│ │ │ │ │ │ │ │ │
+│ │ ▼ ▼ ▼ ▼ ▼ │ │
+│ │ ┌──────────────────────────────────────────────────────────────────────────┐ │ │
+│ │ │ K8s 资源 (Deployments, Services, PVCs, Jobs) │ │ │
+│ │ └──────────────────────────────────────────────────────────────────────────┘ │ │
+│ └───────────────────────────────────────────────────────────────────────────────────────┘ │
+└────────────────────────────────────────────────────────────────────────────────────────────────────┘
+ │ │ │ │
+ │ │ │ │
+┌─────────────▼───────────────────────▼───────────────────────▼───────────────────────▼───────────────┐
+│ 业 务 逻 辑 层 (libs/service) │
+│ │
+│ ┌─────────────────────────────────────────────────────────────────────────────────────────┐ │
+│ │ AppService { 全局服务聚合 } │ │
+│ │ │ │
+│ │ ┌─────────┐ ┌────────┐ ┌─────┐ ┌──────────┐ ┌───────────┐ ┌────────┐ ┌───────────┐ │ │
+│ │ │ agent/ │ │ auth/ │ │git/ │ │ issue/ │ │ project/ │ │ user/ │ │ pull_req/ │ │ │
+│ │ │ (8文件) │ │ (10) │ │(16) │ │ (8) │ │ (20) │ │ (12) │ │ (5) │ │ │
+│ │ │ AI模型 │ │ 认证 │ │Git │ │ Issue │ │ 项目管理 │ │ 用户 │ │ PR审查 │ │ │
+│ │ │ 管理 │ │ 会话 │ │操作 │ │ 追踪 │ │ 权限控制 │ │ 偏好 │ │ 合并 │ │ │
+│ │ └─────────┘ └────────┘ └─────┘ └──────────┘ └───────────┘ └────────┘ └───────────┘ │ │
+│ │ │ │
+│ │ + utils/(project,repo,user) + ws_token + error + Pager │ │
+│ └──────────────────────────────────────┬──────────────────────────────────────────────────┘ │
+└──────────────────────────────────────────┼──────────────────────────────────────────────────────────┘
+ │
+ ┌──────────────────────┼──────────────────────┐
+ │ │ │
+┌───────────────────▼──────────┐ ┌────────▼─────────────┐ ┌──────▼────────────────────────────┐
+│ HTTP 路由层 (libs/api) │ │ WebSocket 层 │ │ 后台 Worker 层 │
+│ 100 个路由文件 │ │ (libs/room) │ │ │
+│ │ │ │ │ libs/queue: │
+│ /api/auth/* (9端点) │ │ /ws │ │ MessageProducer │
+│ /api/git/* (100+端点) │ │ /ws/rooms/{id} │ │ RedisPubSub │
+│ /api/projects/* (50+端点) │ │ /ws/projects/{id} │ │ room_worker_task │
+│ /api/issue/* (30+端点) │ │ │ │ start_email_worker │
+│ /api/room/* (40+端点) │ │ 实时消息广播 │ │ │
+│ /api/pull_request/* (20端点)│ │ 多实例同步 │ │ libs/git/hook: │
+│ /api/agent/* (15端点) │ │ AI 流式输出 │ │ GitServiceHooks │
+│ /api/user/* (20端点) │ │ │ │ GitHookPool │
+│ /api/openapi/* (文档) │ │ │ │ │
+└───────────┬────────────────┘ └──────────┬───────────┘ └─────────────┬───────────────────────┘
+ │ │ │
+ └─────────────────────────────┼───────────────────────────┘
+ │
+┌─────────────────────────────────────────▼────────────────────────────────────────────────────────┐
+│ 基 础 设 施 层 (Infrastructure Libs) │
+│ │
+│ ┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐ │
+│ │ libs/models │ │ libs/db │ │ libs/config │ │ libs/session │ │
+│ │ 92 个实体文件 │ │ 数据库连接池 │ │ 全局配置管理 │ │ 会话管理中间件 │ │
+│ │ Sea-ORM 实体定义 │ │ 缓存抽象 │ │ .env 加载 │ │ Redis Store │ │
+│ │ 类型别名 │ │ 重试机制 │ │ 12 子模块 │ │ JWT + Cookie │ │
+│ └─────────┬─────────┘ └─────────┬─────────┘ └─────────┬─────────┘ └─────────┬─────────┘ │
+│ │ │ │ │ │
+│ ┌─────────▼─────────┐ ┌────────▼─────────┐ ┌─────────▼─────────┐ ┌─────────▼─────────┐ │
+│ │ libs/git │ │ libs/agent │ │ libs/email │ │ libs/avatar │ │
+│ │ 19 子模块 │ │ 6 子模块 │ │ SMTP 邮件发送 │ │ 图片处理 │ │
+│ │ libgit2 封装 │ │ OpenAI 集成 │ │ lettre 客户端 │ │ image crate │ │
+│ │ HTTP + SSH 协议 │ │ Qdrant 向量库 │ │ 模板引擎 │ │ 缩放/裁剪 │ │
+│ └─────────┬─────────┘ └─────────┬─────────┘ └─────────┬─────────┘ └─────────┬─────────┘ │
+│ │ │ │ │ │
+│ ┌─────────▼─────────┐ ┌────────▼─────────┐ ┌─────────▼───────────────────────▼─────────┐ │
+│ │ libs/queue │ │ libs/room │ │ libs/migrate │ │
+│ │ 消息队列 │ │ 实时聊天室 │ │ 82+ 数据库迁移脚本 │ │
+│ │ Redis Streams │ │ 19 子模块 │ │ sea-orm-migration │ │
+│ │ Pub/Sub │ │ WebSocket 管理 │ │ up/down/fresh/refresh/reset │ │
+│ └─────────┬─────────┘ └─────────┬─────────┘ └─────────────────────────────────────────┘ │
+│ │ │ │
+│ ┌─────────▼─────────┐ ┌────────▼─────────┐ │
+│ │ libs/webhook │ │ libs/rpc │ libs/transport │
+│ │ (占位) │ │ (占位) │ (占位) │
+│ └───────────────────┘ └─────────────────┘ │
+└────────────────────────────────────────────────────────────────────────────────────────────────┘
+ │ │
+ │ │
+┌─────────────▼──────────────────────▼────────────────────────────────────────────────────────────┐
+│ 存 储 层 │
+│ │
+│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
+│ │ PostgreSQL │ │ Redis │ │ Qdrant │ │ 文件系统 │ │
+│ │ :5432 │ │ :6379 │ │ :6333 │ │ │ │
+│ │ │ │ │ │ │ │ /data/avatars │ │
+│ │ • 用户数据 │ │ • 会话存储 │ │ • 向量嵌入 │ │ /data/repos │ │
+│ │ • 项目/仓库 │ │ • 缓存数据 │ │ • AI 索引 │ │ • 头像图片 │ │
+│ │ • Issue/PR │ │ • Pub/Sub │ │ • 相似度检索 │ │ • Git 仓库 │ │
+│ │ • Room 消息 │ │ • Stream 队列 │ │ │ │ • 上传文件 │ │
+│ │ • 评论/标签 │ │ • Hook 队列 │ │ │ │ │ │
+│ └──────────────────┘ └──────────────────┘ └──────────────────┘ └──────────────────┘ │
+└────────────────────────────────────────────────────────────────────────────────────────────────┘
+ │
+ │ 外部 API
+ │
+┌─────────────▼────────────────────────────────────────────────────────────────────────────────────┐
+│ 外 部 服 务 │
+│ │
+│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
+│ │ SMTP 服务器 │ │ OpenAI API │ │ Embedding API │ │
+│ │ :587 │ │ HTTPS │ │ HTTPS │ │
+│ │ │ │ │ │ │ │
+│ │ • 邮件发送 │ │ • 聊天补全 │ │ • 文本向量化 │ │
+│ │ • 通知邮件 │ │ • AI 助手 │ │ • 相似度计算 │ │
+│ └──────────────────┘ └──────────────────┘ └──────────────────┘ │
+└────────────────────────────────────────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## 前端架构分层
+
+```
+┌────────────────────────────────────────────────────────────────────────────────────────────────┐
+│ 前 端 应 用 层 (src/) │
+│ │
+│ ┌──────────────────────────────────────────────────────────────────────────────────────────┐ │
+│ │ Vite + React + TypeScript │ │
+│ │ │ │
+│ │ src/main.tsx ──▶ App.tsx ──▶ BrowserRouter ──▶ Routes │ │
+│ │ │ │
+│ │ ┌───────────────────────┐ ┌───────────────────────┐ ┌───────────────────────┐ │ │
+│ │ │ 页面层 (app/) │ │ 组件层 (components/) │ │ 状态管理层 │ │ │
+│ │ │ 59 页面组件 │ │ 108 UI 组件 │ │ │ │ │
+│ │ │ │ │ │ │ TanStack Query │ │ │
+│ │ │ auth/ (4) │ │ ui/ (66) │ │ (服务端状态) │ │ │
+│ │ │ init/ (2) │ │ room/ (20) │ │ │ │ │
+│ │ │ user/ (1) │ │ repository/ (8) │ │ React Context │ │ │
+│ │ │ project/ (22) │ │ project/ (4) │ │ (全局状态) │ │ │
+│ │ │ repository/ (12) │ │ auth/ (2) │ │ │ │ │
+│ │ │ settings/ (8) │ │ layout/ (2) │ │ Local State │ │ │
+│ │ │ │ │ │ │ (组件状态) │ │ │
+│ │ └───────────┬───────────┘ └───────────┬───────────┘ └───────────┬────────────┘ │ │
+│ │ │ │ │ │ │
+│ │ └────────────────────────────┼────────────────────────────┘ │ │
+│ │ │ │ │
+│ │ ┌────────────────────────────────────────┼────────────────────────────────────────┐ │ │
+│ │ │ API 客户端层 │ │ │
+│ │ │ │ │ │
+│ │ │ src/client/ ──▶ openapi-ts 自动生成 (从 openapi.json) │ │ │
+│ │ │ 400+ API 函数 + 完整 TypeScript 类型 │ │ │
+│ │ │ Axios HTTP 客户端 │ │ │
+│ │ └──────────────────────────────────────────────────────────────────────────────────┘ │ │
+│ │ │ │
+│ │ ┌──────────────────────────────────────────────────────────────────────────────────┐ │ │
+│ │ │ 工具层 │ │ │
+│ │ │ │ │ │
+│ │ │ src/hooks/ ──▶ 自定义 React Hooks │ │ │
+│ │ │ src/lib/ ──▶ 工具函数 (api-error, rsa, date 等) │ │ │
+│ │ │ src/contexts/ ──▶ React Context (User, Theme 等) │ │ │
+│ │ │ src/assets/ ──▶ 静态资源 (图片、图标) │ │ │
+│ │ └──────────────────────────────────────────────────────────────────────────────────┘ │ │
+│ └──────────────────────────────────────────────────────────────────────────────────────────┘ │
+└────────────────────────────────────────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## 前端路由结构
+
+```
+/ 首页/仪表板
+│
+├── /auth/ 认证路由
+│ ├── /login 登录页
+│ ├── /register 注册页
+│ ├── /password/reset 密码重置
+│ └── /verify-email 邮箱验证
+│
+├── /init/ 初始化路由
+│ ├── /project 初始化项目
+│ └── /repository 初始化仓库
+│
+├── /user/:user 用户资料页
+│
+├── /settings/ 个人设置
+│ ├── /profile 个人资料
+│ ├── /account 账户设置
+│ ├── /security 安全设置
+│ ├── /tokens 访问令牌
+│ ├── /ssh-keys SSH 密钥
+│ ├── /preferences 偏好设置
+│ └── /activity 活动日志
+│
+├── /project/:project_name/ 项目路由
+│ ├── / 项目概览
+│ ├── /activity 项目活动
+│ ├── /repositories 仓库列表
+│ ├── /issues Issue 列表
+│ │ ├── /new 新建 Issue
+│ │ └── /:issueNumber Issue 详情
+│ ├── /boards 看板列表
+│ │ └── /:boardId 看板详情
+│ ├── /members 成员管理
+│ ├── /room 聊天室列表
+│ │ └── /:roomId 聊天室
+│ ├── /articles 文章
+│ ├── /resources 资源
+│ └── /settings/ 项目设置
+│ ├── /general 通用设置
+│ ├── /labels 标签管理
+│ ├── /billing 账单
+│ ├── /members 成员管理
+│ ├── /oauth OAuth 配置
+│ └── /webhook Webhook 管理
+│
+├── /repository/:namespace/:repoName/ 仓库路由
+│ ├── / 仓库概览
+│ ├── /branches 分支管理
+│ ├── /commits 提交历史
+│ │ └── /:oid 提交详情
+│ ├── /contributors 贡献者
+│ ├── /files 文件浏览
+│ ├── /tags 标签
+│ ├── /pull-requests PR 列表
+│ │ ├── /new 新建 PR
+│ │ └── /:prNumber PR 详情
+│ └── /settings 仓库设置
+│
+├── /search 全局搜索
+└── /notifications 通知中心
+```
+
+---
+
+## 后端服务依赖关系
+
+```
+┌──────────────────────────────────────────────────────────────────────────────────────┐
+│ apps/ 应用依赖关系 │
+│ │
+│ apps/app ────────────────┐ │
+│ apps/email ──────────────┤ │
+│ apps/git-hook ───────────┤──▶ libs/config (全局配置) │
+│ apps/gitserver ──────────┤──▶ libs/db (数据库连接池 + 缓存) │
+│ apps/migrate ────────────┤──▶ libs/session (会话管理) │
+│ apps/operator ───────────┘──▶ libs/migrate (数据库迁移) │
+│ ├──▶ libs/service (业务逻辑层) │
+│ │ │ │
+│ │ ├──▶ libs/api (HTTP 路由) │
+│ │ │ │
+│ │ ├──▶ libs/agent (AI 服务) │
+│ │ ├──▶ libs/avatar (头像处理) │
+│ │ ├──▶ libs/email (邮件发送) │
+│ │ ├──▶ libs/room (聊天室) │
+│ │ │ │ │
+│ │ │ └──▶ libs/queue (消息队列) │
+│ │ │ │
+│ │ └──▶ libs/git (Git 操作) │
+│ │ │ │
+│ │ ├──▶ git2 (libgit2 绑定) │
+│ │ ├──▶ git2-hooks (Git 钩子) │
+│ │ └──▶ russh (SSH 协议) │
+│ │ │
+│ └──▶ libs/models (数据模型 - 所有层共享) │
+│ │ │
+│ ├──▶ users/ (12 实体) │
+│ ├──▶ projects/ (19 实体) │
+│ ├──▶ repos/ (16 实体) │
+│ ├──▶ issues/ (10 实体) │
+│ ├──▶ pull_request/ (5 实体) │
+│ ├──▶ rooms/ (11 实体) │
+│ ├──▶ agents/ (6 实体) │
+│ ├──▶ ai/ (3 实体) │
+│ └──▶ system/ (3 实体) │
+└──────────────────────────────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## libs/models 实体分组
+
+```
+┌─────────────────────────────────────────────────────────────────────────┐
+│ libs/models 实体分组 (92 个) │
+├─────────────────────────────────────────────────────────────────────────┤
+│ │
+│ Users (12 实体) │
+│ ┌─────────────────────────────────────────────────────────────────┐ │
+│ │ user 用户基本信息 │ │
+│ │ user_2fa 双因素认证 │ │
+│ │ user_activity_log 用户活动日志 │ │
+│ │ user_email 用户邮箱 │ │
+│ │ user_email_change 邮箱变更历史 │ │
+│ │ user_notification 用户通知 │ │
+│ │ user_password 用户密码 │ │
+│ │ user_password_reset 密码重置令牌 │ │
+│ │ user_preferences 用户偏好设置 │ │
+│ │ user_relation 用户关系 │ │
+│ │ user_ssh_key SSH 密钥 │ │
+│ │ user_token 访问令牌 │ │
+│ └─────────────────────────────────────────────────────────────────┘ │
+│ │
+│ Projects (19 实体) │
+│ ┌─────────────────────────────────────────────────────────────────┐ │
+│ │ project 项目基本信息 │ │
+│ │ project_access_log 访问日志 │ │
+│ │ project_activity 活动记录 │ │
+│ │ project_audit_log 审计日志 │ │
+│ │ project_billing 账单信息 │ │
+│ │ project_billing_history 账单历史 │ │
+│ │ project_board 看板 │ │
+│ │ project_board_card 看板卡片 │ │
+│ │ project_board_column 看板列 │ │
+│ │ project_follow 项目关注 │ │
+│ │ project_history_name 历史名称 │ │
+│ │ project_label 项目标签 │ │
+│ │ project_like 项目点赞 │ │
+│ │ project_member_ 成员邀请 │ │
+│ │ invitations │ │
+│ │ project_member_join_ 加入问答 │ │
+│ │ answers │ │
+│ │ project_member_join_ 加入请求 │ │
+│ │ request │ │
+│ │ project_member_join_ 加入设置 │ │
+│ │ settings │ │
+│ │ project_members 项目成员 │ │
+│ │ project_watch 项目观看 │ │
+│ └─────────────────────────────────────────────────────────────────┘ │
+│ │
+│ Repos (16 实体) │
+│ ┌─────────────────────────────────────────────────────────────────┐ │
+│ │ repo 仓库基本信息 │ │
+│ │ repo_branch 分支信息 │ │
+│ │ repo_branch_protect 分支保护 │ │
+│ │ repo_collaborator 协作者 │ │
+│ │ repo_commit 提交记录 │ │
+│ │ repo_fork 仓库 Fork │ │
+│ │ repo_history_name 历史名称 │ │
+│ │ repo_hook Git 钩子 │ │
+│ │ repo_lfs_lock LFS 锁定 │ │
+│ │ repo_lfs_object LFS 对象 │ │
+│ │ repo_lock 仓库锁定 │ │
+│ │ repo_star 仓库星标 │ │
+│ │ repo_tag 仓库标签 │ │
+│ │ repo_upstream 上游仓库 │ │
+│ │ repo_watch 仓库观看 │ │
+│ │ repo_webhook 仓库 Webhook │ │
+│ └─────────────────────────────────────────────────────────────────┘ │
+│ │
+│ Issues (10 实体) │
+│ ┌─────────────────────────────────────────────────────────────────┐ │
+│ │ issue Issue 基本信息 │ │
+│ │ issue_assignee Issue 负责人 │ │
+│ │ issue_comment Issue 评论 │ │
+│ │ issue_comment_reaction 评论表情 │ │
+│ │ issue_label Issue 标签 │ │
+│ │ issue_pull_request Issue 关联 PR │ │
+│ │ issue_reaction Issue 表情 │ │
+│ │ issue_repo Issue 仓库 │ │
+│ │ issue_subscriber Issue 订阅者 │ │
+│ └─────────────────────────────────────────────────────────────────┘ │
+│ │
+│ Pull Requests (5 实体) │
+│ ┌─────────────────────────────────────────────────────────────────┐ │
+│ │ pull_request PR 基本信息 │ │
+│ │ pull_request_commit PR 提交记录 │ │
+│ │ pull_request_review PR 审查 │ │
+│ │ pull_request_review_ PR 审查评论 │ │
+│ │ comment │ │
+│ │ pull_request_review_ PR 审查请求 │ │
+│ │ request │ │
+│ └─────────────────────────────────────────────────────────────────┘ │
+│ │
+│ Rooms (11 实体) │
+│ ┌─────────────────────────────────────────────────────────────────┐ │
+│ │ room 聊天室基本信息 │ │
+│ │ room_ai 聊天室 AI 配置 │ │
+│ │ room_category 聊天室分类 │ │
+│ │ room_member 聊天室成员 │ │
+│ │ room_message 聊天消息 │ │
+│ │ room_message_edit_ 消息编辑历史 │ │
+│ │ history │ │
+│ │ room_message_reaction 消息表情 │ │
+│ │ room_notifications 聊天室通知 │ │
+│ │ room_pin 聊天室置顶 │ │
+│ │ room_thread 聊天室 Thread │ │
+│ └─────────────────────────────────────────────────────────────────┘ │
+│ │
+│ Agents (6 实体) │
+│ ┌─────────────────────────────────────────────────────────────────┐ │
+│ │ model AI 模型 │ │
+│ │ model_capability 模型能力 │ │
+│ │ model_parameter_profile 模型参数配置 │ │
+│ │ model_pricing 模型定价 │ │
+│ │ model_provider 模型提供商 │ │
+│ │ model_version 模型版本 │ │
+│ └─────────────────────────────────────────────────────────────────┘ │
+│ │
+│ AI (3 实体) │
+│ ┌─────────────────────────────────────────────────────────────────┐ │
+│ │ ai_session AI 会话 │ │
+│ │ ai_tool_auth AI 工具认证 │ │
+│ │ ai_tool_call AI 工具调用 │ │
+│ └─────────────────────────────────────────────────────────────────┘ │
+│ │
+│ System (3 实体) │
+│ ┌─────────────────────────────────────────────────────────────────┐ │
+│ │ label 系统标签 │ │
+│ │ notify 系统通知 │ │
+│ └─────────────────────────────────────────────────────────────────┘ │
+└─────────────────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## libs/service 业务模块
+
+```
+┌─────────────────────────────────────────────────────────────────────────┐
+│ libs/service 业务模块 (93 个文件) │
+├─────────────────────────────────────────────────────────────────────────┤
+│ │
+│ agent/ AI 模型管理 (8 文件) │
+│ ├── code_review AI 代码审查 │
+│ ├── model AI 模型管理 │
+│ ├── model_capability 模型能力管理 │
+│ ├── model_parameter_ 模型参数配置 │
+│ │ profile │
+│ ├── model_pricing 模型定价管理 │
+│ ├── model_version 模型版本管理 │
+│ ├── pr_summary PR 摘要生成 │
+│ └── provider 模型提供商管理 │
+│ │
+│ auth/ 认证管理 (10 文件) │
+│ ├── captcha 验证码管理 │
+│ ├── email 邮箱认证 │
+│ ├── login 登录逻辑 │
+│ ├── logout 登出逻辑 │
+│ ├── me 当前用户信息 │
+│ ├── password 密码管理 │
+│ ├── register 注册逻辑 │
+│ ├── rsa RSA 加密 │
+│ └── totp TOTP 双因素认证 │
+│ │
+│ git/ Git 操作 (16 文件) │
+│ ├── archive 仓库归档 │
+│ ├── blocking 阻塞操作 │
+│ ├── blame Git Blame │
+│ ├── blob Blob 操作 │
+│ ├── branch 分支操作 │
+│ ├── branch_ 分支保护 │
+│ │ protection │
+│ ├── commit 提交操作 │
+│ ├── contributors 贡献者统计 │
+│ ├── diff Diff 操作 │
+│ ├── init 仓库初始化 │
+│ ├── refs 引用操作 │
+│ ├── repo 仓库操作 │
+│ ├── star 星标操作 │
+│ ├── tag 标签操作 │
+│ ├── tree 树操作 │
+│ └── watch 观看操作 │
+│ │
+│ issue/ Issue 管理 (8 文件) │
+│ ├── assignee 负责人管理 │
+│ ├── comment 评论管理 │
+│ ├── issue Issue CRUD │
+│ ├── label 标签管理 │
+│ ├── pull_request Issue 关联 PR │
+│ ├── reaction 表情回应 │
+│ ├── repo 仓库 Issue │
+│ └── subscriber 订阅者管理 │
+│ │
+│ project/ 项目管理 (20 文件) │
+│ ├── activity 项目活动 │
+│ ├── audit 审计日志 │
+│ ├── avatar 项目头像 │
+│ ├── billing 账单管理 │
+│ ├── board 看板管理 │
+│ ├── can_use 权限检查 │
+│ ├── info 项目信息 │
+│ ├── init 项目初始化 │
+│ ├── invitation 邀请管理 │
+│ ├── join_answers 加入问答 │
+│ ├── join_request 加入请求 │
+│ ├── join_settings 加入设置 │
+│ ├── labels 标签管理 │
+│ ├── like 点赞管理 │
+│ ├── members 成员管理 │
+│ ├── repo 仓库管理 │
+│ ├── repo_ 仓库权限 │
+│ │ permission │
+│ ├── settings 项目设置 │
+│ ├── standard 项目标准 │
+│ ├── transfer_repo 仓库转移 │
+│ └── watch 观看管理 │
+│ │
+│ pull_request/ PR 管理 (5 文件) │
+│ ├── merge PR 合并 │
+│ ├── pull_request PR CRUD │
+│ ├── review PR 审查 │
+│ ├── review_comment 审查评论 │
+│ └── review_request 审查请求 │
+│ │
+│ user/ 用户管理 (12 文件) │
+│ ├── access_key 访问密钥 │
+│ ├── avatar 用户头像 │
+│ ├── chpc 用户 CHPC │
+│ ├── notification 通知管理 │
+│ ├── notify 通知发送 │
+│ ├── preferences 偏好设置 │
+│ ├── profile 用户资料 │
+│ ├── projects 用户项目 │
+│ ├── repository 用户仓库 │
+│ ├── ssh_key SSH 密钥 │
+│ ├── subscribe 订阅管理 │
+│ └── user_info 用户信息 │
+│ │
+│ utils/ 工具函数 (3 文件) │
+│ ├── project 项目工具 │
+│ ├── repo 仓库工具 │
+│ └── user 用户工具 │
+│ │
+│ ws_token WebSocket Token 服务 │
+│ error 服务层错误 │
+│ Pager 分页结构体 │
+└─────────────────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## libs/api 路由模块
+
+```
+┌─────────────────────────────────────────────────────────────────────────┐
+│ libs/api 路由模块 (100 个文件) │
+├─────────────────────────────────────────────────────────────────────────┤
+│ │
+│ auth/ 认证路由 (9 文件) │
+│ ├── captcha 验证码接口 │
+│ ├── email 邮箱认证接口 │
+│ ├── login 登录接口 │
+│ ├── logout 登出接口 │
+│ ├── me 当前用户接口 │
+│ ├── password 密码接口 │
+│ ├── register 注册接口 │
+│ ├── totp TOTP 接口 │
+│ └── ws_token WebSocket Token 接口 │
+│ │
+│ git/ Git 路由 (18 文件) │
+│ ├── archive 归档接口 │
+│ ├── blame Blame 接口 │
+│ ├── blob Blob 接口 │
+│ ├── branch 分支接口 │
+│ ├── branch_ 分支保护接口 │
+│ │ protection │
+│ ├── commit 提交接口 │
+│ ├── contributors 贡献者接口 │
+│ ├── diff Diff 接口 │
+│ ├── init 初始化接口 │
+│ ├── refs 引用接口 │
+│ ├── repo 仓库接口 │
+│ ├── star 星标接口 │
+│ ├── tag 标签接口 │
+│ ├── tree 树接口 │
+│ └── watch 观看接口 │
+│ │
+│ project/ 项目路由 (17 文件) │
+│ ├── activity 活动接口 │
+│ ├── audit 审计接口 │
+│ ├── billing 账单接口 │
+│ ├── board 看板接口 │
+│ ├── info 信息接口 │
+│ ├── init 初始化接口 │
+│ ├── invitation 邀请接口 │
+│ ├── join_answers 加入问答接口 │
+│ ├── join_request 加入请求接口 │
+│ ├── join_settings 加入设置接口 │
+│ ├── labels 标签接口 │
+│ ├── like 点赞接口 │
+│ ├── members 成员接口 │
+│ ├── repo 仓库接口 │
+│ ├── settings 设置接口 │
+│ ├── transfer_repo 仓库转移接口 │
+│ └── watch 观看接口 │
+│ │
+│ issue/ Issue 路由 (10 文件) │
+│ ├── assignee 负责人接口 │
+│ ├── comment 评论接口 │
+│ ├── comment_ 评论表情接口 │
+│ │ reaction │
+│ ├── issue_label Issue 标签接口 │
+│ ├── label 标签接口 │
+│ ├── pull_request Issue 关联 PR 接口 │
+│ ├── reaction 表情接口 │
+│ ├── repo 仓库 Issue 接口 │
+│ └── subscriber 订阅者接口 │
+│ │
+│ room/ 聊天室路由 (14 文件) │
+│ ├── ai AI 接口 │
+│ ├── category 分类接口 │
+│ ├── draft_and_ 草稿和历史接口 │
+│ │ history │
+│ ├── member 成员接口 │
+│ ├── message 消息接口 │
+│ ├── notification 通知接口 │
+│ ├── pin 置顶接口 │
+│ ├── reaction 表情接口 │
+│ ├── room 聊天室接口 │
+│ ├── thread Thread 接口 │
+│ ├── ws WebSocket 接口 │
+│ ├── ws_handler WebSocket 处理器 │
+│ ├── ws_types WebSocket 类型 │
+│ └── ws_universal 通用 WebSocket 接口 │
+│ │
+│ pull_request/ PR 路由 (5 文件) │
+│ ├── merge 合并接口 │
+│ ├── pull_request PR CRUD 接口 │
+│ ├── review 审查接口 │
+│ ├── review_comment 审查评论接口 │
+│ └── review_request 审查请求接口 │
+│ │
+│ agent/ AI Agent 路由 (8 文件) │
+│ ├── code_review 代码审查接口 │
+│ ├── model 模型接口 │
+│ ├── model_ 模型能力接口 │
+│ │ capability │
+│ ├── model_ 模型参数配置接口 │
+│ │ parameter_profile │
+│ ├── model_pricing 模型定价接口 │
+│ ├── model_version 模型版本接口 │
+│ ├── pr_summary PR 摘要接口 │
+│ └── provider 模型提供商接口 │
+│ │
+│ user/ 用户路由 (10 文件) │
+│ ├── access_key 访问密钥接口 │
+│ ├── chpc CHPC 接口 │
+│ ├── notification 通知接口 │
+│ ├── preferences 偏好接口 │
+│ ├── profile 资料接口 │
+│ ├── projects 项目接口 │
+│ ├── repository 仓库接口 │
+│ ├── ssh_key SSH 密钥接口 │
+│ ├── subscribe 订阅接口 │
+│ └── user_info 用户信息接口 │
+│ │
+│ openapi/ OpenAPI 文档生成 │
+│ route/ 路由聚合 │
+│ error/ API 错误处理 │
+└─────────────────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## 服务间通信机制
+
+```
+┌────────────────────────────────────────────────────────────────────────────────────────────┐
+│ 服务间通信机制 │
+│ │
+│ ┌──────────────────────────────────────────────────────────────────────────────────┐ │
+│ │ Redis (核心通信总线) │ │
+│ │ │ │
+│ │ Redis Streams ──▶ 异步消息队列 │ │
+│ │ ├── room:stream:{room_id} 房间消息持久化 │ │
+│ │ └── email:stream 邮件发送队列 │ │
+│ │ │ │
+│ │ Redis Pub/Sub ──▶ 实时事件广播 │ │
+│ │ ├── room:pub:{room_id} 房间级广播 │ │
+│ │ └── project:pub:{proj_id} 项目级广播 │ │
+│ │ │ │
+│ │ Redis Lists ──▶ 任务队列 │ │
+│ │ ├── {hook}:sync Git Hook 同步任务 │ │
+│ │ ├── {hook}:fsck Git Hook 完整性检查 │ │
+│ │ └── {hook}:gc Git Hook 垃圾回收 │ │
+│ └──────────────────────────────────────────────────────────────────────────────────┘ │
+│ │
+│ ┌──────────────────────────────────────────────────────────────────────────────────┐ │
+│ │ HTTP/REST API ──▶ 同步服务调用 │ │
+│ │ ├── app ↔ gitserver Git 元数据查询 │ │
+│ │ └── app → 外部 AI 服务 OpenAI 兼容 API 调用 │ │
+│ └──────────────────────────────────────────────────────────────────────────────────┘ │
+│ │
+│ ┌──────────────────────────────────────────────────────────────────────────────────┐ │
+│ │ WebSocket ──▶ 客户端实时通信 │ │
+│ │ ├── /ws 通用 WebSocket (多房间订阅) │ │
+│ │ ├── /ws/rooms/{room_id} 房间级 WebSocket │ │
+│ │ └── /ws/projects/{proj_id} 项目级 WebSocket │ │
+│ └──────────────────────────────────────────────────────────────────────────────────┘ │
+│ │
+│ ┌──────────────────────────────────────────────────────────────────────────────────┐ │
+│ │ Kubernetes CRD + Operator ──▶ 基础设施编排 │ │
+│ │ ├── apps.code.dev App CRD → Deployment + Service │ │
+│ │ ├── gitservers.code.dev GitServer CRD → Deployment + Service + PVC │ │
+│ │ ├── emailworkers.code.dev EmailWorker CRD → Deployment │ │
+│ │ ├── githooks.code.dev GitHook CRD → Deployment + ConfigMap │ │
+│ │ └── migrates.code.dev Migrate CRD → Job │ │
+│ └──────────────────────────────────────────────────────────────────────────────────┘ │
+└────────────────────────────────────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## 数据流详解
+
+### 1. 聊天消息流程
+
+```
+客户端 A app 实例 1 Redis app 实例 2 客户端 B
+ │ │ │ │ │
+ │── WS 发送消息 ───────▶│ │ │ │
+ │ │── XADD ──────────────▶│ │ │
+ │ │ room:stream:{id} │ │ │
+ │ │── PUBLISH ────────────▶│ │ │
+ │ │ room:pub:{id} │ │ │
+ │ │ │── 事件通知 ────────────▶│ │
+ │ │ │ │── WS 推送 ────────────▶│
+ │◀─ ACK ───────────────│ │ │ │
+ │ │ │ │ │
+ │ │◀──── XREADGROUP ─────│ │ │
+ │ │ (room_worker) │ │ │
+ │ │── 写入 PostgreSQL ────│ │ │
+```
+
+### 2. Git Push 流程
+
+```
+客户端 gitserver Redis git-hook PostgreSQL
+ │ │ │ │ │
+ │── git push ────────▶│ │ │ │
+ │ (HTTP/SSH) │ │ │ │
+ │ │── git-receive-pack──▶│ │ │
+ │ │── LPUSH ────────────▶│ │ │
+ │ │ {hook}:sync │ │ │
+ │◀─ ACK ─────────────│ │ │ │
+ │ │ │── BRPOPLPUSH ─────▶│ │
+ │ │ │ │── 同步元数据 ────────▶│
+ │ │ │ │── 可选: fsck/gc ─────▶│
+ │ │ │◀── XACK ──────────│ │
+```
+
+### 3. 邮件发送流程
+
+```
+业务逻辑 app Redis email-worker SMTP
+ │ │ │ │ │
+ │── 触发邮件 ────────▶│ │ │ │
+ │ │── XADD ───────────▶│ │ │
+ │ │ email:stream │ │ │
+ │◀─ 返回 ───────────│ │ │ │
+ │ │ │── XREADGROUP ─────▶│ │
+ │ │ │ │── 渲染模板 ──────────▶│
+ │ │ │ │── SMTP 发送 ─────────▶│
+ │ │ │◀── XACK ──────────│ │
+```
+
+### 4. AI 聊天流程
+
+```
+客户端 app OpenAI API Qdrant PostgreSQL
+ │ │ │ │ │
+ │── AI 消息 ──────────▶│ │ │ │
+ │ │── 生成 Embedding ──▶│ │ │
+ │ │◀──── 向量 ──────────│ │ │
+ │ │── 存储向量 ─────────────────────────────▶│ │
+ │ │── 流式 Chat ─────────▶│ │ │
+ │◀─ Stream Chunk ──────│◀──── Stream ─────────│ │ │
+ │ │ │ │ │
+ │ │── 保存消息 ────────────────────────────────────────────────▶│
+ │ │── 检索相似消息 ────────────────────────▶│ │
+ │ │◀── 相似结果 ───────────────────────────│ │
+```
+
+---
+
+## 技术栈汇总
+
+### 后端技术栈
+
+| 类别 | 技术 | 版本 |
+|------|------|------|
+| **语言** | Rust | Edition 2024 |
+| **Web 框架** | Actix-web | 4.13.0 |
+| **WebSocket** | Actix-ws | 0.4.0 |
+| **ORM** | SeaORM | 2.0.0-rc.37 |
+| **数据库** | PostgreSQL | - |
+| **缓存/消息** | Redis | 1.1.0 |
+| **向量库** | Qdrant | 1.17.0 |
+| **Git** | git2 / russh | 0.20.0 / 0.55.0 |
+| **邮件** | Lettre | 0.11.19 |
+| **AI** | async-openai | 0.34.0 |
+| **K8s** | kube-rs | 0.98 |
+| **gRPC** | Tonic | 0.14.5 |
+| **日志** | slog / tracing | 2.8 / 0.1.44 |
+
+### 前端技术栈
+
+| 类别 | 技术 | 版本 |
+|------|------|------|
+| **语言** | TypeScript | 5.9 |
+| **框架** | React | 19.2 |
+| **路由** | React Router | 7.13 |
+| **构建** | Vite + SWC | 8.0 |
+| **UI** | shadcn/ui + Tailwind | 4.11 / 4.2 |
+| **状态** | TanStack Query | 5.96 |
+| **HTTP** | Axios + OpenAPI 生成 | 1.7 |
+| **Markdown** | react-markdown + Shiki | 10 / 1 |
+| **拖拽** | dnd-kit | 6.3 |
+
+---
+
+## Docker 与 K8s 部署
+
+```
+┌──────────────────────────────────────────────────────────────────────────────┐
+│ Docker 镜像 (6 个) │
+│ │
+│ docker/app.Dockerfile ──▶ apps/app 主应用镜像 │
+│ docker/email-worker.Dockerfile ──▶ apps/email 邮件 Worker 镜像 │
+│ docker/git-hook.Dockerfile ──▶ apps/git-hook Git Hook 镜像 │
+│ docker/gitserver.Dockerfile ──▶ apps/gitserver Git Server 镜像 │
+│ docker/migrate.Dockerfile ──▶ apps/migrate 数据库迁移镜像 │
+│ docker/operator.Dockerfile ──▶ apps/operator K8s Operator 镜像 │
+└──────────────────────────────────────────────────────────────────────────────┘
+
+┌──────────────────────────────────────────────────────────────────────────────┐
+│ Kubernetes CRD (5 个) │
+│ │
+│ docker/crd/app-crd.yaml ──▶ apps.code.dev │
+│ docker/crd/gitserver-crd.yaml ──▶ gitservers.code.dev │
+│ docker/crd/email-worker-crd.yaml ──▶ emailworkers.code.dev │
+│ docker/crd/git-hook-crd.yaml ──▶ githooks.code.dev │
+│ docker/crd/migrate-crd.yaml ──▶ migrates.code.dev │
+└──────────────────────────────────────────────────────────────────────────────┘
+
+┌──────────────────────────────────────────────────────────────────────────────┐
+│ K8s 部署配置 │
+│ │
+│ docker/operator/deployment.yaml ──▶ Operator Deployment │
+│ docker/operator/example/ ──▶ CRD 使用示例 │
+│ code-system.yaml │
+└──────────────────────────────────────────────────────────────────────────────┘
+```
+
+---
+
+## 关键设计特点
+
+| 特点 | 描述 |
+|------|------|
+| **Monorepo 架构** | Rust workspace + 前端 monorepo,统一管理 |
+| **清晰分层** | 路由层 → 业务层 → 基础设施层 → 存储层,职责明确 |
+| **异步优先** | 基于 Redis Streams 的异步消息处理 |
+| **实时通信** | WebSocket + Redis Pub/Sub 实现多实例同步 |
+| **K8s 原生** | Operator + 5 个 CRD 管理全生命周期 |
+| **类型安全** | OpenAPI 自动生成 TypeScript 客户端 |
+| **可扩展** | 服务独立部署,水平扩展 |
+| **Git 兼容** | 完整支持 HTTP/SSH Git 协议 + LFS |
+| **AI 集成** | 原生集成 OpenAI 兼容 API + 向量检索 |
+| **92 个数据库实体** | 覆盖用户、项目、仓库、Issue、PR、聊天室、AI 等完整业务域 |
diff --git a/eslint.config.js b/eslint.config.js
new file mode 100644
index 0000000..ea00f0e
--- /dev/null
+++ b/eslint.config.js
@@ -0,0 +1,27 @@
+import js from '@eslint/js'
+import globals from 'globals'
+import reactHooks from 'eslint-plugin-react-hooks'
+import reactRefresh from 'eslint-plugin-react-refresh'
+import tseslint from 'typescript-eslint'
+import {defineConfig, globalIgnores} from 'eslint/config'
+
+export default defineConfig([
+ globalIgnores(['dist', 'src/client/**']),
+ {
+ files: ['**/*.{ts,tsx}'],
+ extends: [
+ js.configs.recommended,
+ tseslint.configs.recommended,
+ reactHooks.configs.flat.recommended,
+ reactRefresh.configs.vite,
+ ],
+ rules: {
+ // Disable set-state-in-effect as it's a valid pattern for initializing form state from server data
+ 'react-hooks/set-state-in-effect': 'off',
+ },
+ languageOptions: {
+ ecmaVersion: 2020,
+ globals: globals.browser,
+ },
+ },
+])
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..6384476
--- /dev/null
+++ b/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ GitDataAi
+
+
+
+
+
+
diff --git a/libs/agent-tool-derive/Cargo.toml b/libs/agent-tool-derive/Cargo.toml
new file mode 100644
index 0000000..1fa6e38
--- /dev/null
+++ b/libs/agent-tool-derive/Cargo.toml
@@ -0,0 +1,26 @@
+[package]
+name = "agent-tool-derive"
+version.workspace = true
+edition.workspace = true
+authors.workspace = true
+description.workspace = true
+repository.workspace = true
+readme.workspace = true
+homepage.workspace = true
+license.workspace = true
+keywords.workspace = true
+categories.workspace = true
+documentation.workspace = true
+
+[lib]
+proc-macro = true
+path = "src/lib.rs"
+
+[dependencies]
+syn = { version = "2", features = ["full", "extra-traits"] }
+quote = "1"
+proc-macro2 = "1"
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
+convert_case = "0.11"
+futures = "0.3"
\ No newline at end of file
diff --git a/libs/agent-tool-derive/src/lib.rs b/libs/agent-tool-derive/src/lib.rs
new file mode 100644
index 0000000..7ce2512
--- /dev/null
+++ b/libs/agent-tool-derive/src/lib.rs
@@ -0,0 +1,373 @@
+//! Procedural macro for generating tool definitions from functions.
+//!
+//! # Example
+//!
+//! ```
+//! use agent_tool_derive::tool;
+//!
+//! #[tool(description = "Search issues by title")]
+//! fn search_issues(
+//! title: String,
+//! status: Option,
+//! ) -> Result, String> {
+//! Ok(vec![])
+//! }
+//! ```
+//!
+//! Generates:
+//! - A `SearchIssuesParameters` struct (serde Deserialize)
+//! - A `SEARCH_ISSUES_DEFINITION: ToolDefinition` constant
+//! - A `register_search_issues(registry: &mut ToolRegistry)` helper
+
+extern crate proc_macro;
+
+use convert_case::{Case, Casing};
+use proc_macro::TokenStream;
+use quote::{format_ident, quote};
+use std::collections::HashMap;
+use syn::punctuated::Punctuated;
+use syn::{
+ Expr, ExprLit, Ident, Lit, Meta, ReturnType, Token, Type,
+ parse::{Parse, ParseStream},
+};
+
+/// Parse the attribute arguments: `description = "...", params(...), required(...)`
+struct ToolArgs {
+ description: Option,
+ param_descriptions: HashMap,
+ required: Vec,
+}
+
+impl Parse for ToolArgs {
+ fn parse(input: ParseStream) -> syn::Result {
+ Self::parse_from(input)
+ }
+}
+
+impl ToolArgs {
+ fn new() -> Self {
+ Self {
+ description: None,
+ param_descriptions: HashMap::new(),
+ required: Vec::new(),
+ }
+ }
+
+ fn parse_from(input: ParseStream) -> syn::Result {
+ let mut this = Self::new();
+ if input.is_empty() {
+ return Ok(this);
+ }
+
+ let meta_list: Punctuated = Punctuated::parse_terminated(input)?;
+
+ for meta in meta_list {
+ match meta {
+ Meta::NameValue(nv) => {
+ let ident = nv
+ .path
+ .get_ident()
+ .ok_or_else(|| syn::Error::new_spanned(&nv.path, "expected identifier"))?;
+ if ident == "description" {
+ if let Expr::Lit(ExprLit {
+ lit: Lit::Str(s), ..
+ }) = nv.value
+ {
+ this.description = Some(s.value());
+ } else {
+ return Err(syn::Error::new_spanned(
+ &nv.value,
+ "description must be a string literal",
+ ));
+ }
+ }
+ }
+ Meta::List(list) if list.path.is_ident("params") => {
+ let inner: Punctuated =
+ list.parse_args_with(Punctuated::parse_terminated)?;
+ for item in inner {
+ if let Meta::NameValue(nv) = item {
+ let param_name = nv
+ .path
+ .get_ident()
+ .ok_or_else(|| {
+ syn::Error::new_spanned(&nv.path, "expected identifier")
+ })?
+ .to_string();
+ if let Expr::Lit(ExprLit {
+ lit: Lit::Str(s), ..
+ }) = nv.value
+ {
+ this.param_descriptions.insert(param_name, s.value());
+ }
+ }
+ }
+ }
+ Meta::List(list) if list.path.is_ident("required") => {
+ let required_vars: Punctuated =
+ list.parse_args_with(Punctuated::parse_terminated)?;
+ for var in required_vars {
+ this.required.push(var.to_string());
+ }
+ }
+ _ => {}
+ }
+ }
+ Ok(this)
+ }
+}
+
+/// Map a Rust type to its JSON Schema type name.
+fn json_type(ty: &Type) -> proc_macro2::TokenStream {
+ use syn::Type as T;
+ let segs = match ty {
+ T::Path(p) => &p.path.segments,
+ _ => return quote! { "type": "object" },
+ };
+ let last = segs.last().map(|s| &s.ident);
+ let args = segs.last().and_then(|s| {
+ if let syn::PathArguments::AngleBracketed(a) = &s.arguments {
+ Some(&a.args)
+ } else {
+ None
+ }
+ });
+
+ match (last.map(|i| i.to_string()).as_deref(), args) {
+ (Some("Vec" | "vec::Vec"), Some(args)) if !args.is_empty() => {
+ if let syn::GenericArgument::Type(inner) = &args[0] {
+ let inner_type = json_type(inner);
+ return quote! {
+ {
+ "type": "array",
+ "items": { #inner_type }
+ }
+ };
+ }
+ quote! { "type": "array" }
+ }
+ (Some("String" | "str" | "char"), _) => quote! { "type": "string" },
+ (Some("bool"), _) => quote! { "type": "boolean" },
+ (Some("i8" | "i16" | "i32" | "i64" | "isize"), _) => quote! { "type": "integer" },
+ (Some("u8" | "u16" | "u32" | "u64" | "usize"), _) => quote! { "type": "integer" },
+ (Some("f32" | "f64"), _) => quote! { "type": "number" },
+ _ => quote! { "type": "object" },
+ }
+}
+
+/// Extract return type info from `-> Result`.
+fn parse_return_type(
+ ret: &ReturnType,
+) -> syn::Result<(proc_macro2::TokenStream, proc_macro2::TokenStream)> {
+ match ret {
+ ReturnType::Type(_, ty) => {
+ let ty = &**ty;
+ if let Type::Path(p) = ty {
+ let last = p
+ .path
+ .segments
+ .last()
+ .ok_or_else(|| syn::Error::new_spanned(&p.path, "invalid return type"))?;
+ if last.ident == "Result" {
+ if let syn::PathArguments::AngleBracketed(a) = &last.arguments {
+ let args = &a.args;
+ if args.len() == 2 {
+ let ok = &args[0];
+ let err = &args[1];
+ return Ok((quote!(#ok), quote!(#err)));
+ }
+ }
+ return Err(syn::Error::new_spanned(
+ &last,
+ "Result must have 2 type parameters",
+ ));
+ }
+ }
+ Err(syn::Error::new_spanned(
+ ty,
+ "function must return Result",
+ ))
+ }
+ _ => Err(syn::Error::new_spanned(
+ ret,
+ "function must have a return type",
+ )),
+ }
+}
+
+/// The `#[tool]` attribute macro.
+///
+/// Usage:
+/// ```
+/// #[tool(description = "Tool description", params(
+/// arg1 = "Description of arg1",
+/// arg2 = "Description of arg2",
+/// ))]
+/// async fn my_tool(arg1: String, arg2: Option) -> Result {
+/// Ok(serde_json::json!({}))
+/// }
+/// ```
+///
+/// Generates:
+/// - `MyToolParameters` struct with serde Deserialize
+/// - `MY_TOOL_DEFINITION: ToolDefinition` constant
+/// - `register_my_tool(registry: &mut ToolRegistry)` helper function
+#[proc_macro_attribute]
+pub fn tool(args: TokenStream, input: TokenStream) -> TokenStream {
+ let args = syn::parse_macro_input!(args as ToolArgs);
+ let input_fn = syn::parse_macro_input!(input as syn::ItemFn);
+
+ let fn_name = &input_fn.sig.ident;
+ let fn_name_str = fn_name.to_string();
+ let vis = &input_fn.vis;
+ let is_async = input_fn.sig.asyncness.is_some();
+
+ // Parse return type: Result
+ let (_output_type, _error_type) = match parse_return_type(&input_fn.sig.output) {
+ Ok(t) => t,
+ Err(e) => return e.into_compile_error().into(),
+ };
+
+ // PascalCase struct name
+ let struct_name = format_ident!("{}", fn_name_str.to_case(Case::Pascal));
+ let params_struct_name = format_ident!("{}Parameters", struct_name);
+ let definition_const_name = format_ident!("{}_DEFINITION", fn_name_str.to_uppercase());
+ let register_fn_name = format_ident!("register_{}", fn_name_str);
+
+ // Extract parameters from function signature
+ let mut param_names: Vec = Vec::new();
+ let mut param_types: Vec = Vec::new();
+ let mut param_json_types: Vec = Vec::new();
+ let mut param_descs: Vec = Vec::new();
+
+ let required_args = args.required.clone();
+
+ for arg in &input_fn.sig.inputs {
+ let syn::FnArg::Typed(pat_type) = arg else {
+ continue;
+ };
+ let syn::Pat::Ident(pat_ident) = &*pat_type.pat else {
+ continue;
+ };
+ let name = &pat_ident.ident;
+ let ty = &*pat_type.ty;
+
+ let name_str = name.to_string();
+ let desc = args
+ .param_descriptions
+ .get(&name_str)
+ .map(|s| quote! { #s.to_string() })
+ .unwrap_or_else(|| quote! { format!("Parameter {}", #name_str) });
+
+ param_names.push(format_ident!("{}", name.to_string()));
+ param_types.push(ty.clone());
+ param_json_types.push(json_type(ty));
+ param_descs.push(desc);
+ }
+
+ // Which params are required (not Option)
+ let required: Vec = if required_args.is_empty() {
+ param_names
+ .iter()
+ .filter(|name| {
+ let name_str = name.to_string();
+ !args
+ .param_descriptions
+ .contains_key(&format!("{}_opt", name_str))
+ })
+ .map(|name| quote! { stringify!(#name) })
+ .collect()
+ } else {
+ required_args.iter().map(|s| quote! { #s }).collect()
+ };
+
+ // Tool description
+ let tool_description = args
+ .description
+ .map(|s| quote! { #s.to_string() })
+ .unwrap_or_else(|| quote! { format!("Function {}", #fn_name_str) });
+
+ // Call invocation (async vs sync)
+ let call_args = param_names.iter().map(|n| quote! { args.#n });
+ let fn_call = if is_async {
+ quote! { #fn_name(#(#call_args),*).await }
+ } else {
+ quote! { #fn_name(#(#call_args),*) }
+ };
+
+ let expanded = quote! {
+ // Parameters struct: deserialized from JSON args by serde
+ #[derive(serde::Deserialize)]
+ #vis struct #params_struct_name {
+ #(#vis #param_names: #param_types,)*
+ }
+
+ // Keep the original function unchanged
+ #input_fn
+
+ // Static ToolDefinition constant — register this with ToolRegistry
+ #vis const #definition_const_name: agent::ToolDefinition = agent::ToolDefinition {
+ name: #fn_name_str.to_string(),
+ description: Some(#tool_description),
+ parameters: Some(agent::ToolSchema {
+ schema_type: "object".to_string(),
+ properties: Some({
+ let mut map = std::collections::HashMap::new();
+ #({
+ map.insert(stringify!(#param_names).to_string(), agent::ToolParam {
+ name: stringify!(#param_names).to_string(),
+ param_type: {
+ let jt = #param_json_types;
+ jt.get("type")
+ .and_then(|v| v.as_str())
+ .unwrap_or("object")
+ .to_string()
+ },
+ description: Some(#param_descs),
+ required: true,
+ properties: None,
+ items: None,
+ });
+ })*
+ map
+ }),
+ required: Some(vec![#(#required.to_string()),*]),
+ }),
+ strict: false,
+ };
+
+ /// Registers this tool in the given registry.
+ ///
+ /// Generated by `#[tool]` macro for function `#fn_name_str`.
+ #vis fn #register_fn_name(registry: &mut agent::ToolRegistry) {
+ let def = #definition_const_name.clone();
+ let fn_name = #fn_name_str.to_string();
+ registry.register_fn(fn_name, move |_ctx, args| {
+ let args: #params_struct_name = match serde_json::from_value(args) {
+ Ok(a) => a,
+ Err(e) => {
+ return std::pin::Pin::new(Box::new(async move {
+ Err(agent::ToolError::ParseError(e.to_string()))
+ }))
+ }
+ };
+ std::pin::Pin::new(Box::new(async move {
+ let result = #fn_call;
+ match result {
+ Ok(v) => Ok(serde_json::to_value(v).unwrap_or(serde_json::Value::Null)),
+ Err(e) => Err(agent::ToolError::ExecutionError(e.to_string())),
+ }
+ }))
+ });
+ }
+ };
+
+ // We need to use boxed futures for the return type.
+ // Since we can't add runtime dependencies to the proc-macro crate,
+ // we emit the .boxed() call and the caller must ensure
+ // `use futures::FutureExt;` or equivalent is in scope.
+ // The generated code requires: `futures::FutureExt` (for .boxed()).
+
+ // Re-emit with futures dependency note
+ TokenStream::from(expanded)
+}
diff --git a/libs/agent/Cargo.toml b/libs/agent/Cargo.toml
new file mode 100644
index 0000000..ecb2bee
--- /dev/null
+++ b/libs/agent/Cargo.toml
@@ -0,0 +1,37 @@
+[package]
+name = "agent"
+version.workspace = true
+edition.workspace = true
+authors.workspace = true
+description.workspace = true
+repository.workspace = true
+readme.workspace = true
+homepage.workspace = true
+license.workspace = true
+keywords.workspace = true
+categories.workspace = true
+documentation.workspace = true
+[lib]
+path = "lib.rs"
+name = "agent"
+[dependencies]
+async-openai = { version = "0.34.0", features = ["embedding", "chat-completion", "model"] }
+tokio = { workspace = true }
+async-trait = { workspace = true }
+qdrant-client = { workspace = true }
+sea-orm = { workspace = true }
+serde = { workspace = true, features = ["derive"] }
+serde_json = { workspace = true }
+thiserror = { workspace = true }
+db = { workspace = true }
+config = { path = "../config" }
+models = { workspace = true }
+chrono = { workspace = true }
+uuid = { workspace = true }
+futures = { workspace = true }
+tiktoken-rs = { workspace = true }
+agent-tool-derive = { path = "../agent-tool-derive" }
+once_cell = { workspace = true }
+regex = { workspace = true }
+[lints]
+workspace = true
diff --git a/libs/agent/chat/context.rs b/libs/agent/chat/context.rs
new file mode 100644
index 0000000..438d632
--- /dev/null
+++ b/libs/agent/chat/context.rs
@@ -0,0 +1,200 @@
+use async_openai::types::chat::{
+ ChatCompletionRequestAssistantMessage, ChatCompletionRequestAssistantMessageContent,
+ ChatCompletionRequestDeveloperMessage, ChatCompletionRequestDeveloperMessageContent,
+ ChatCompletionRequestFunctionMessage, ChatCompletionRequestMessage,
+ ChatCompletionRequestSystemMessage, ChatCompletionRequestSystemMessageContent,
+ ChatCompletionRequestToolMessage, ChatCompletionRequestToolMessageContent,
+ ChatCompletionRequestUserMessage, ChatCompletionRequestUserMessageContent,
+};
+use chrono::{DateTime, Utc};
+use serde::{Deserialize, Serialize};
+use serde_json::Value;
+use std::collections::HashMap;
+use uuid::Uuid;
+
+use crate::compact::MessageSummary;
+use models::rooms::room_message::Model as RoomMessageModel;
+
+/// Sender type for AI context, supporting all roles in the chat.
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub enum AiContextSenderType {
+ /// Regular user message
+ User,
+ /// AI assistant message
+ Ai,
+ /// System message (e.g., summary, notification)
+ System,
+ /// Developer message (for system-level instructions)
+ Developer,
+ /// Tool call message
+ Function,
+ /// Tool result message
+ FunctionResult,
+}
+
+impl AiContextSenderType {
+ pub fn from_sender_type(sender_type: &models::rooms::MessageSenderType) -> Self {
+ match sender_type {
+ models::rooms::MessageSenderType::Member => Self::User,
+ models::rooms::MessageSenderType::Admin => Self::User,
+ models::rooms::MessageSenderType::Owner => Self::User,
+ models::rooms::MessageSenderType::Ai => Self::Ai,
+ models::rooms::MessageSenderType::System => Self::System,
+ models::rooms::MessageSenderType::Tool => Self::Function,
+ models::rooms::MessageSenderType::Guest => Self::User,
+ }
+ }
+}
+
+/// Room message context for AI processing.
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct RoomMessageContext {
+ pub uid: Uuid,
+ pub sender_type: AiContextSenderType,
+ pub sender_uid: Option,
+ pub sender_name: Option,
+ pub content: String,
+ pub content_type: models::rooms::MessageContentType,
+ pub send_at: DateTime,
+ /// Tool call ID for FunctionResult messages, used to associate tool results with their calls.
+ pub tool_call_id: Option,
+}
+
+impl RoomMessageContext {
+ pub fn from_model(model: &RoomMessageModel, sender_name: Option) -> Self {
+ Self {
+ uid: model.id,
+ sender_type: AiContextSenderType::from_sender_type(&model.sender_type),
+ sender_uid: model.sender_id,
+ sender_name,
+ content: model.content.clone(),
+ content_type: model.content_type.clone(),
+ send_at: model.send_at,
+ tool_call_id: Self::extract_tool_call_id(&model.content),
+ }
+ }
+
+ fn extract_tool_call_id(content: &str) -> Option {
+ let content = content.trim();
+ if let Ok(v) = serde_json::from_str::(content) {
+ v.get("tool_call_id")
+ .and_then(|v| v.as_str())
+ .map(|s| s.to_string())
+ } else {
+ None
+ }
+ }
+
+ pub fn from_model_with_names(
+ model: &RoomMessageModel,
+ user_names: &HashMap,
+ ) -> Self {
+ let sender_name = model
+ .sender_id
+ .and_then(|uid| user_names.get(&uid).cloned());
+ Self::from_model(model, sender_name)
+ }
+
+ pub fn to_message(&self) -> ChatCompletionRequestMessage {
+ match self.sender_type {
+ AiContextSenderType::User => {
+ ChatCompletionRequestMessage::User(ChatCompletionRequestUserMessage {
+ content: ChatCompletionRequestUserMessageContent::Text(self.display_content()),
+ name: self.sender_name.clone(),
+ })
+ }
+ AiContextSenderType::Ai => {
+ ChatCompletionRequestMessage::Assistant(ChatCompletionRequestAssistantMessage {
+ content: Some(ChatCompletionRequestAssistantMessageContent::Text(
+ self.display_content(),
+ )),
+ name: self.sender_name.clone(),
+ refusal: None,
+ audio: None,
+ tool_calls: None,
+ #[allow(deprecated)]
+ function_call: None,
+ })
+ }
+ AiContextSenderType::System => {
+ ChatCompletionRequestMessage::System(ChatCompletionRequestSystemMessage {
+ content: ChatCompletionRequestSystemMessageContent::Text(
+ self.display_content(),
+ ),
+ name: self.sender_name.clone(),
+ })
+ }
+ AiContextSenderType::Developer => {
+ ChatCompletionRequestMessage::Developer(ChatCompletionRequestDeveloperMessage {
+ content: ChatCompletionRequestDeveloperMessageContent::Text(
+ self.display_content(),
+ ),
+ name: self.sender_name.clone(),
+ })
+ }
+ AiContextSenderType::Function => {
+ ChatCompletionRequestMessage::Function(ChatCompletionRequestFunctionMessage {
+ content: Some(self.content.clone()),
+ name: self.display_content(), // Function name is stored in content
+ })
+ }
+ AiContextSenderType::FunctionResult => {
+ ChatCompletionRequestMessage::Tool(ChatCompletionRequestToolMessage {
+ content: ChatCompletionRequestToolMessageContent::Text(self.display_content()),
+ tool_call_id: self
+ .tool_call_id
+ .clone()
+ .unwrap_or_else(|| "unknown".to_string()),
+ })
+ }
+ }
+ }
+
+ fn display_content(&self) -> String {
+ let mut content = self.content.trim().to_string();
+ if content.is_empty() {
+ content = match self.content_type {
+ models::rooms::MessageContentType::Text => "[empty]".to_string(),
+ models::rooms::MessageContentType::Image => "[image]".to_string(),
+ models::rooms::MessageContentType::Audio => "[audio]".to_string(),
+ models::rooms::MessageContentType::Video => "[video]".to_string(),
+ models::rooms::MessageContentType::File => "[file]".to_string(),
+ };
+ }
+
+ if let Some(sender_name) = &self.sender_name {
+ content = format!("[{}] {}", sender_name, content);
+ }
+
+ content
+ }
+}
+
+impl From<&RoomMessageModel> for RoomMessageContext {
+ fn from(model: &RoomMessageModel) -> Self {
+ RoomMessageContext::from_model(model, None)
+ }
+}
+
+impl From for RoomMessageContext {
+ fn from(summary: MessageSummary) -> Self {
+ // Map MessageSenderType to AiContextSenderType
+ let sender_type = AiContextSenderType::from_sender_type(&summary.sender_type);
+ // For FunctionResult (tool results), ensure tool_call_id is set
+ let tool_call_id = if sender_type == AiContextSenderType::FunctionResult {
+ summary.tool_call_id
+ } else {
+ None
+ };
+ Self {
+ uid: summary.id,
+ sender_type,
+ sender_uid: summary.sender_id,
+ sender_name: Some(summary.sender_name),
+ content: summary.content,
+ content_type: summary.content_type,
+ send_at: summary.send_at,
+ tool_call_id,
+ }
+ }
+}
diff --git a/libs/agent/chat/mod.rs b/libs/agent/chat/mod.rs
new file mode 100644
index 0000000..57723e1
--- /dev/null
+++ b/libs/agent/chat/mod.rs
@@ -0,0 +1,61 @@
+use std::pin::Pin;
+
+use async_openai::types::chat::ChatCompletionTool;
+use db::cache::AppCache;
+use db::database::AppDatabase;
+use models::agents::model;
+use models::projects::project;
+use models::repos::repo;
+use models::rooms::{room, room_message};
+use models::users::user;
+use std::collections::HashMap;
+use uuid::Uuid;
+
+/// Maximum recursion rounds for tool-call loops (AI → tool → result → AI).
+pub const DEFAULT_MAX_TOOL_DEPTH: usize = 3;
+
+/// A single chunk from an AI streaming response.
+#[derive(Debug, Clone)]
+pub struct AiStreamChunk {
+ pub content: String,
+ pub done: bool,
+}
+
+/// Optional streaming callback: called for each token chunk.
+pub type StreamCallback = Box<
+ dyn Fn(AiStreamChunk) -> Pin + Send>> + Send + Sync,
+>;
+
+pub struct AiRequest {
+ pub db: AppDatabase,
+ pub cache: AppCache,
+ pub model: model::Model,
+ pub project: project::Model,
+ pub sender: user::Model,
+ pub room: room::Model,
+ pub input: String,
+ pub mention: Vec,
+ pub history: Vec,
+ /// Optional user name mapping: user_id -> username
+ pub user_names: HashMap,
+ pub temperature: f64,
+ pub max_tokens: i32,
+ pub top_p: f64,
+ pub frequency_penalty: f64,
+ pub presence_penalty: f64,
+ pub think: bool,
+ /// OpenAI tool definitions. If None or empty, tool calling is disabled.
+ pub tools: Option>,
+ /// Maximum tool-call recursion depth (AI → tool → result → AI loops). Default: 3.
+ pub max_tool_depth: usize,
+}
+
+pub enum Mention {
+ User(user::Model),
+ Repo(repo::Model),
+}
+
+pub mod context;
+pub mod service;
+pub use context::{AiContextSenderType, RoomMessageContext};
+pub use service::ChatService;
diff --git a/libs/agent/chat/service.rs b/libs/agent/chat/service.rs
new file mode 100644
index 0000000..7dd2a15
--- /dev/null
+++ b/libs/agent/chat/service.rs
@@ -0,0 +1,655 @@
+use async_openai::Client;
+use async_openai::config::OpenAIConfig;
+use async_openai::types::chat::{
+ ChatCompletionMessageToolCalls, ChatCompletionRequestAssistantMessage,
+ ChatCompletionRequestAssistantMessageContent, ChatCompletionRequestMessage,
+ ChatCompletionRequestSystemMessage, ChatCompletionRequestUserMessage, ChatCompletionTool,
+ ChatCompletionTools, CreateChatCompletionRequest, CreateChatCompletionResponse,
+ CreateChatCompletionStreamResponse, FinishReason, ReasoningEffort, ToolChoiceOptions,
+};
+use futures::StreamExt;
+use models::projects::project_skill;
+use models::rooms::room_ai;
+use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
+use uuid::Uuid;
+
+use super::context::RoomMessageContext;
+use super::{AiRequest, AiStreamChunk, Mention, StreamCallback};
+use crate::compact::{CompactConfig, CompactService};
+use crate::embed::EmbedService;
+use crate::error::{AgentError, Result};
+use crate::perception::{PerceptionService, SkillEntry, ToolCallEvent};
+use crate::tool::{ToolCall, ToolContext, ToolExecutor};
+
+/// Service for handling AI chat requests in rooms.
+pub struct ChatService {
+ openai_client: Client,
+ compact_service: Option,
+ embed_service: Option,
+ perception_service: PerceptionService,
+}
+
+impl ChatService {
+ pub fn new(openai_client: Client) -> Self {
+ Self {
+ openai_client,
+ compact_service: None,
+ embed_service: None,
+ perception_service: PerceptionService::default(),
+ }
+ }
+
+ pub fn with_compact_service(mut self, compact_service: CompactService) -> Self {
+ self.compact_service = Some(compact_service);
+ self
+ }
+
+ pub fn with_embed_service(mut self, embed_service: EmbedService) -> Self {
+ self.embed_service = Some(embed_service);
+ self
+ }
+
+ pub fn with_perception_service(mut self, perception_service: PerceptionService) -> Self {
+ self.perception_service = perception_service;
+ self
+ }
+
+ #[allow(deprecated)]
+ pub async fn process(&self, request: AiRequest) -> Result {
+ let tools: Vec = request.tools.clone().unwrap_or_default();
+ let tools_enabled = !tools.is_empty();
+ let tool_choice = tools_enabled.then(|| {
+ async_openai::types::chat::ChatCompletionToolChoiceOption::Mode(ToolChoiceOptions::Auto)
+ });
+ let think = request.think;
+ let max_tool_depth = request.max_tool_depth;
+ let top_p = request.top_p;
+ let frequency_penalty = request.frequency_penalty;
+ let presence_penalty = request.presence_penalty;
+ let temperature_f = request.temperature;
+ let max_tokens_i = request.max_tokens;
+
+ let mut messages = self.build_messages(&request).await?;
+
+ let room_ai = room_ai::Entity::find()
+ .filter(room_ai::Column::Room.eq(request.room.id))
+ .filter(room_ai::Column::Model.eq(request.model.id))
+ .one(&request.db)
+ .await?;
+
+ let model_name = request.model.name.clone();
+ let temperature = room_ai
+ .as_ref()
+ .and_then(|r| r.temperature.map(|v| v as f32))
+ .unwrap_or(temperature_f as f32);
+ let max_tokens = room_ai
+ .as_ref()
+ .and_then(|r| r.max_tokens.map(|v| v as u32))
+ .unwrap_or(max_tokens_i as u32);
+ let mut tool_depth = 0;
+
+ loop {
+ let req = CreateChatCompletionRequest {
+ model: model_name.clone(),
+ messages: messages.clone(),
+ temperature: Some(temperature),
+ max_completion_tokens: Some(max_tokens),
+ top_p: Some(top_p as f32),
+ frequency_penalty: Some(frequency_penalty as f32),
+ presence_penalty: Some(presence_penalty as f32),
+ stream: Some(false),
+ reasoning_effort: Some(if think {
+ ReasoningEffort::High
+ } else {
+ ReasoningEffort::None
+ }),
+ tools: if tools_enabled {
+ Some(
+ tools
+ .iter()
+ .map(|t| ChatCompletionTools::Function(t.clone()))
+ .collect(),
+ )
+ } else {
+ None
+ },
+ tool_choice: tool_choice.clone(),
+ ..Default::default()
+ };
+
+ let response: CreateChatCompletionResponse = self
+ .openai_client
+ .chat()
+ .create(req)
+ .await
+ .map_err(|e| AgentError::OpenAi(e.to_string()))?;
+
+ let choice = response
+ .choices
+ .into_iter()
+ .next()
+ .ok_or_else(|| AgentError::Internal("no choice in response".into()))?;
+
+ if tools_enabled {
+ if let Some(ref tool_calls) = choice.message.tool_calls {
+ if !tool_calls.is_empty() {
+ messages.push(ChatCompletionRequestMessage::Assistant(
+ ChatCompletionRequestAssistantMessage {
+ content: choice
+ .message
+ .content
+ .clone()
+ .map(ChatCompletionRequestAssistantMessageContent::Text),
+ name: None,
+ refusal: None,
+ audio: None,
+ tool_calls: Some(tool_calls.clone()),
+ function_call: None,
+ },
+ ));
+
+ let calls: Vec = tool_calls
+ .iter()
+ .filter_map(|tc| {
+ if let ChatCompletionMessageToolCalls::Function(
+ async_openai::types::chat::ChatCompletionMessageToolCall {
+ id,
+ function,
+ },
+ ) = tc
+ {
+ Some(ToolCall {
+ id: id.clone(),
+ name: function.name.clone(),
+ arguments: function.arguments.clone(),
+ })
+ } else {
+ None
+ }
+ })
+ .collect();
+
+ if !calls.is_empty() {
+ let tool_messages = self.execute_tool_calls(calls, &request).await?;
+ messages.extend(tool_messages);
+
+ tool_depth += 1;
+ if tool_depth >= max_tool_depth {
+ return Ok(String::new());
+ }
+ continue;
+ }
+ }
+ }
+ }
+
+ let text = choice.message.content.unwrap_or_default();
+ return Ok(text);
+ }
+ }
+
+ #[allow(deprecated)]
+ pub async fn process_stream(&self, request: AiRequest, on_chunk: StreamCallback) -> Result<()> {
+ let tools: Vec = request.tools.clone().unwrap_or_default();
+ let tools_enabled = !tools.is_empty();
+ let tool_choice = tools_enabled.then(|| {
+ async_openai::types::chat::ChatCompletionToolChoiceOption::Mode(ToolChoiceOptions::Auto)
+ });
+ let think = request.think;
+ let max_tool_depth = request.max_tool_depth;
+ let top_p = request.top_p;
+ let frequency_penalty = request.frequency_penalty;
+ let presence_penalty = request.presence_penalty;
+ let temperature_f = request.temperature;
+ let max_tokens_i = request.max_tokens;
+
+ let mut messages = self.build_messages(&request).await?;
+
+ let room_ai = room_ai::Entity::find()
+ .filter(room_ai::Column::Room.eq(request.room.id))
+ .filter(room_ai::Column::Model.eq(request.model.id))
+ .one(&request.db)
+ .await?;
+
+ let model_name = request.model.name.clone();
+ let temperature = room_ai
+ .as_ref()
+ .and_then(|r| r.temperature.map(|v| v as f32))
+ .unwrap_or(temperature_f as f32);
+ let max_tokens = room_ai
+ .as_ref()
+ .and_then(|r| r.max_tokens.map(|v| v as u32))
+ .unwrap_or(max_tokens_i as u32);
+ let mut tool_depth = 0;
+
+ loop {
+ let req = CreateChatCompletionRequest {
+ model: model_name.clone(),
+ messages: messages.clone(),
+ temperature: Some(temperature),
+ max_completion_tokens: Some(max_tokens),
+ top_p: Some(top_p as f32),
+ frequency_penalty: Some(frequency_penalty as f32),
+ presence_penalty: Some(presence_penalty as f32),
+ stream: Some(true),
+ reasoning_effort: Some(if think {
+ ReasoningEffort::High
+ } else {
+ ReasoningEffort::None
+ }),
+ tools: if tools_enabled {
+ Some(
+ tools
+ .iter()
+ .map(|t| ChatCompletionTools::Function(t.clone()))
+ .collect(),
+ )
+ } else {
+ None
+ },
+ tool_choice: tool_choice.clone(),
+ ..Default::default()
+ };
+
+ let mut stream = self
+ .openai_client
+ .chat()
+ .create_stream(req)
+ .await
+ .map_err(|e| AgentError::OpenAi(e.to_string()))?;
+
+ let mut text_accumulated = String::new();
+ let mut tool_call_chunks: Vec = Vec::new();
+ let mut finish_reason: Option = None;
+
+ while let Some(chunk_result) = stream.next().await {
+ let chunk: CreateChatCompletionStreamResponse =
+ chunk_result.map_err(|e| AgentError::OpenAi(e.to_string()))?;
+
+ let choice = match chunk.choices.first() {
+ Some(c) => c,
+ None => continue,
+ };
+
+ // Track finish reason
+ if let Some(ref fr) = choice.finish_reason {
+ finish_reason = Some(fr.clone());
+ }
+
+ // Text delta
+ if let Some(content) = &choice.delta.content {
+ text_accumulated.push_str(content);
+ on_chunk(AiStreamChunk {
+ content: text_accumulated.clone(),
+ done: false,
+ })
+ .await;
+ }
+
+ // Tool call deltas
+ if let Some(ref tool_chunks) = choice.delta.tool_calls {
+ for tc in tool_chunks {
+ let idx = tc.index as usize;
+ if tool_call_chunks.len() <= idx {
+ tool_call_chunks.resize(idx + 1, ToolCallChunkAccum::default());
+ }
+ if let Some(ref id) = tc.id {
+ tool_call_chunks[idx].id = Some(id.clone());
+ }
+ if let Some(ref fc) = tc.function {
+ if let Some(ref name) = fc.name {
+ tool_call_chunks[idx].name.push_str(name);
+ }
+ if let Some(ref args) = fc.arguments {
+ tool_call_chunks[idx].arguments.push_str(args);
+ }
+ }
+ }
+ }
+ }
+
+ let has_tool_calls = matches!(
+ finish_reason,
+ Some(FinishReason::ToolCalls) | Some(FinishReason::FunctionCall)
+ );
+
+ if has_tool_calls && tools_enabled {
+ // Send final text chunk
+ on_chunk(AiStreamChunk {
+ content: text_accumulated.clone(),
+ done: true,
+ })
+ .await;
+
+ // Build ToolCall list from accumulated chunks
+ let tool_calls: Vec<_> = tool_call_chunks
+ .into_iter()
+ .filter(|c| !c.name.is_empty())
+ .map(|c| ToolCall {
+ id: c.id.unwrap_or_else(|| Uuid::new_v4().to_string()),
+ name: c.name,
+ arguments: c.arguments,
+ })
+ .collect();
+
+ if !tool_calls.is_empty() {
+ // Append assistant message with tool calls to history
+ messages.push(ChatCompletionRequestMessage::Assistant(
+ ChatCompletionRequestAssistantMessage {
+ content: Some(
+ ChatCompletionRequestAssistantMessageContent::Text(
+ text_accumulated,
+ ),
+ ),
+ name: None,
+ refusal: None,
+ audio: None,
+ tool_calls: Some(
+ tool_calls
+ .iter()
+ .map(|tc| {
+ ChatCompletionMessageToolCalls::Function(
+ async_openai::types::chat::ChatCompletionMessageToolCall {
+ id: tc.id.clone(),
+ function: async_openai::types::chat::FunctionCall {
+ name: tc.name.clone(),
+ arguments: tc.arguments.clone(),
+ },
+ },
+ )
+ })
+ .collect(),
+ ),
+ function_call: None,
+ },
+ ));
+
+ let tool_messages = self.execute_tool_calls(tool_calls, &request).await?;
+ messages.extend(tool_messages);
+
+ tool_depth += 1;
+ if tool_depth >= max_tool_depth {
+ return Ok(());
+ }
+ continue;
+ }
+ }
+
+ on_chunk(AiStreamChunk {
+ content: text_accumulated,
+ done: true,
+ })
+ .await;
+ return Ok(());
+ }
+ }
+
+ /// Executes a batch of tool calls and returns the tool result messages.
+ async fn execute_tool_calls(
+ &self,
+ calls: Vec,
+ request: &AiRequest,
+ ) -> Result> {
+ let mut ctx = ToolContext::new(
+ request.db.clone(),
+ request.cache.clone(),
+ request.room.id,
+ Some(request.sender.uid),
+ )
+ .with_project(request.project.id);
+
+ let executor = ToolExecutor::new();
+
+ let results = executor
+ .execute_batch(calls, &mut ctx)
+ .await
+ .map_err(|e| AgentError::Internal(e.to_string()))?;
+
+ Ok(ToolExecutor::to_tool_messages(&results))
+ }
+
+ async fn build_messages(
+ &self,
+ request: &AiRequest,
+ ) -> Result> {
+ let mut messages = Vec::new();
+
+ let mut processed_history = Vec::new();
+ if let Some(compact_service) = &self.compact_service {
+ // Auto-compact: only compresses when token count exceeds threshold
+ let config = CompactConfig::default();
+ match compact_service
+ .compact_room_auto(request.room.id, Some(request.user_names.clone()), config)
+ .await
+ {
+ Ok(compact_summary) => {
+ if !compact_summary.summary.is_empty() {
+ messages.push(ChatCompletionRequestMessage::System(
+ ChatCompletionRequestSystemMessage {
+ content: async_openai::types::chat::ChatCompletionRequestSystemMessageContent::Text(
+ format!("Conversation summary:\n{}", compact_summary.summary),
+ ),
+ ..Default::default()
+ },
+ ));
+ }
+ processed_history = compact_summary.retained;
+ }
+ Err(e) => {
+ let _ = e;
+ }
+ }
+ }
+
+ if !processed_history.is_empty() {
+ for msg_summary in processed_history {
+ let ctx = RoomMessageContext::from(msg_summary);
+ messages.push(ctx.to_message());
+ }
+ } else {
+ for msg in &request.history {
+ let ctx = RoomMessageContext::from_model_with_names(msg, &request.user_names);
+ messages.push(ctx.to_message());
+ }
+ }
+
+ if let Some(embed_service) = &self.embed_service {
+ for mention in &request.mention {
+ match mention {
+ Mention::Repo(repo) => {
+ let query = format!(
+ "{} {}",
+ repo.repo_name,
+ repo.description.as_deref().unwrap_or_default()
+ );
+ match embed_service.search_issues(&query, 5).await {
+ Ok(issues) if !issues.is_empty() => {
+ let context = format!(
+ "Related issues:\n{}",
+ issues
+ .iter()
+ .map(|i| format!("- {}", i.payload.text))
+ .collect::>()
+ .join("\n")
+ );
+ messages.push(ChatCompletionRequestMessage::System(
+ ChatCompletionRequestSystemMessage {
+ content: async_openai::types::chat::ChatCompletionRequestSystemMessageContent::Text(
+ context,
+ ),
+ ..Default::default()
+ },
+ ));
+ }
+ Err(e) => {
+ let _ = e;
+ }
+ _ => {}
+ }
+ match embed_service.search_repos(&query, 3).await {
+ Ok(repos) if !repos.is_empty() => {
+ let context = format!(
+ "Related repositories:\n{}",
+ repos
+ .iter()
+ .map(|r| format!("- {}", r.payload.text))
+ .collect::>()
+ .join("\n")
+ );
+ messages.push(ChatCompletionRequestMessage::System(
+ ChatCompletionRequestSystemMessage {
+ content: async_openai::types::chat::ChatCompletionRequestSystemMessageContent::Text(
+ context,
+ ),
+ ..Default::default()
+ },
+ ));
+ }
+ Err(e) => {
+ let _ = e;
+ }
+ _ => {}
+ }
+ }
+ Mention::User(user) => {
+ let mut profile_parts = vec![format!("Username: {}", user.username)];
+ if let Some(ref display_name) = user.display_name {
+ profile_parts.push(format!("Display name: {}", display_name));
+ }
+ if let Some(ref org) = user.organization {
+ profile_parts.push(format!("Organization: {}", org));
+ }
+ if let Some(ref website) = user.website_url {
+ profile_parts.push(format!("Website: {}", website));
+ }
+ messages.push(ChatCompletionRequestMessage::System(
+ ChatCompletionRequestSystemMessage {
+ content: async_openai::types::chat::ChatCompletionRequestSystemMessageContent::Text(
+ format!("Mentioned user profile:\n{}", profile_parts.join("\n")),
+ ),
+ ..Default::default()
+ },
+ ));
+ }
+ }
+ }
+ }
+
+ // Inject relevant skills via the perception system (auto + active + passive).
+ let skill_contexts = self.build_skill_context(request).await;
+ for ctx in skill_contexts {
+ messages.push(ctx.to_system_message() as ChatCompletionRequestMessage);
+ }
+
+ // Inject relevant past conversation memories via vector similarity.
+ let memories = self.build_memory_context(request).await;
+ for mem in memories {
+ messages.push(mem.to_system_message());
+ }
+
+ messages.push(ChatCompletionRequestMessage::User(
+ ChatCompletionRequestUserMessage {
+ content: async_openai::types::chat::ChatCompletionRequestUserMessageContent::Text(
+ request.input.clone(),
+ ),
+ ..Default::default()
+ },
+ ));
+
+ Ok(messages)
+ }
+
+ /// Fetch enabled skills for the current project and run them through the
+ /// perception system (auto + active + passive) to inject relevant context.
+ async fn build_skill_context(
+ &self,
+ request: &AiRequest,
+ ) -> Vec {
+ // Fetch enabled skills for this project.
+ let skills: Vec = match project_skill::Entity::find()
+ .filter(project_skill::Column::ProjectUuid.eq(request.project.id))
+ .filter(project_skill::Column::Enabled.eq(true))
+ .all(&request.db)
+ .await
+ {
+ Ok(models) => models
+ .into_iter()
+ .map(|s| SkillEntry {
+ slug: s.slug,
+ name: s.name,
+ description: s.description,
+ content: s.content,
+ })
+ .collect(),
+ Err(_) => return Vec::new(),
+ };
+
+ if skills.is_empty() {
+ return Vec::new();
+ }
+
+ // Build history text for auto-awareness scoring.
+ let history_texts: Vec = request
+ .history
+ .iter()
+ .rev()
+ .take(10)
+ .map(|msg| msg.content.clone())
+ .collect();
+
+ // Active + passive + auto perception (keyword-based).
+ let tool_events: Vec = Vec::new(); // Tool calls tracked in loop via process()
+ let keyword_skills = self
+ .perception_service
+ .inject_skills(&request.input, &history_texts, &tool_events, &skills)
+ .await;
+
+ // Vector-aware active perception: semantic search for skills via Qdrant.
+ let mut vector_skills = Vec::new();
+ if let Some(embed_service) = &self.embed_service {
+ let awareness = crate::perception::VectorActiveAwareness::default();
+ vector_skills = awareness
+ .detect(embed_service, &request.input, &request.project.id.to_string())
+ .await;
+ }
+
+ // Merge: deduplicate by label, preferring vector results (higher signal).
+ let mut seen = std::collections::HashSet::new();
+ let mut result = Vec::new();
+ for ctx in vector_skills {
+ if seen.insert(ctx.label.clone()) {
+ result.push(ctx);
+ }
+ }
+ for ctx in keyword_skills {
+ if seen.insert(ctx.label.clone()) {
+ result.push(ctx);
+ }
+ }
+
+ result
+ }
+
+ /// Inject relevant past conversation memories via vector similarity search.
+ async fn build_memory_context(
+ &self,
+ request: &AiRequest,
+ ) -> Vec {
+ let embed_service = match &self.embed_service {
+ Some(s) => s,
+ None => return Vec::new(),
+ };
+
+ // Search memories by current input semantic similarity.
+ let awareness = crate::perception::VectorPassiveAwareness::default();
+ awareness
+ .detect(embed_service, &request.input, &request.room.id.to_string())
+ .await
+ }
+}
+#[derive(Clone, Debug, Default)]
+struct ToolCallChunkAccum {
+ id: Option,
+ name: String,
+ arguments: String,
+}
diff --git a/libs/agent/client.rs b/libs/agent/client.rs
new file mode 100644
index 0000000..ef1cf89
--- /dev/null
+++ b/libs/agent/client.rs
@@ -0,0 +1,279 @@
+//! Unified AI client with built-in retry, token tracking, and session recording.
+//!
+//! Provides a single entry point for all AI calls with:
+//! - Exponential backoff with jitter (max 3 retries)
+//! - Retryable error classification (429/500/502/503/504)
+//! - Token usage tracking (input/output)
+
+use async_openai::Client;
+use async_openai::config::OpenAIConfig;
+use async_openai::types::chat::{
+ ChatCompletionRequestMessage, ChatCompletionTool, ChatCompletionToolChoiceOption,
+ ChatCompletionTools, CreateChatCompletionRequest, CreateChatCompletionResponse,
+};
+use std::time::Instant;
+
+use crate::error::{AgentError, Result};
+
+/// Configuration for the AI client.
+#[derive(Clone)]
+pub struct AiClientConfig {
+ pub api_key: String,
+ pub base_url: Option,
+}
+
+impl AiClientConfig {
+ pub fn new(api_key: String) -> Self {
+ Self {
+ api_key,
+ base_url: None,
+ }
+ }
+
+ pub fn with_base_url(mut self, base_url: impl Into) -> Self {
+ self.base_url = Some(base_url.into());
+ self
+ }
+
+ pub fn build_client(&self) -> Client {
+ let mut config = OpenAIConfig::new().with_api_key(&self.api_key);
+ if let Some(ref url) = self.base_url {
+ config = config.with_api_base(url);
+ }
+ Client::with_config(config)
+ }
+}
+
+/// Response from an AI call, including usage statistics.
+#[derive(Debug, Clone)]
+pub struct AiCallResponse {
+ pub content: String,
+ pub input_tokens: i64,
+ pub output_tokens: i64,
+ pub latency_ms: i64,
+}
+
+impl AiCallResponse {
+ pub fn total_tokens(&self) -> i64 {
+ self.input_tokens + self.output_tokens
+ }
+}
+
+/// Internal state for retry tracking.
+#[derive(Debug)]
+struct RetryState {
+ attempt: u32,
+ max_retries: u32,
+ max_backoff_ms: u64,
+}
+
+impl RetryState {
+ fn new(max_retries: u32) -> Self {
+ Self {
+ attempt: 0,
+ max_retries,
+ max_backoff_ms: 5000,
+ }
+ }
+
+ fn should_retry(&self) -> bool {
+ self.attempt < self.max_retries
+ }
+
+ /// Calculate backoff duration with "full jitter" technique.
+ fn backoff_duration(&self) -> std::time::Duration {
+ let exp = self.attempt.min(5);
+ // base = 500 * 2^exp, capped at max_backoff_ms
+ let base_ms = 500u64
+ .saturating_mul(2u64.pow(exp))
+ .min(self.max_backoff_ms);
+ // jitter: random [0, base_ms/2]
+ let jitter = (fastrand_u64(base_ms / 2 + 1)) as u64;
+ std::time::Duration::from_millis(base_ms / 2 + jitter)
+ }
+
+ fn next(&mut self) {
+ self.attempt += 1;
+ }
+}
+
+/// Fast pseudo-random u64 using a simple LCG.
+/// Good enough for jitter — not for cryptography.
+fn fastrand_u64(n: u64) -> u64 {
+ use std::sync::atomic::{AtomicU64, Ordering};
+ static STATE: AtomicU64 = AtomicU64::new(0x193_667_6a_5e_7c_57);
+ if n <= 1 {
+ return 0;
+ }
+ let mut current = STATE.load(Ordering::Relaxed);
+ loop {
+ let new_val = current.wrapping_mul(6364136223846793005).wrapping_add(1);
+ match STATE.compare_exchange_weak(current, new_val, Ordering::Relaxed, Ordering::Relaxed) {
+ Ok(_) => return new_val % n,
+ Err(actual) => current = actual,
+ }
+ }
+}
+
+/// Determine if an error is retryable.
+fn is_retryable_error(err: &async_openai::error::OpenAIError) -> bool {
+ use async_openai::error::OpenAIError;
+ match err {
+ // Network errors (DNS failure, connection refused, timeout) are always retryable
+ OpenAIError::Reqwest(_) => true,
+ // For API errors, check the error code string (e.g., "rate_limit_exceeded")
+ OpenAIError::ApiError(api_err) => api_err.code.as_ref().map_or(false, |code| {
+ matches!(
+ code.as_str(),
+ "rate_limit_exceeded"
+ | "internal_server_error"
+ | "service_unavailable"
+ | "gateway_timeout"
+ | "bad_gateway"
+ )
+ }),
+ _ => false,
+ }
+}
+
+/// Call the AI model with automatic retry.
+pub async fn call_with_retry(
+ messages: &[ChatCompletionRequestMessage],
+ model: &str,
+ config: &AiClientConfig,
+ max_retries: Option,
+) -> Result {
+ let client = config.build_client();
+ let mut state = RetryState::new(max_retries.unwrap_or(3));
+
+ loop {
+ let start = Instant::now();
+
+ let req = CreateChatCompletionRequest {
+ model: model.to_string(),
+ messages: messages.to_vec(),
+ ..Default::default()
+ };
+
+ let result = client.chat().create(req).await;
+
+ match result {
+ Ok(response) => {
+ let latency_ms = start.elapsed().as_millis() as i64;
+ let (input_tokens, output_tokens) = extract_usage(&response);
+
+ return Ok(AiCallResponse {
+ content: extract_content(&response),
+ input_tokens,
+ output_tokens,
+ latency_ms,
+ });
+ }
+ Err(err) => {
+ if state.should_retry() && is_retryable_error(&err) {
+ let duration = state.backoff_duration();
+ eprintln!(
+ "AI call failed (attempt {}/{}), retrying in {:?}",
+ state.attempt + 1,
+ state.max_retries,
+ duration
+ );
+ tokio::time::sleep(duration).await;
+ state.next();
+ continue;
+ }
+ return Err(AgentError::OpenAi(err.to_string()));
+ }
+ }
+ }
+}
+
+/// Call with custom parameters (temperature, max_tokens, optional tools).
+pub async fn call_with_params(
+ messages: &[ChatCompletionRequestMessage],
+ model: &str,
+ config: &AiClientConfig,
+ temperature: f32,
+ max_tokens: u32,
+ max_retries: Option,
+ tools: Option<&[ChatCompletionTool]>,
+) -> Result {
+ let client = config.build_client();
+ let mut state = RetryState::new(max_retries.unwrap_or(3));
+
+ loop {
+ let start = Instant::now();
+
+ let req = CreateChatCompletionRequest {
+ model: model.to_string(),
+ messages: messages.to_vec(),
+ temperature: Some(temperature),
+ max_completion_tokens: Some(max_tokens),
+ tools: tools.map(|ts| {
+ ts.iter()
+ .map(|t| ChatCompletionTools::Function(t.clone()))
+ .collect()
+ }),
+ tool_choice: tools.filter(|ts| !ts.is_empty()).map(|_| {
+ ChatCompletionToolChoiceOption::Mode(
+ async_openai::types::chat::ToolChoiceOptions::Auto,
+ )
+ }),
+ ..Default::default()
+ };
+
+ let result = client.chat().create(req).await;
+
+ match result {
+ Ok(response) => {
+ let latency_ms = start.elapsed().as_millis() as i64;
+ let (input_tokens, output_tokens) = extract_usage(&response);
+
+ return Ok(AiCallResponse {
+ content: extract_content(&response),
+ input_tokens,
+ output_tokens,
+ latency_ms,
+ });
+ }
+ Err(err) => {
+ if state.should_retry() && is_retryable_error(&err) {
+ let duration = state.backoff_duration();
+ eprintln!(
+ "AI call failed (attempt {}/{}), retrying in {:?}",
+ state.attempt + 1,
+ state.max_retries,
+ duration
+ );
+ tokio::time::sleep(duration).await;
+ state.next();
+ continue;
+ }
+ return Err(AgentError::OpenAi(err.to_string()));
+ }
+ }
+ }
+}
+
+/// Extract text content from a chat completion response.
+fn extract_content(response: &CreateChatCompletionResponse) -> String {
+ response
+ .choices
+ .first()
+ .and_then(|c| c.message.content.clone())
+ .unwrap_or_default()
+}
+
+/// Extract usage (input/output tokens) from a response.
+fn extract_usage(response: &CreateChatCompletionResponse) -> (i64, i64) {
+ response
+ .usage
+ .as_ref()
+ .map(|u| {
+ (
+ i64::try_from(u.prompt_tokens).unwrap_or(0),
+ i64::try_from(u.completion_tokens).unwrap_or(0),
+ )
+ })
+ .unwrap_or((0, 0))
+}
diff --git a/libs/agent/compact/helpers.rs b/libs/agent/compact/helpers.rs
new file mode 100644
index 0000000..80b8091
--- /dev/null
+++ b/libs/agent/compact/helpers.rs
@@ -0,0 +1,45 @@
+use super::types::{CompactSummary, MessageSummary};
+
+pub fn messages_to_text(
+ messages: &[models::rooms::room_message::Model],
+ sender_mapper: F,
+) -> String
+where
+ F: Fn(&models::rooms::room_message::Model) -> String,
+{
+ messages
+ .iter()
+ .map(|m| {
+ let sender = sender_mapper(m);
+ format!("[{}] {}: {}", m.send_at, sender, m.content)
+ })
+ .collect::>()
+ .join("\n")
+}
+
+pub fn retained_as_text(retained: &[MessageSummary]) -> String {
+ retained
+ .iter()
+ .map(|m| format!("[{}] {}: {}", m.send_at, m.sender_name, m.content))
+ .collect::>()
+ .join("\n")
+}
+
+pub fn summary_content(summary: &CompactSummary) -> String {
+ if summary.summary.is_empty() {
+ format!(
+ "## Recent conversation ({} messages)\n\n{}",
+ summary.retained.len(),
+ retained_as_text(&summary.retained)
+ )
+ } else {
+ format!(
+ "## Earlier conversation ({} messages summarised)\n{}\n\n\
+ ## Most recent {} messages\n\n{}",
+ summary.messages_compressed,
+ summary.summary,
+ summary.retained.len(),
+ retained_as_text(&summary.retained)
+ )
+ }
+}
diff --git a/libs/agent/compact/mod.rs b/libs/agent/compact/mod.rs
new file mode 100644
index 0000000..7e2b56b
--- /dev/null
+++ b/libs/agent/compact/mod.rs
@@ -0,0 +1,8 @@
+//! Context compaction for AI sessions and room message history.
+
+pub mod helpers;
+pub mod service;
+pub mod types;
+
+pub use service::CompactService;
+pub use types::{CompactConfig, CompactLevel, CompactSummary, MessageSummary, ThresholdResult};
diff --git a/libs/agent/compact/service.rs b/libs/agent/compact/service.rs
new file mode 100644
index 0000000..483fa6f
--- /dev/null
+++ b/libs/agent/compact/service.rs
@@ -0,0 +1,467 @@
+use async_openai::Client;
+use async_openai::config::OpenAIConfig;
+use async_openai::types::chat::{
+ ChatCompletionRequestMessage, ChatCompletionRequestUserMessage, CreateChatCompletionRequest,
+ CreateChatCompletionResponse,
+};
+use chrono::Utc;
+use models::ColumnTrait;
+use models::rooms::room_message::{
+ Column as RmCol, Entity as RoomMessage, Model as RoomMessageModel,
+};
+use models::users::user::{Column as UserCol, Entity as User};
+use sea_orm::{DatabaseConnection, EntityTrait, QueryFilter, QueryOrder};
+use serde_json::Value;
+use uuid::Uuid;
+
+use crate::AgentError;
+use crate::compact::helpers::summary_content;
+use crate::compact::types::{
+ CompactConfig, CompactLevel, CompactSummary, MessageSummary, ThresholdResult,
+};
+use crate::tokent::{TokenUsage, resolve_usage};
+
+#[derive(Clone)]
+pub struct CompactService {
+ db: DatabaseConnection,
+ openai: Client,
+ model: String,
+}
+
+impl CompactService {
+ pub fn new(db: DatabaseConnection, openai: Client, model: String) -> Self {
+ Self { db, openai, model }
+ }
+
+ pub async fn compact_room(
+ &self,
+ room_id: Uuid,
+ level: CompactLevel,
+ user_names: Option>,
+ ) -> Result {
+ let messages = self.fetch_room_messages(room_id).await?;
+
+ let user_ids: Vec = messages
+ .iter()
+ .filter_map(|m| m.sender_id)
+ .collect::>()
+ .into_iter()
+ .collect();
+ let user_name_map = match user_names {
+ Some(map) => map,
+ None => self.get_user_name_map(&user_ids).await?,
+ };
+
+ if messages.len() <= level.retain_count() {
+ let retained: Vec = messages
+ .iter()
+ .map(|m| Self::message_to_summary(m, &user_name_map))
+ .collect();
+ return Ok(CompactSummary {
+ session_id: Uuid::new_v4(),
+ room_id,
+ retained,
+ summary: String::new(),
+ compacted_at: Utc::now(),
+ messages_compressed: 0,
+ usage: None,
+ });
+ }
+
+ let retain_count = level.retain_count();
+ let split_index = messages.len().saturating_sub(retain_count);
+ let (to_summarize, retained_messages) = messages.split_at(split_index);
+
+ let retained: Vec = retained_messages
+ .iter()
+ .map(|m| Self::message_to_summary(m, &user_name_map))
+ .collect();
+
+ let (summary, remote_usage) = self.summarize_messages(to_summarize).await?;
+
+ // Build text of what was summarized (for tiktoken fallback)
+ let summarized_text = to_summarize
+ .iter()
+ .map(|m| m.content.as_str())
+ .collect::>()
+ .join("\n");
+ let usage = resolve_usage(remote_usage, &self.model, &summarized_text, &summary);
+
+ Ok(CompactSummary {
+ session_id: Uuid::new_v4(),
+ room_id,
+ retained,
+ summary,
+ compacted_at: Utc::now(),
+ messages_compressed: to_summarize.len(),
+ usage: Some(usage),
+ })
+ }
+
+ pub async fn compact_session(
+ &self,
+ session_id: Uuid,
+ level: CompactLevel,
+ user_names: Option>,
+ ) -> Result {
+ let messages: Vec = RoomMessage::find()
+ .filter(RmCol::Room.eq(session_id))
+ .order_by_asc(RmCol::Seq)
+ .all(&self.db)
+ .await
+ .map_err(|e| AgentError::Internal(e.to_string()))?;
+
+ if messages.is_empty() {
+ return Err(AgentError::Internal("session has no messages".into()));
+ }
+
+ let user_ids: Vec = messages
+ .iter()
+ .filter_map(|m| m.sender_id)
+ .collect::>()
+ .into_iter()
+ .collect();
+ let user_name_map = match user_names {
+ Some(map) => map,
+ None => self.get_user_name_map(&user_ids).await?,
+ };
+
+ if messages.len() <= level.retain_count() {
+ let retained: Vec = messages
+ .iter()
+ .map(|m| Self::message_to_summary(m, &user_name_map))
+ .collect();
+ return Ok(CompactSummary {
+ session_id,
+ room_id: Uuid::nil(),
+ retained,
+ summary: String::new(),
+ compacted_at: Utc::now(),
+ messages_compressed: 0,
+ usage: None,
+ });
+ }
+
+ let retain_count = level.retain_count();
+ let split_index = messages.len().saturating_sub(retain_count);
+ let (to_summarize, retained_messages) = messages.split_at(split_index);
+
+ let retained: Vec = retained_messages
+ .iter()
+ .map(|m| Self::message_to_summary(m, &user_name_map))
+ .collect();
+
+ // Summarize the earlier messages
+ let (summary, remote_usage) = self.summarize_messages(to_summarize).await?;
+
+ // Build text of what was summarized (for tiktoken fallback)
+ let summarized_text = to_summarize
+ .iter()
+ .map(|m| m.content.as_str())
+ .collect::>()
+ .join("\n");
+ let usage = resolve_usage(remote_usage, &self.model, &summarized_text, &summary);
+
+ Ok(CompactSummary {
+ session_id,
+ room_id: Uuid::nil(),
+ retained,
+ summary,
+ compacted_at: Utc::now(),
+ messages_compressed: to_summarize.len(),
+ usage: Some(usage),
+ })
+ }
+
+ pub fn summary_as_system_message(summary: &CompactSummary) -> ChatCompletionRequestMessage {
+ let content = summary_content(summary);
+ ChatCompletionRequestMessage::System(
+ async_openai::types::chat::ChatCompletionRequestSystemMessage {
+ content: async_openai::types::chat::ChatCompletionRequestSystemMessageContent::Text(
+ content,
+ ),
+ ..Default::default()
+ },
+ )
+ }
+
+ /// Check if the message history for a room exceeds the token threshold.
+ /// Returns `ThresholdResult::Skip` if below threshold, `Compact` if above.
+ ///
+ /// This method fetches messages and estimates their token count using tiktoken.
+ /// Call this before deciding whether to run full compaction.
+ pub async fn check_threshold(
+ &self,
+ room_id: Uuid,
+ config: CompactConfig,
+ ) -> Result {
+ let messages = self.fetch_room_messages(room_id).await?;
+ let tokens = self.estimate_message_tokens(&messages);
+
+ if tokens < config.token_threshold {
+ return Ok(ThresholdResult::Skip {
+ estimated_tokens: tokens,
+ });
+ }
+
+ let level = if config.auto_level {
+ CompactLevel::auto_select(tokens, config.token_threshold)
+ } else {
+ config.default_level
+ };
+
+ Ok(ThresholdResult::Compact {
+ estimated_tokens: tokens,
+ level,
+ })
+ }
+
+ /// Auto-compact a room: estimates token count, only compresses if over threshold.
+ ///
+ /// This is the recommended entry point for automatic compaction.
+ /// - If tokens < threshold → returns a no-op summary (empty summary, no compression)
+ /// - If tokens >= threshold → compresses with auto-selected level
+ pub async fn compact_room_auto(
+ &self,
+ room_id: Uuid,
+ user_names: Option>,
+ config: CompactConfig,
+ ) -> Result {
+ let threshold_result = self.check_threshold(room_id, config).await?;
+
+ match threshold_result {
+ ThresholdResult::Skip { .. } => {
+ // Below threshold — no compaction needed, return empty summary
+ let messages = self.fetch_room_messages(room_id).await?;
+ let user_ids: Vec = messages.iter().filter_map(|m| m.sender_id).collect();
+ let user_name_map = match user_names {
+ Some(map) => map,
+ None => self.get_user_name_map(&user_ids).await?,
+ };
+ let retained: Vec = messages
+ .iter()
+ .map(|m| Self::message_to_summary(m, &user_name_map))
+ .collect();
+
+ return Ok(CompactSummary {
+ session_id: Uuid::new_v4(),
+ room_id,
+ retained,
+ summary: String::new(),
+ compacted_at: Utc::now(),
+ messages_compressed: 0,
+ usage: None,
+ });
+ }
+ ThresholdResult::Compact { level, .. } => {
+ // Above threshold — compress with selected level
+ return self
+ .compact_room_with_level(room_id, level, user_names)
+ .await;
+ }
+ }
+ }
+
+ /// Compact a room with a specific level (bypassing threshold check).
+ /// Use this when the caller has already decided compaction is needed.
+ async fn compact_room_with_level(
+ &self,
+ room_id: Uuid,
+ level: CompactLevel,
+ user_names: Option>,
+ ) -> Result {
+ let messages = self.fetch_room_messages(room_id).await?;
+
+ let user_ids: Vec = messages.iter().filter_map(|m| m.sender_id).collect();
+ let user_name_map = match user_names {
+ Some(map) => map,
+ None => self.get_user_name_map(&user_ids).await?,
+ };
+
+ if messages.len() <= level.retain_count() {
+ let retained: Vec = messages
+ .iter()
+ .map(|m| Self::message_to_summary(m, &user_name_map))
+ .collect();
+ return Ok(CompactSummary {
+ session_id: Uuid::new_v4(),
+ room_id,
+ retained,
+ summary: String::new(),
+ compacted_at: Utc::now(),
+ messages_compressed: 0,
+ usage: None,
+ });
+ }
+
+ let retain_count = level.retain_count();
+ let split_index = messages.len().saturating_sub(retain_count);
+ let (to_summarize, retained_messages) = messages.split_at(split_index);
+
+ let retained: Vec = retained_messages
+ .iter()
+ .map(|m| Self::message_to_summary(m, &user_name_map))
+ .collect();
+
+ let (summary, remote_usage) = self.summarize_messages(to_summarize).await?;
+
+ let summarized_text = to_summarize
+ .iter()
+ .map(|m| m.content.as_str())
+ .collect::>()
+ .join("\n");
+ let usage = resolve_usage(remote_usage, &self.model, &summarized_text, &summary);
+
+ Ok(CompactSummary {
+ session_id: Uuid::new_v4(),
+ room_id,
+ retained,
+ summary,
+ compacted_at: Utc::now(),
+ messages_compressed: to_summarize.len(),
+ usage: Some(usage),
+ })
+ }
+
+ /// Estimate total token count of a message list using tiktoken.
+ fn estimate_message_tokens(&self, messages: &[RoomMessageModel]) -> usize {
+ let total_chars: usize = messages.iter().map(|m| m.content.len()).sum();
+ // Rough estimate: ~4 chars per token (safe upper bound)
+ total_chars / 4
+ }
+
+ fn message_to_summary(
+ m: &RoomMessageModel,
+ user_name_map: &std::collections::HashMap,
+ ) -> MessageSummary {
+ let sender_name = m
+ .sender_id
+ .and_then(|id| user_name_map.get(&id).cloned())
+ .unwrap_or_else(|| m.sender_type.to_string());
+ MessageSummary {
+ id: m.id,
+ sender_type: m.sender_type.clone(),
+ sender_id: m.sender_id,
+ sender_name,
+ content: m.content.clone(),
+ content_type: m.content_type.clone(),
+ tool_call_id: Self::extract_tool_call_id(&m.content),
+ send_at: m.send_at,
+ }
+ }
+
+ fn extract_tool_call_id(content: &str) -> Option {
+ let content = content.trim();
+ if let Ok(v) = serde_json::from_str::(content) {
+ v.get("tool_call_id")
+ .and_then(|v| v.as_str())
+ .map(|s| s.to_string())
+ } else {
+ None
+ }
+ }
+
+ async fn fetch_room_messages(
+ &self,
+ room_id: Uuid,
+ ) -> Result, AgentError> {
+ let messages: Vec = RoomMessage::find()
+ .filter(RmCol::Room.eq(room_id))
+ .order_by_asc(RmCol::Seq)
+ .all(&self.db)
+ .await
+ .map_err(|e| AgentError::Internal(e.to_string()))?;
+ Ok(messages)
+ }
+
+ async fn get_user_name_map(
+ &self,
+ user_ids: &[Uuid],
+ ) -> Result, AgentError> {
+ use std::collections::HashMap;
+ let mut map = HashMap::new();
+ if !user_ids.is_empty() {
+ let users = User::find()
+ .filter(UserCol::Uid.is_in(user_ids.to_vec()))
+ .all(&self.db)
+ .await
+ .map_err(|e| AgentError::Internal(e.to_string()))?;
+ for user in users {
+ map.insert(user.uid, user.username);
+ }
+ }
+ Ok(map)
+ }
+
+ async fn summarize_messages(
+ &self,
+ messages: &[RoomMessageModel],
+ ) -> Result<(String, Option), AgentError> {
+ // Collect distinct user IDs
+ let user_ids: Vec = messages
+ .iter()
+ .filter_map(|m| m.sender_id)
+ .collect::>()
+ .into_iter()
+ .collect();
+
+ // Query usernames
+ let user_name_map = self.get_user_name_map(&user_ids).await?;
+
+ // Define sender mapper
+ let sender_mapper = |m: &RoomMessageModel| {
+ if let Some(user_id) = m.sender_id {
+ if let Some(username) = user_name_map.get(&user_id) {
+ return username.clone();
+ }
+ }
+ m.sender_type.to_string()
+ };
+
+ let body = crate::compact::helpers::messages_to_text(messages, sender_mapper);
+
+ let user_msg = ChatCompletionRequestMessage::User(ChatCompletionRequestUserMessage {
+ content: async_openai::types::chat::ChatCompletionRequestUserMessageContent::Text(
+ format!(
+ "Summarise the following conversation concisely, preserving all key facts, \
+ decisions, and any pending or in-progress work. \
+ Use this format:\n\n\
+ **Summary:** \n\
+ **Key decisions:** \n\
+ **Open items:** \n\n\
+ Conversation:\n\n{}",
+ body
+ ),
+ ),
+ ..Default::default()
+ });
+
+ let request = CreateChatCompletionRequest {
+ model: self.model.clone(),
+ messages: vec![user_msg],
+ stream: Some(false),
+ ..Default::default()
+ };
+
+ let response: CreateChatCompletionResponse = self
+ .openai
+ .chat()
+ .create(request)
+ .await
+ .map_err(|e| AgentError::OpenAi(e.to_string()))?;
+
+ let text = response
+ .choices
+ .first()
+ .and_then(|c| c.message.content.clone())
+ .unwrap_or_default();
+
+ // Prefer remote usage; fall back to None (caller will use tiktoken via resolve_usage)
+ let remote_usage = response
+ .usage
+ .as_ref()
+ .and_then(|u| TokenUsage::from_remote(u.prompt_tokens, u.completion_tokens));
+
+ Ok((text, remote_usage))
+ }
+}
diff --git a/libs/agent/compact/types.rs b/libs/agent/compact/types.rs
new file mode 100644
index 0000000..05b0493
--- /dev/null
+++ b/libs/agent/compact/types.rs
@@ -0,0 +1,130 @@
+use chrono::{DateTime, Utc};
+use models::rooms::{
+ MessageContentType, MessageSenderType, room_message::Model as RoomMessageModel,
+};
+use serde::{Deserialize, Serialize};
+use serde_json::Value;
+use uuid::Uuid;
+
+use crate::tokent::TokenUsage;
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct MessageSummary {
+ pub id: Uuid,
+ pub sender_type: MessageSenderType,
+ pub sender_id: Option,
+ pub sender_name: String,
+ pub content: String,
+ pub content_type: MessageContentType,
+ /// Tool call ID extracted from message content JSON, if present.
+ pub tool_call_id: Option,
+ pub send_at: DateTime