From 2519294add33a511bae87625c626d1786fb44938 Mon Sep 17 00:00:00 2001 From: "Rodrigo Rodriguez (Pragmatismo)" Date: Sun, 12 Apr 2026 09:56:34 -0300 Subject: [PATCH] Force CI rebuild --- .gitignore | 3 +- .goutputstream-0HCON3 | Bin 60175 -> 0 bytes AGENTS.md | 784 +---------- Cargo.lock | 77 -- PROD.md | 1331 +------------------ prompts/{container.md => automate-incus.md} | 0 prompts/folha.md | 18 +- prompts/htmlview.md | 1 - prompts/nodrive.md | 46 - prompts/switcher.md | 434 ++++++ setup_zitadel.sh | 194 +++ 11 files changed, 756 insertions(+), 2132 deletions(-) delete mode 100644 .goutputstream-0HCON3 rename prompts/{container.md => automate-incus.md} (100%) delete mode 100644 prompts/htmlview.md delete mode 100644 prompts/nodrive.md create mode 100644 prompts/switcher.md create mode 100755 setup_zitadel.sh diff --git a/.gitignore b/.gitignore index 8a4e431..47a8e36 100644 --- a/.gitignore +++ b/.gitignore @@ -30,7 +30,8 @@ work/ # Documentation build docs/book - +.ruff_cache +.goutputstream* # Installers (keep gitkeep) botserver-installers/* !botserver-installers/.gitkeep diff --git a/.goutputstream-0HCON3 b/.goutputstream-0HCON3 deleted file mode 100644 index 5aaec64253d4b71ff6f454f7d50a13d45c96e528..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 60175 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%0D0=INcY>>aj(#7t2SZTSO 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⁢TsVFEf7Fr zH%?tGsIi%xS5fX_3eg?9Fm z1>Ci7PVGvUnlDU>Z9c4_@zk7Nd0w`!)=cg~OIzDr?h}Gp>ZsrZxeHrbUJ z+7%^uzo_rKgjMzS9edmZv9pglT%%*b9Cz`n%2m9Gkfl`8zAs`%ozF~b+ktHRns4rS5hTb=Mdq` zj2XXo)EG#ejC9PZ#GA%{4hZ{Ez+1;|r|qX8@!Fl`)wDQBWI@;*8)nzJcer|VKaMzP zkE@92I-vF(wcLkLt9HA6()&T10lo3bz4Nk@74v?wa%&=`i`sW`wRe@5M$Rp(+-hxA zkUSZr3bIbovjmMnaksV9IR|IA`xv)xCq4z>Pc*AfGns89HI~nYxm#YV+N8wVjKs!o zcssktq??K=i^f4$KDqw!M`KYXnJZI#(RmeL3aUttp;rY6g1THlSY3hG}*tax$7U{M4Iqfrn?OnDKc%G#Ld? zNiWOGJI72jF*A|X)RZJrfP;~30InBOR^Ci2IRHInG&n{MQE?!cdEAB;R$+7XW^&{@ zZ_c!q)aI8P?fk;lYe{L>Ir+$g9Y=T?;LW zD^~i6(n4B>x>#c~Gc@)DZwBouflolUpLw8vM!6qnj2XzNQ3Z zT5{o<37rV7RD@bZT4A;c{WxPbXKgPZoC;xhV{ptln{l;_mC)ePKyoYfTMCgxWNP;wKxEl%gxj*LJkr$GxAx8nrw{w_FS>9KY z$%o9X4O|3Ewcet!l$z7t(}Y}(24Aeq85x`ugY!{9T$zWOsn0%OK<{}XreEXsKr8Go$kay8hsIWWzKZf_)^P!rrrh6+#sR|RFXST zDd0HEo-q@_TBG5gT^Hnm=Af~$Po&7h9F6u#Opj)V4M7Z2ZODRbRKIpv2df}QIZ+0E zu-J&^{Gw@syBx|+9G$M0K9`Emm<~)9VPdB_#h{O+RUMxZP4*hCa;Hq3>hkY)d0C8} z^~yu=?+(1r^hx%Rk(f)ga)sD^y~yftz<`#)Cra`__<4dL$E9g@{hipJ|_FsM#elL;q3BaGvm``$_ks!hJhV| z=;IsQEptpssWv6sA~aOfyTtp`^mHYTsWL=yuxMJC^4Wk4_@=jSx4UFZ6{QFslsp^q z>Y7ivoPJBWjc1ynlk}Nut_vTJ)Ge=QuvaKZE3~$ob^46i-mXAvu6F&^S?4ZM`|ijH zy?Vu3oIPh@in7vT$Y#&$cceCk)jP+Ae4o?D&ut(7Q7&nVGMadQ%@f|V0f{vbn6~lT z&fsCFOjRT)7)tj(1bMCvAvZ;;qWnQ|*h+Q628T|Gc_Dj>$S6{%Yq735;ZREgKmHP7 zJIN6(ZB{xPB*?^ydutV`4r5qBqLTJ$xtv2N*fOe4Mx^fN&pf+JL+e#H&}XiN8{%-P z4yOZ}%RH1;zzw0j_&a!?9DCrYtE>t?%F0T7_(L!}@LpY-MkUB-wwGtREWDZFW?1Y8 zO*0-$8Z~|st<878$|aAMF1s=AF^)MaNV$#pjv@*at?gd$E3l9R}I}y6(55n#;cjdHw;X%6v z1O#fXjGn)I`7n`)`KRRz{<(Yy)?5;$Ur$VALAJkbXkk&u=n8w*Qq|W6KfT1Nbkoq( ztc0%ToR9n=r}A2AUEof!S`S}@Rcghke3$m@M18s*_Z2*FMrJ!{eh+(-)m;MYA|qz~ z?WMlFnDF%l(W+;xO#vzrd`2g6t-|sxUNm9KAKZJgBhHALv_#o7HMgi{z%-Y}u3{~|l-vA~)_>_n{N8(ilR|MxLHg1xX z<<-?~O`UR0d-6(u0qlUQA1kux5ElMWVe^GfR{Wa=gns@T(15&fyH=M#;4Q;QcQgyh zpUl~e(^{4$Nr{38<4%Kf$N)p+s!xrt@AfH(BM=C-4n1*pcsrz?-vV~x$O}`u=OH@) zeeRM}xr0hS4OJ^M_q8G1&rOJoqAhm1zq!XSTXz-Z}VujgN+();#fcP#gARb;kc>PbE-EDBs!1TqH#K5^hMR%IC zwDgYR(4$-zsmy7hsKH8=v|)Q1(C%URBT0`Rorlzc)UZQ3I{GsY9~M&2rInw{mTeYh zy7=2~?+UW676(b_!`GQ2tByR=_qk`S_4CHQf?F8?FF#)u6lBUGT1_=wy+jwc(j?Gx>T>G|aMsifT^ky* zfnDH*SLUh)TMe@vA(MOb)%An#UigEIb4N)07`jxve_$ZER9uYuIO /tmp/check.log 2>&1 &`, capture the PID, then loop watching line count and kill the process once it exceeds 20 lines. After killing, check for errors with `strings /tmp/check.log | grep "^error" | head -20`. Fix errors immediately, then repeat. Never use `--all-features` (pulls docs/slides dependencies). This saves 10+ minutes per error cycle since full compilation takes 2–3 minutes. The key rule: kill at 20 lines, fix immediately, loop until clean. -### Code Quality -- ❌ **NEVER** use `panic!()`, `todo!()`, `unimplemented!()`, `unwrap()`, `expect()` -- ❌ **NEVER** use `Command::new()` directly — use `SafeCommand` -- ❌ **NEVER** return raw error strings to HTTP clients — use `ErrorSanitizer` -- ❌ **NEVER** use `#[allow()]` or lint exceptions in `Cargo.toml` — FIX the code -- ❌ **NEVER** use `_` prefix for unused vars — DELETE or USE them -- ❌ **NEVER** leave unused imports, dead code, or commented-out code -- ❌ **NEVER** use CDN links — all assets must be local -- ❌ **NEVER** create `.md` docs without checking `botbook/` first -- ❌ **NEVER** hardcode credentials — use `generate_random_string()` or env vars - -### Build Pattern (MANDATORY) - Fix Fast Loop -When building botserver, use this pattern to fix errors ASAP: - -```bash -# Run cargo in background, kill at 20 lines, fix errors, loop -# IMPORTANT: Never use --all-features (pulls docs/slides dependencies) -cd /home/rodriguez/src/gb -cargo check -p botserver > /tmp/check.log 2>&1 & -CARGO_PID=$! -while kill -0 $CARGO_PID 2>/dev/null; do - LINES=$(wc -l < /tmp/check.log 2>/dev/null || echo 0) - if [ "$LINES" -gt 20 ]; then - kill $CARGO_PID 2>/dev/null - echo "=== Got $LINES lines, killing cargo ===" - break - fi - sleep 1 -done -# Check for errors - use strings to handle binary output -if strings /tmp/check.log | grep -q "^error"; then - echo "❌ Errors found:" - strings /tmp/check.log | grep "^error" | head -20 - # Fix errors, then re-run this pattern -else - echo "✅ No errors - build clean!" -fi -``` - -**Key Rule:** Kill cargo at 20 lines, fix errors immediately, loop until clean. - -**Why:** Compiling takes 2-3+ minutes. Getting errors in 20s saves 10+ minutes per error. - -### Security -- ❌ **NEVER** include sensitive data (IPs, tokens, keys) in docs or code -- ❌ **NEVER** write internal IPs to logs — mask them (e.g., "10.x.x.x") -- ❌ **NEVER** create files with secrets in repo root - -> **Secret files MUST be placed in `/tmp/` only** (ephemeral, not tracked by git). +If the process is killed by OOM, run `pkill -9 cargo; pkill -9 rustc; pkill -9 botserver` then retry with `CARGO_BUILD_JOBS=1 cargo check -p botserver 2>&1 | tail -200`. --- -## 🔐 Security Directives — MANDATORY +## Security Directives — Mandatory -### 1. Error Handling — No Panics -```rust -// ❌ FORBIDDEN: unwrap(), expect(), panic!(), todo!() -// ✅ REQUIRED: -value? -value.ok_or_else(|| Error::NotFound)? -value.unwrap_or_default() -if let Some(v) = value { ... } -``` +For error handling, never use `unwrap()`, `expect()`, `panic!()`, or `todo!()`. Use `value?`, `value.ok_or_else(|| Error::NotFound)?`, `value.unwrap_or_default()`, or `if let Some(v) = value { ... }`. -### 2. Command Execution — SafeCommand -```rust -// ❌ FORBIDDEN: Command::new("cmd").arg(user_input).output() -// ✅ REQUIRED: -use crate::security::command_guard::SafeCommand; -SafeCommand::new("allowed_command")?.arg("safe_arg")?.execute() -``` +For command execution, never use `Command::new("cmd").arg(user_input).output()`. Use `SafeCommand::new("allowed_command")?.arg("safe_arg")?.execute()` from `crate::security::command_guard`. -### 3. Error Responses — ErrorSanitizer -```rust -// ❌ FORBIDDEN: Json(json!({ "error": e.to_string() })) -// ✅ REQUIRED: -use crate::security::error_sanitizer::log_and_sanitize; -let sanitized = log_and_sanitize(&e, "context", None); -(StatusCode::INTERNAL_SERVER_ERROR, sanitized) -``` +For error responses, never return `Json(json!({ "error": e.to_string() }))`. Use `log_and_sanitize(&e, "context", None)` from `crate::security::error_sanitizer` and return `(StatusCode::INTERNAL_SERVER_ERROR, sanitized)`. -### 4. SQL — sql_guard -```rust -// ❌ FORBIDDEN: format!("SELECT * FROM {}", user_table) -// ✅ REQUIRED: -use crate::security::sql_guard::{sanitize_identifier, validate_table_name}; -let safe_table = sanitize_identifier(&user_table); -validate_table_name(&safe_table)?; -``` +For SQL, never use `format!("SELECT * FROM {}", user_table)`. Use `sanitize_identifier` and `validate_table_name` from `crate::security::sql_guard`. -### 5. Rate Limiting -- General: 100 req/s, Auth: 10 req/s, API: 50 req/s per token, WebSocket: 10 msgs/s -- Use `governor` crate with per-IP and per-User tracking +Rate limits: general 100 req/s, auth 10 req/s, API 50 req/s per token, WebSocket 10 msgs/s. Use the `governor` crate with per-IP and per-user tracking. All state-changing endpoints (POST/PUT/DELETE/PATCH) must require CSRF tokens via `tower_csrf` bound to the user session; Bearer Token endpoints are exempt. Every response must include these security headers: `Content-Security-Policy`, `Strict-Transport-Security`, `X-Frame-Options: DENY`, `X-Content-Type-Options: nosniff`, `Referrer-Policy: strict-origin-when-cross-origin`, and `Permissions-Policy: geolocation=(), microphone=(), camera=()`. -### 6. CSRF Protection -- ALL state-changing endpoints (POST/PUT/DELETE/PATCH) MUST require CSRF token -- Use `tower_csrf`, bound to user session. Exempt: Bearer Token endpoints - -### 7. Security Headers (ALL responses) -`Content-Security-Policy`, `Strict-Transport-Security`, `X-Frame-Options: DENY`, `X-Content-Type-Options: nosniff`, `Referrer-Policy: strict-origin-when-cross-origin`, `Permissions-Policy: geolocation=(), microphone=(), camera=()` - -### 8. Dependency Management -- App crates track `Cargo.lock`; lib crates don't -- Critical deps: exact versions (`=1.0.1`); regular: caret (`1.0`) -- Run `cargo audit` weekly; update only via PR with testing +For dependencies, app crates track `Cargo.lock`; lib crates do not. Critical deps use exact versions (`=1.0.1`); regular deps use caret (`1.0`). Run `cargo audit` weekly and update only via PR with testing. --- -## ✅ Mandatory Code Patterns +## Mandatory Code Patterns -```rust -impl MyStruct { fn new() -> Self { Self { } } } // Use Self, not type name -#[derive(PartialEq, Eq)] // Always derive both -format!("Hello {name}") // Inline format args -match x { A | B => do_thing(), C => other() } // Combine identical arms -``` +Use `Self` not the type name in `impl` blocks. Always derive both `PartialEq` and `Eq` together. Use inline format args: `format!("Hello {name}")` not `format!("Hello {}", name)`. Combine identical match arms: `A | B => do_thing()`. Maximum 450 lines per file — split proactively at 350 lines into `types.rs`, `handlers.rs`, `operations.rs`, `utils.rs`, and `mod.rs`, re-exporting all public items in `mod.rs`. --- -## 📏 File Size Limits +## Error Fixing Workflow -- **Max 450 lines per file** — split proactively at 350 lines -- Split by: `types.rs`, `handlers.rs`, `operations.rs`, `utils.rs`, `mod.rs` -- Re-export all public items in `mod.rs` +Read the entire error list first. Group errors by file. For each file: view it, fix all errors, then write once. Only verify with `cargo check` after all fixes are applied — never compile after each individual fix. `cargo clippy --workspace` must pass with zero warnings. --- -## 🔥 Error Fixing Workflow +## Execution Modes -### Preferred: Offline Batch Fix -1. Read ENTIRE error list first -2. Group errors by file -3. For each file: view → fix ALL errors → write once -4. Verify with build/diagnostics only AFTER all fixes +In local standalone mode (no incus), botserver manages all services itself. Run `cargo run -- --install` once to download and extract PostgreSQL, Valkey, MinIO, and Vault binaries into `botserver-stack/bin/`, initialize data directories, and download the LLM model. Then `cargo run` starts everything and serves at `http://localhost:8080`. Use `./reset.sh` to wipe and restart the local environment. -### ⚡ Streaming Build Rule -Don't wait for `cargo` to finish — cancel at first errors, fix, re-run. +In container (Incus) production mode, services run in separate named containers. Start them all with `sudo incus start system tables vault directory drive cache llm vector_db`. Access the system container with `sudo incus exec system -- bash`. View botserver logs with `sudo incus exec system -- journalctl -u botserver -f`. The container layout is: `system` runs BotServer on 8080; `tables` runs PostgreSQL on 5432; `vault` runs Vault on 8200; `directory` runs Zitadel on 8080 internally (external port 9000 via iptables NAT); `drive` runs MinIO on 9100; `cache` runs Valkey on 6379; `llm` runs llama.cpp on 8081; `vector_db` runs Qdrant on 6333. -### 🧠 Memory Issues (process "Killed") -```bash -pkill -9 cargo; pkill -9 rustc; pkill -9 botserver -CARGO_BUILD_JOBS=1 cargo check -p botserver 2>&1 | tail -200 -``` +Use the `LOAD_ONLY` variable in `/opt/gbo/bin/.env` to filter which bots are loaded and monitored by DriveMonitor, for example `LOAD_ONLY=default,salesianos`. --- -## 🔄 Modos de Execução +## Debugging & Testing -O botserver suporta **dois modos** de execução: +To watch for errors live: `tail -f botserver.log | grep -i "error\|tool"`. To debug a specific tool: grep `Tool error` in logs, fix the `.bas` file in MinIO at `/{bot}.gbai/{bot}.gbdialog/{tool}.bas`, then wait for DriveMonitor to recompile (automatic on file change, in-memory only, no local `.ast` cache). Test in browser at `http://localhost:3000/{botname}`. -### Modo 1: Local Standalone (sem Docker/Incus) +Common BASIC errors: `=== is not a valid operator` means you used JavaScript-style `===` — replace with `==` or use `--` for string separators. `Syntax error` means bad BASIC syntax — check parentheses and commas. `Tool execution failed` means a runtime error — check logs for stack trace. -O botserver sobe **tudo localmente** (PostgreSQL, Valkey, MinIO, Vault, LLM). +For Playwright testing, navigate to `http://localhost:3000/`, snapshot to verify welcome message and suggestion buttons including Portuguese accents, click a suggestion, wait 3–5 seconds, snapshot, fill data, submit, then verify DB records and backend logs. If the browser hangs, run `pkill -9 -f brave; pkill -9 -f chrome; pkill -9 -f chromium`, wait 3 seconds, and navigate again. The chat window may overlap other apps — click the middle (restore) button to minimize it or navigate directly via URL. -```bash -cd /home/rodriguez/src/gb/botserver -cargo run -- --install # Instala dependências (PostgreSQL, Valkey, MinIO, etc.) -cargo run # Sobe tudo e inicia o servidor -``` - -**O que acontece:** -- `PackageManager` baixa e extrai binários para `botserver-stack/bin/` -- Cria `botserver-stack/data/pgdata/` com PostgreSQL -- Inicia PostgreSQL na porta 5432 -- Inicia Valkey na porta 6379 -- Inicia MinIO na porta 9100 -- Configura Vault para secrets -- Baixa modelo LLM (llama.cpp) para detecção de anomalias -- Ao final: `http://localhost:8080` - -**Verificar se está rodando:** -```bash -curl http://localhost:8080/health -curl http://localhost:5432 # PostgreSQL -curl http://localhost:6379 # Valkey -``` - -**Testar com Playwright:** -```bash -# Navegar para bot de teste -npx playwright open http://localhost:3000/salesianos -# Ou diretamente -npx playwright open http://localhost:3000/detecta -``` - -### Modo 2: Container (Incus) — Produção - -Os serviços rodam em containers Incus separados. - -```bash -# Subir todos os containers -sudo incus start system tables vault directory drive cache llm vector_db - -# Verificar status -sudo incus list - -# Acessar container system (onde roda botserver) -sudo incus exec system -- bash - -# Ver logs do botserver -sudo incus exec system -- journalctl -u botserver -f -``` - -**Arquitetura de Containers:** - -| Container | Services | Portas | -|-----------|----------|--------| -| system | BotServer, Valkey | 8080, 6379 | -| tables | PostgreSQL | 5432 | -| vault | Vault | 8200 | -| directory | Zitadel | 9000 | -| drive | MinIO | 9100 | -| cache | Valkey (backup) | 6379 | -| llm | llama.cpp | 8081 | -| vector_db | Qdrant | 6333 | - -### reset.sh (Ambiente Local) -```bash -./reset.sh # Limpa e reinicia tudo localmente -``` - -### Service Commands -```bash -ps aux | grep -E "(botserver|botui)" | grep -v grep -curl http://localhost:8080/health -./restart.sh # Restart services -``` - -### LOAD_ONLY Environment Variable -Use `LOAD_ONLY` in `/opt/gbo/bin/.env` to filter which bots are loaded and monitored: - -```bash -# Example: only load default and salesianos bots -LOAD_ONLY=default,salesianos -``` - -This applies to both: -1. Bot discovery (auto-creating bots from S3 buckets) -2. DriveMonitor (syncing config.csv) +WhatsApp routing is global — one number serves all bots, with routing determined by the `whatsapp-id` key in each bot's `config.csv`. The bot name is sent as the first message to route correctly. --- -## 🎭 Playwright Browser Testing +## Bot Scripts Architecture -### Browser Setup -If browser fails: `pkill -9 -f brave; pkill -9 -f chrome; pkill -9 -f chromium` → wait 3s → navigate again. +`start.bas` is the entry point executed on WebSocket connect and on the first user message (once per session). It loads suggestion buttons via `ADD_SUGGESTION_TOOL` and marks the session in Redis to prevent re-runs. `{tool}.bas` files implement individual tools (e.g. `detecta.bas`). `tables.bas` is a special file — never call it with `CALL`; it is parsed automatically at compile time by `process_table_definitions()` and its table definitions are synced to the database via `sync_bot_tables()`. `init_folha.bas` handles initialization for specific features. -### Bot Testing Flow -1. Navigate to `http://localhost:3000/` -2. Snapshot → verify welcome message + suggestion buttons + Portuguese accents -3. Click suggestion → wait 3-5s → snapshot → fill data → submit -4. Verify DB records and backend logs +The `CALL` keyword can invoke in-memory procedures or `.bas` scripts by name. If the target is not in memory, botserver looks for `{name}.bas` in the bot's gbdialog folder in Drive. The `DETECT` keyword analyzes a database table for anomalies: it requires the table to exist (defined in `tables.bas`) and calls the BotModels API at `/api/anomaly/detect`. -### Desktop UI Note -Chat window may cover other apps — click **middle button** (restore) to minimize, or navigate directly via URL. - -### WhatsApp Testing -- Webhook is **global** — bot routing by typing bot name as first message -- Single WhatsApp number serves ALL bots; routing via `whatsapp-id` in `config.csv` +Tool buttons use `MessageType::TOOL_EXEC` (id 6). When the frontend sends `message_type: 6` via WebSocket, the backend executes the named tool directly in `stream_response()`, bypassing KB injection and LLM entirely. The result appears in chat without any "/tool" prefix text. Other message types are: 0 EXTERNAL, 1 USER, 2 BOT_RESPONSE, 3 CONTINUE, 4 SUGGESTION, 5 CONTEXT_CHANGE. --- -## ➕ Adding New Features +## Submodule Push Rule — Mandatory -### Checklist -- [ ] Which module owns this? (Check Module Responsibility Matrix) -- [ ] Database migrations needed? -- [ ] New API endpoints? -- [ ] Security: input validation, auth, rate limiting, error sanitization? -- [ ] Screens in botui? -- [ ] No `unwrap()`/`expect()`? +Every time you push the main repo, you must also push all submodules. CI builds based on submodule commits — if a submodule is not pushed, CI deploys old code. Always push botserver, botui, and botlib to both `origin` and `alm` remotes before or alongside the main repo push. -### Pattern: types → schema → Diesel model → business logic → API endpoint → BASIC keyword (if applicable) → tests → docs in `botbook/` - -### Commit & Deploy -```bash -cd botserver && git push alm main && git push origin main -cd .. && git add botserver && git commit -m "Update botserver: " && git push alm main && git push origin main -``` +The deploy workflow is: push to ALM → CI triggers on alm-ci → builds inside system container via SSH (to match glibc 2.36 on Debian 12 Bookworm, not the CI runner's glibc 2.41) → deploys binary → service auto-restarts. Verify by checking service status and logs about 10 minutes after pushing. --- -## 🎨 Frontend Standards +## Zitadel Setup (Directory Service) -- **HTMX-first** — server returns HTML fragments, not JSON -- **Local assets only** — NO CDN links -- Use `hx-get`, `hx-post`, `hx-target`, `hx-swap`; WebSocket via htmx-ws +Zitadel runs in the `directory` container on port 8080 internally. External port 9000 is forwarded to it via iptables NAT on the system container. The database is `PROD-DIRECTORY` on the `tables` container. The PAT file is at `/opt/gbo/conf/directory/admin-pat.txt` on the directory container. Admin credentials are username `admin`, password `Admin123!`. Current version is Zitadel v4.13.1. + +To reinstall: drop and recreate `PROD-DIRECTORY` on the tables container, write the init YAML to `/opt/gbo/conf/directory/zitadel-init-steps.yaml` (defining org name, admin user, and PAT expiry), then start Zitadel with env vars for the PostgreSQL host/port/database/credentials, `ZITADEL_EXTERNALSECURE=false`, `ZITADEL_EXTERNALDOMAIN=`, `ZITADEL_EXTERNALPORT=9000`, and `ZITADEL_TLS_ENABLED=false`. Pass `--masterkey MasterkeyNeedsToHave32Characters`, `--tlsMode disabled`, and `--steps `. Bootstrap takes about 90 seconds; verify with `curl -sf http://localhost:8080/debug/healthz`. + +Key API endpoints: `GET /management/v1/iam` for IAM info, `GET /management/v1/orgs/me` for current org, `POST /management/v1/users/human` to create a human user, `POST /oauth/v2/token` for access tokens, `GET /debug/healthz` for health. When calling externally via port 9000, include `Host: ` header. --- -## 🚀 Performance & Quality +## SEPLAGSE Bot Configuration -- `cargo clippy --workspace` must pass with **0 warnings** -- `cargo tree --duplicates` / `cargo machete` / `cargo audit` weekly -- Release profile: `opt-level = "z"`, `lto = true`, `codegen-units = 1`, `strip = true`, `panic = "abort"` -- Use `default-features = false` and opt-in to needed features +SEPLAGSE bot files are at `{botname}.gbai/{botname}.gbdialog/` in MinIO. Key files: `start.bas` is the entry point with suggestion buttons; `detecta.bas` implements anomaly detection on `folha_salarios`; `init_folha.bas` initializes test data (note: the INSERT keyword has parsing issues in multi-line scripts — workaround by inserting data manually or via external SQL); `tables.bas` defines database tables auto-processed on compile. + +The detection flow is: user clicks the "Detectar Desvios" tool button → frontend sends `message_type: 6` (TOOL_EXEC) → backend executes `detecta.bas` directly → `DETECT "folha_salarios"` queries the bot-specific database → data is sent to BotModels API at `/api/anomaly/detect` → results appear in chat. + +MinIO in production runs on port 9100 (not 9000). Default credentials are `gbadmin` / `Pesquisa@1000`. The mc config is at `/root/.mc/config.json` inside the drive container. To set up a new alias: `mc alias set local2 http://127.0.0.1:9100 gbadmin "Pesquisa@1000"`. Use `--recursive` for copying or moving buckets. To rename a bucket, copy all contents to the new name then delete the old one with `mc rb`. Bot buckets should be named `{botname}.gbai` without a `gbo-` prefix. --- -## 🧪 Testing +## Frontend Standards & Performance -- **Unit:** per-crate `tests/` or `#[cfg(test)]` modules — `cargo test -p ` -- **Integration:** `bottest/` crate — `cargo test -p bottest` -- **Coverage:** 80%+ on critical paths; ALL error paths and security guards tested +HTMX-first: the server returns HTML fragments, not JSON. Use `hx-get`, `hx-post`, `hx-target`, `hx-swap`, and WebSocket via htmx-ws. All assets must be local — no CDN links. + +Release profile must use `opt-level = "z"`, `lto = true`, `codegen-units = 1`, `strip = true`, and `panic = "abort"`. Use `default-features = false` and opt into only needed features. Run `cargo tree --duplicates`, `cargo machete`, and `cargo audit` weekly. + +Testing: unit tests live in per-crate `tests/` folders or `#[cfg(test)]` modules, run with `cargo test -p `. Integration tests live in `bottest/`, run with `cargo test -p bottest`. Aim for 80%+ coverage on critical paths; all error paths and security guards must be tested. --- -## 🚢 Deploy Workflow (CI/CD Only) +## Core Directives Summary -1. Push to ALM (triggers CI automatically) -2. CI builds on alm-ci → deploys to system container via SSH -3. Service auto-restarts on binary update -4. Verify: check service status + logs after ~10 min - -### Container Architecture - -| Container | Service | Port | -|-----------|---------|------| -| system | BotServer + Valkey | 8080/6379 | -| tables | PostgreSQL | 5432 | -| vault | Vault | 8200 | ---- - -## 🔑 Core Directives Summary - -- **OFFLINE FIRST** — fix all errors from list before compiling -- **BATCH BY FILE** — fix ALL errors in a file at once, write once -- **VERIFY LAST** — only compile after ALL fixes applied -- **DELETE DEAD CODE** — never keep unused code -- **GIT WORKFLOW** — always push to ALL repositories -- **0 warnings, 0 errors** — loop until clean - ---- - -## 🔧 Bot Scripts Architecture - -### File Types -| File | Purpose | -|------|---------| -| `start.bas` | Entry point, executed on session start | -| `{tool}.bas` | Tool implementation (e.g., `detecta.bas`) | -| `tables.bas` | **SPECIAL** - Defines database tables, auto-creates on compile | -| `init_folha.bas` | Initialization script for specific features | - -### tables.bas — SPECIAL FILE -- **DO NOT call via CALL keyword** - it's processed automatically -- Parsed at compile time by `process_table_definitions()` -- Tables are created/updated in database via `sync_bot_tables()` -- Location: `/opt/gbo/data/{bot}.gbai/{bot}.gbdialog/tables.bas` - -### Tool Button Execution (TOOL_EXEC) -- Frontend sends `message_type: 6` via WebSocket -- Backend handles in `stream_response()` when `message_type == MessageType::TOOL_EXEC` -- Tool executes directly, skips KB injection and LLM -- Result appears in chat (tool output), no "/tool" text shown - -### CALL Keyword -- Can call in-memory procedures OR .bas scripts -- Syntax: `CALL "script_name"` or `CALL "procedure_name"` -- If not in memory, looks for `{name}.bas` in bot's gbdialog folder - -### DETECT Keyword -- Analyzes database table for anomalies -- Requires table to exist (defined in tables.bas) -- Example: `result = DETECT "folha_salarios"` -- Calls BotModels API at `/api/anomaly/detect` - -### start.bas Execution -- Executed on WebSocket connect (for web clients) -- Also on first user message (blocking, once per session) -- Loads suggestions via `ADD_SUGGESTION_TOOL` -- Marks session with Redis key to prevent re-run - -### MessageType Enum (botlib/src/message_types.rs) -| ID | Name | Purpose | -|----|------|---------| -| 0 | EXTERNAL | External message | -| 1 | USER | User message | -| 2 | BOT_RESPONSE | Bot response | -| 3 | CONTINUE | Continue processing | -| 4 | SUGGESTION | Suggestion button | -| 5 | CONTEXT_CHANGE | Context change | -| 6 | TOOL_EXEC | Direct tool execution (skips KB/LLM) | - -**Usage:** When frontend sends `message_type: 6`, backend executes tool directly without going through LLM. - -### 🚨 FUNDAMENTAL: Submodule Push Rule (MANDATORY) - -**Every time you push the main repo, you MUST also push ALL submodules!** - -```bash -# After ANY main repo push, ALWAYS run: -cd botserver && git push origin main && git push alm main -cd ../botui && git push origin main && git push alm main -cd ../botlib && git push origin main && git push alm main -# ... repeat for ALL submodules -``` - -**Why:** CI builds based on submodule commits. If submodule isn't pushed, CI deploys old code. - -**Checklist before pushing:** -- [ ] botserver pushed? -- [ ] botui pushed? -- [ ] botlib pushed? -- [ ] All other submodules pushed? -- [ ] Main repo points to new submodule commits? - ---- - -## 🔐 Zitadel Setup (Directory Service) - -### Container Architecture -- **directory container**: Zitadel running on port **8080** internally -- **tables container**: PostgreSQL database on port 5432 -- Use database **PROD-DIRECTORY** for Zitadel data - -### Network Access (Container Mode) -- **Internal API**: `http://:8080` -- **External port 9000** redirected via iptables NAT to directory:8080 -- **Health check**: `curl -sf http://localhost:8080/debug/healthz` - -### Zitadel Installation Steps - -1. **Reset database** (on tables container): -```bash -psql -h localhost -U postgres -d postgres -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = 'PROD-DIRECTORY' AND pid <> pg_backend_pid();" -psql -h localhost -U postgres -d postgres -c "DROP DATABASE IF EXISTS \"PROD-DIRECTORY\";" -psql -h localhost -U postgres -d postgres -c "CREATE DATABASE \"PROD-DIRECTORY\";" -``` - -2. **Create init config** (on directory container): -```bash -cat > /opt/gbo/conf/directory/zitadel-init-steps.yaml << "EOF" -FirstInstance: - InstanceName: "BotServer" - DefaultLanguage: "en" - PatPath: "/opt/gbo/conf/directory/admin-pat.txt" - Org: - Name: "BotServer" - Machine: - Machine: - Username: "admin-sa" - Name: "Admin Service Account" - Pat: - ExpirationDate: "2099-01-01T00:00:00Z" - Human: - UserName: "admin" - FirstName: "Admin" - LastName: "User" - Email: - Address: "admin@localhost" - Verified: true - Password: "Admin123!" - PasswordChangeRequired: false -EOF -``` - -3. **Start Zitadel** (on directory container): -```bash -pkill -9 zitadel || true -nohup env \ - ZITADEL_DATABASE_POSTGRES_HOST= \ - ZITADEL_DATABASE_POSTGRES_PORT=5432 \ - ZITADEL_DATABASE_POSTGRES_DATABASE=PROD-DIRECTORY \ - ZITADEL_DATABASE_POSTGRES_USER_USERNAME=postgres \ - ZITADEL_DATABASE_POSTGRES_USER_PASSWORD=postgres \ - ZITADEL_DATABASE_POSTGRES_USER_SSL_MODE=disable \ - ZITADEL_DATABASE_POSTGRES_ADMIN_USERNAME=postgres \ - ZITADEL_DATABASE_POSTGRES_ADMIN_PASSWORD=postgres \ - ZITADEL_DATABASE_POSTGRES_ADMIN_SSL_MODE=disable \ - ZITADEL_EXTERNALSECURE=false \ - ZITADEL_EXTERNALDOMAIN= \ - ZITADEL_EXTERNALPORT=9000 \ - ZITADEL_TLS_ENABLED=false \ - /opt/gbo/bin/zitadel start-from-init \ - --masterkey MasterkeyNeedsToHave32Characters \ - --tlsMode disabled \ - --externalDomain \ - --externalPort 9000 \ - --steps /opt/gbo/conf/directory/zitadel-init-steps.yaml \ - > /opt/gbo/logs/zitadel.log 2>&1 & -``` - -4. **Wait for bootstrap** (~90 seconds), then verify: -```bash -curl -sf http://localhost:8080/debug/healthz -cat /opt/gbo/conf/directory/admin-pat.txt -``` - -5. **Configure iptables** (on system container): -```bash -iptables -t nat -A PREROUTING -p tcp --dport 9000 -j DNAT --to-destination :8080 -iptables -t nat -A OUTPUT -p tcp -d --dport 9000 -j DNAT --to-destination :8080 -``` - -### Zitadel API Usage - -**PAT file location**: `/opt/gbo/conf/directory/admin-pat.txt` (on directory container) - -#### Get IAM Info (internal) -```bash -curl -s -H "Authorization: Bearer $PAT" http://:8080/management/v1/iam -``` - -#### Get IAM Info (external via port 9000) -```bash -curl -s -H "Authorization: Bearer $PAT" -H "Host: " http://:9000/management/v1/iam -``` - -#### Create Human User -```bash -curl -s -X POST \ - -H "Authorization: Bearer $PAT" \ - -H "Host: " \ - -H "Content-Type: application/json" \ - http://:9000/management/v1/users/human \ - -d '{ - "userName": "janedoe", - "name": "Jane Doe", - "profile": {"firstName": "Jane", "lastName": "Doe"}, - "email": {"email": "jane@example.com"} - }' -``` - -### Zitadel API Endpoints Reference - -| Endpoint | Method | Description | -|----------|--------|-------------| -| `/management/v1/iam` | GET | Get IAM info | -| `/management/v1/orgs/me` | GET | Get current org | -| `/management/v1/users/human` | POST | Create human user | -| `/management/v1/users/machine` | POST | Create machine user | -| `/oauth/v2/token` | POST | Get access token | -| `/debug/healthz` | GET | Health check | - -### Important Notes - -- **Zitadel listens on port 8080 internally** -- **External port 9000** is forwarded via iptables NAT -- **Use Host header** with directory IP for external API calls -- **PAT file**: `/opt/gbo/conf/directory/admin-pat.txt` -- **Admin credentials**: `admin` / `Admin123!` (human user) -- **Database**: `PROD-DIRECTORY` on tables container -- **Zitadel v4.13.1** is the current version - ---- - -## 📊 SEPLAGSE Bot Configuration - -### Drive (MinIO) Access - Production - -**Quick Access:** -```bash -# SSH to the host server -ssh administrator@63.141.255.9 - -# Access drive container -sudo incus exec drive -- bash - -# List all buckets -/opt/gbo/bin/mc ls local/ - -# Access specific bucket -/opt/gbo/bin/mc alias set local2 http://127.0.0.1:9100 gbadmin "Pesquisa@1000" -/opt/gbo/bin/mc ls local2/ -``` - -**Credentials (from drive container):** -- Check `/root/.mc/config.json` or `/opt/gbo/conf/minio.json` -- Default: `gbadmin` / `Pesquisa@1000` on port **9100** (not 9000!) -- Port 9100 is the MinIO console/API in this setup - -**Common Operations:** -```bash -# List bucket contents -/opt/gbo/bin/mc ls local2/default.gbai/ - -# Create new bucket -/opt/gbo/bin/mc mb local2/newbot.gbai - -# Copy files between buckets -/opt/gbo/bin/mc cp --recursive local2/gbo-oldbot.gbai/somefolder local2/newbot.gbai/ - -# Rename/move bucket (must copy + delete): -/opt/gbo/bin/mc cp --recursive local2/gbo-old.gbai/* local2/old.gbai/ -/opt/gbo/bin/mc rb local2/gbo-old.gbai -``` - -**Tips:** -- MinIO runs on port 9100 in this container setup (not 9000) -- mc config is at `/root/.mc/config.json` in the drive container -- Always use `--recursive` flag for moving/copying buckets -- Bot buckets should be named `{botname}.gbai` (no gbo- prefix needed) - -### Bot Location -- **Source**: `/opt/gbo/data/seplagse.gbai/seplagse.gbdialog/` -- **Work**: `./botserver-stack/data/system/work/seplagse.gbai/seplagse.gbdialog/` - -### Key Files -| File | Purpose | -|------|---------| -| `start.bas` | Entry point with suggestion buttons | -| `detecta.bas` | Tool for detecting anomalies in folha_salarios | -| `init_folha.bas` | Tool to initialize test data (INSERT keyword has issues) | -| `tables.bas` | Table definitions - auto-processed on compile | - -### Tool Button Configuration (start.bas) -```bas -ADD_SUGGESTION_TOOL "detecta" AS "🔍 Detectar Desvios na Folha" -ADD_SUGGESTION_TOOL "init_folha" AS "⚙️ Inicializar Dados de Teste" -``` - -### Detection Flow -1. User clicks "Detectar Desvios na Folha" button -2. Frontend sends `message_type: 6` (TOOL_EXEC) via WebSocket -3. Backend executes `detecta.bas` directly (skips KB/LLM) -4. `detecta.bas` calls `DETECT "folha_salarios"` keyword -5. Keyword queries bot-specific database for table data -6. Data sent to BotModels API at `/api/anomaly/detect` -7. Results displayed in chat - -### Fixes Applied -1. **TOOL_EXEC message type**: Added `MessageType::TOOL_EXEC` (id=6) -2. **Frontend WebSocket**: Sends `message_type: 6` for tool buttons -3. **Backend handler**: `stream_response()` handles TOOL_EXEC directly -4. **DETECT keyword**: Fixed to use bot-specific database (`bot_database_manager`) -5. **Bot execution**: Tool buttons work - no "/tool" text shown - -### Known Issues -- **INSERT keyword**: Has parsing issues in multi-line scripts -- **Test data**: `init_folha.bas` cannot insert data due to INSERT issues -- **Workaround**: Insert data manually or via external SQL tool - -### Testing -```bash -# Restart services -./restart.sh - -# Test in browser -http://localhost:3000/seplagse - -# Check logs -tail -f botserver.log | grep -i "detecta\|error" -``` +Fix offline first — read all errors before compiling again. Batch by file — fix all errors in a file at once and write once. Verify last — only run `cargo check` after all fixes are applied. Delete dead code — never keep unused code. Git workflow — always push to all repositories (origin and alm). Target zero warnings and zero errors — loop until clean. \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 8ed87a1..e02b994 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1373,7 +1373,6 @@ dependencies = [ "log", "mailparse", "mockito", - "notify", "num-format", "once_cell", "ooxmlsdk", @@ -3415,15 +3414,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" -[[package]] -name = "fsevent-sys" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" -dependencies = [ - "libc", -] - [[package]] name = "futf" version = "0.1.5" @@ -4563,26 +4553,6 @@ dependencies = [ "cfb 0.7.3", ] -[[package]] -name = "inotify" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" -dependencies = [ - "bitflags 2.10.0", - "inotify-sys", - "libc", -] - -[[package]] -name = "inotify-sys" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" -dependencies = [ - "libc", -] - [[package]] name = "inout" version = "0.1.4" @@ -4852,26 +4822,6 @@ dependencies = [ "unicode-segmentation", ] -[[package]] -name = "kqueue" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" -dependencies = [ - "kqueue-sys", - "libc", -] - -[[package]] -name = "kqueue-sys" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" -dependencies = [ - "bitflags 1.3.2", - "libc", -] - [[package]] name = "kuchikiki" version = "0.8.8-speedreader" @@ -5568,24 +5518,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" -[[package]] -name = "notify" -version = "8.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" -dependencies = [ - "bitflags 2.10.0", - "fsevent-sys", - "inotify", - "kqueue", - "libc", - "log", - "mio", - "notify-types", - "walkdir", - "windows-sys 0.60.2", -] - [[package]] name = "notify-rust" version = "4.12.0" @@ -5600,15 +5532,6 @@ dependencies = [ "zbus", ] -[[package]] -name = "notify-types" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" -dependencies = [ - "bitflags 2.10.0", -] - [[package]] name = "ntapi" version = "0.4.2" diff --git a/PROD.md b/PROD.md index 5f7174a..26ecd59 100644 --- a/PROD.md +++ b/PROD.md @@ -1,1336 +1,115 @@ -# Production Environment Guide +# Production Environment Guide (Compact) -## ⚠️ CRITICAL PRODUCTION RULES +## CRITICAL RULES — READ FIRST -**READ THIS FIRST:** +Always manage services with `systemctl` inside the `system` Incus container. Never run `/opt/gbo/bin/botserver` or `/opt/gbo/bin/botui` directly — they will fail because they won't load the `.env` file containing Vault credentials and paths. The correct commands are `sudo incus exec system -- systemctl start|stop|restart|status botserver` and the same for `ui`. Systemctl handles environment loading, auto-restart, logging, and dependencies. -### 🚫 NEVER Start Services Directly - -In production, **NEVER** start botserver or botui directly. Always use `systemctl`: - -```bash -# ❌ NEVER DO THIS IN PRODUCTION: -/opt/gbo/bin/botserver # Wrong -./botserver # Wrong -/opt/gbo/bin/botserver & # Wrong - -# ✅ ALWAYS USE THIS: -sudo incus exec system -- systemctl start botserver -sudo incus exec system -- systemctl restart botserver -sudo incus exec system -- systemctl stop botserver -sudo incus exec system -- systemctl status botserver -``` - -**Why:** -- `systemctl` loads `/opt/gbo/bin/.env` (Vault credentials, paths, etc.) -- Direct execution skips environment variables → services fail -- `systemctl` manages auto-restart, logging, and dependencies - -### 🔐 Security Rules - -- **NEVER** push secrets to git (API keys, passwords, tokens) -- **NEVER** commit `init.json` (Vault unseal keys) -- **ALWAYS** use Vault for secrets (see [Vault Security Architecture](#vault-security-architecture)) -- **ONLY** `VAULT_*` environment variables allowed in `.env` - -### 🚢 Deployment Rules - -- **NEVER** deploy manually (scp, ssh copy) — use CI/CD only -- **NEVER** push to ALM without asking first -- **ALWAYS** push ALL submodules (botserver, botui, botlib) when pushing main repo -- **ALWAYS** use `systemctl` to restart services after deployment +Never push secrets (API keys, passwords, tokens) to git. Never commit `init.json` (it contains Vault unseal keys). All secrets must come from Vault — only `VAULT_*` variables are allowed in `.env`. Never deploy manually via scp or ssh; always use CI/CD. Always push all submodules (botserver, botui, botlib) before or alongside the main repo. Always ask before pushing to ALM. --- -## Infrastructure +## Infrastructure Overview -### Servers +The host machine is `PROD-GBO1`, accessed via `ssh administrator@`, running Incus (an LXD fork) as hypervisor. All services run inside named Incus containers. You enter containers with `sudo incus exec -- ` and list them with `sudo incus list`. -| Host | IP | Purpose | -|------|-----|---------| -| `system` | `` | Main botserver + botui container | -| `alm-ci` | `` | CI/CD runner (Forgejo Actions) | -| `drive` | `` | Object storage | -| `monitor` | `` | Monitoring service | +The containers and their roles are: `system` runs botserver on port 5858 and botui on port 5859; `alm-ci` runs the Forgejo Actions CI runner; `alm` hosts the Forgejo git server; `tables` runs PostgreSQL on port 5432; `cache` runs Valkey/Redis on port 6379; `drive` runs MinIO object storage on port 9100; `vault` runs HashiCorp Vault on port 8200; `vector` runs Qdrant on port 6333. -### Port Mapping (system container) +Externally, botserver is reachable at `https://system.example.com` and botui at `https://chat.example.com`. Internally, botui's `BOTSERVER_URL` must be `http://localhost:5858` — never the external HTTPS URL, because the Rust proxy runs server-side and needs direct localhost access. -| Service | Internal Port | External URL | -|---------|--------------|--------------| -| botserver | `5858` | `https://system.example.com` | -| botui | `5859` | `https://chat.example.com` | +--- -### Access +## Services Detail -```bash -# SSH to host -ssh admin@ +Botserver runs as user `gbuser`, binary at `/opt/gbo/bin/botserver`, logs at `/opt/gbo/logs/out.log` and `/opt/gbo/logs/err.log`, systemd unit at `/etc/systemd/system/botserver.service`, env loaded from `/opt/gbo/bin/.env`. Bot BASIC scripts live under `/opt/gbo/data/.gbai/.gbdialog/*.bas`; compiled AST cache goes to `/opt/gbo/work/`. -# Execute inside system container -sudo incus exec system -- bash -c 'command' +The botserver bootstrap also manages: Vault (secrets), PostgreSQL (database), Valkey (cache, password auth), MinIO (object storage), Zitadel (identity provider), and llama.cpp (LLM). -# SSH from host to container (used by CI) -ssh -o StrictHostKeyChecking=no system "command" -``` - -## Services - -### botserver.service - -- **Binary**: `/opt/gbo/bin/botserver` -- **Port**: `5858` -- **User**: `gbuser` -- **Logs**: `/opt/gbo/logs/out.log`, `/opt/gbo/logs/err.log` -- **Config**: `/etc/systemd/system/botserver.service` -- **Env**: `PORT=5858` - -### ui.service - -- **Binary**: `/opt/gbo/bin/botui` -- **Port**: `5859` -- **Config**: `/etc/systemd/system/ui.service` -- **Env**: `BOTSERVER_URL=http://localhost:5858` - - ⚠️ MUST be `http://localhost:5858` — NOT `https://system.example.com` - - Rust proxy runs server-side, needs direct localhost access - - JS client uses relative URLs through `chat.example.com` - -### Data Directory - -- **Path**: `/opt/gbo/data/` -- **Structure**: `.gbai/.gbdialog/*.bas` -- **Work dir**: `/opt/gbo/work/` (compiled .ast cache) - -### Stack Services (managed by botserver bootstrap) - -- **Vault**: Secrets management -- **PostgreSQL**: Database (port 5432) -- **Valkey**: Cache (port 6379, password auth) -- **MinIO**: Object storage -- **Zitadel**: Identity provider -- **LLM**: llama.cpp - -## CI/CD Pipeline - -### Repositories - -| Repo | ALM URL | GitHub URL | -|------|---------|------------| -| gb | `https://alm.example.com/organization/gb.git` | `git@github.com:organization/gb.git` | -| botserver | `https://alm.example.com/organization/BotServer.git` | `git@github.com:organization/botserver.git` | -| botui | `https://alm.example.com/organization/BotUI.git` | `git@github.com:organization/botui.git` | -| botlib | `https://alm.example.com/organization/botlib.git` | `git@github.com:organization/botlib.git` | - -### Push Order - -```bash -# 1. Push submodules first -cd botserver && git push alm main && git push origin main && cd .. -cd botui && git push alm main && git push origin main && cd .. - -# 2. Update root workspace references -git add botserver botui botlib -git commit -m "Update submodules: " -git push alm main && git push origin main -``` - -### Build Environment - -- **CI runner**: `ci-runner` container (Debian Trixie, glibc 2.41) -- **Target**: `system` container (Debian 12 Bookworm, glibc 2.36) -- **⚠️ GLIBC MISMATCH**: Building on CI runner produces binaries incompatible with system container -- **Solution**: CI workflow transfers source to system container and builds there via SSH - -### Workflow File - -- **Location**: `botserver/.forgejo/workflows/botserver.yaml` -- **Triggers**: Push to `main` branch -- **Steps**: - 1. Setup workspace on CI runner (clone repos) - 2. Transfer source to system container via `tar | ssh` - 3. Build inside system container (matches glibc 2.36) - 4. Deploy binary inside container - 5. Verify botserver is running +--- ## Common Operations -### Check Service Status +**Check status:** `sudo incus exec system -- systemctl status botserver --no-pager` (same for `ui`). To check process existence: `sudo incus exec system -- pgrep -f botserver`. -```bash -# From host -sudo incus exec system -- systemctl status botserver --no-pager -sudo incus exec system -- systemctl status ui --no-pager +**View logs:** For systemd journal: `sudo incus exec system -- journalctl -u botserver --no-pager -n 50`. For application logs: `sudo incus exec system -- tail -50 /opt/gbo/logs/out.log` or `err.log`. For live tail: `sudo incus exec system -- tail -f /opt/gbo/logs/out.log`. -# Check if running -sudo incus exec system -- pgrep -f botserver -sudo incus exec system -- pgrep -f botui -``` +**Restart:** `sudo incus exec system -- systemctl restart botserver` and same for `ui`. Never run the binary directly. -### View Logs +**Emergency manual deploy:** Kill the old process with `sudo incus exec system -- killall botserver`, copy the new binary from `/opt/gbo/ci/botserver/target/debug/botserver` to `/opt/gbo/bin/botserver`, set permissions with `chmod +x` and `chown gbuser:gbuser`, then start with `systemctl start botserver`. -```bash -# Systemd journal -sudo incus exec system -- journalctl -u botserver --no-pager -n 50 -sudo incus exec system -- journalctl -u ui --no-pager -n 50 +**Transfer bot files:** Archive locally with `tar czf /tmp/bots.tar.gz -C /opt/gbo/data .gbai`, copy to host with `scp`, then extract inside container with `sudo incus exec system -- bash -c 'tar xzf /tmp/bots.tar.gz -C /opt/gbo/data/'`. Clear compiled cache with `find /opt/gbo/data -name "*.ast" -delete` and same for `/opt/gbo/work`. -# Application logs -sudo incus exec system -- tail -50 /opt/gbo/logs/out.log -sudo incus exec system -- tail -50 /opt/gbo/logs/err.log +**Snapshots:** `sudo incus snapshot list system` to list, `sudo incus snapshot restore system ` to restore. -# Live tail -sudo incus exec system -- tail -f /opt/gbo/logs/out.log -``` +--- -### Restart Services +## CI/CD Pipeline -**CRITICAL PRODUCTION RULE:** In production, NEVER start botserver or botui directly. Always use `systemctl` to ensure proper initialization, environment loading, and logging. +Repositories exist on both GitHub and the internal ALM (Forgejo). The four repos are `gb` (main workspace), `botserver`, `botui`, and `botlib`. Always push submodules first (`cd botserver && git push alm main && git push origin main`), then update submodule references in the root repo and push that too. -```bash -sudo incus exec system -- systemctl restart botserver -sudo incus exec system -- systemctl restart ui -``` +The CI runner container (`alm-ci`) runs Debian Trixie with glibc 2.41, but the `system` container runs Debian 12 Bookworm with glibc 2.36. Binaries compiled on the CI runner are incompatible with the system container. The CI workflow (`botserver/.forgejo/workflows/botserver.yaml`) solves this by transferring source to the system container via `tar | ssh` and building there. The workflow triggers on pushes to `main`, clones repos, transfers source, builds inside system container, deploys the binary, and verifies botserver is running. -**PROHIBITED in production:** -```bash -# ❌ NEVER DO THIS IN PRODUCTION: -sudo incus exec system -- /opt/gbo/bin/botserver # Wrong - no systemd integration -sudo incus exec system -- /opt/gbo/bin/botserver & # Wrong - no service management -sudo incus exec system -- cd /opt/gbo/bin && ./botserver # Wrong - missing env vars +--- -# ✅ CORRECT - Always use systemctl: -sudo incus exec system -- systemctl start botserver -sudo incus exec system -- systemctl restart botserver -sudo incus exec system -- systemctl stop botserver -sudo incus exec system -- systemctl status botserver -``` +## DriveMonitor & Bot Configuration -**Why:** -- `systemctl` loads `/opt/gbo/bin/.env` (via `EnvironmentFile` in service definition) -- `systemctl` manages process lifecycle, auto-restart, and dependencies -- `systemctl` sends logs to `/opt/gbo/logs/out.log` and `/opt/gbo/logs/err.log` -- Direct execution skips environment variables and systemd service configuration +DriveMonitor is a background service inside botserver that watches MinIO buckets and syncs changes to the local filesystem and database every 10 seconds. It monitors three directory types per bot: the `.gbdialog/` folder for BASIC scripts (downloads and recompiles on change), the `.gbot/` folder for `config.csv` (syncs to the `bot_configuration` database table), and the `.gbkb/` folder for knowledge base documents (downloads and indexes for vector search). -### Manual Deploy (emergency) +Bot configuration is stored in two PostgreSQL tables inside the `botserver` database. The `bot_configuration` table holds key-value pairs with columns `bot_id`, `config_key`, `config_value`, `config_type`, `is_encrypted`, and `updated_at`. The `gbot_config_sync` table tracks sync state with columns `bot_id`, `config_file_path`, `last_sync_at`, `file_hash`, and `sync_count`. -```bash -# Kill old process -sudo incus exec system -- killall botserver +The `config.csv` format is a plain CSV with no header: each line is `key,value`, for example `llm-provider,groq` or `theme-color1,#cc0000`. DriveMonitor syncs it when the file ETag changes in MinIO, on botserver startup, or after a restart. -# Copy binary (from host CI workspace or local) -sudo incus exec system -- cp /opt/gbo/ci/botserver/target/debug/botserver /opt/gbo/bin/botserver -sudo incus exec system -- chmod +x /opt/gbo/bin/botserver -sudo incus exec system -- chown gbuser:gbuser /opt/gbo/bin/botserver +**Check config status:** Query `bot_configuration` via `sudo incus exec tables -- psql -h localhost -U postgres -d botserver -c "SELECT config_key, config_value FROM bot_configuration WHERE bot_id = (SELECT id FROM bots WHERE name = 'salesianos') ORDER BY config_key;"`. Check sync state via the `gbot_config_sync` table. Inspect the bucket directly with `sudo incus exec drive -- /opt/gbo/bin/mc cat local/salesianos.gbai/salesianos.gbot/config.csv`. -# Start service -sudo incus exec system -- systemctl start botserver -``` +**Debug DriveMonitor:** Monitor live logs with `sudo incus exec system -- tail -f /opt/gbo/logs/out.log | grep -E "(DRIVE_MONITOR|check_gbot|config)"`. An empty `gbot_config_sync` table means DriveMonitor has not synced yet. If no new log entries appear after 30 seconds, the loop may be stuck — restart botserver with systemctl to clear the state. -### Transfer Bot Files to Production +**Common config issues:** If config.csv is missing from the bucket, create and upload it with `mc cp`. If the database shows stale values, restart botserver to force a fresh sync, or as a temporary fix update the database directly with `UPDATE bot_configuration SET config_value = 'groq', updated_at = NOW() WHERE ...`. To force a re-sync without restarting, copy config.csv over itself with `mc cp local/... local/...` to change the ETag. -```bash -# From local to prod host -tar czf /tmp/bots.tar.gz -C /opt/gbo/data .gbai -scp /tmp/bots.tar.gz admin@:/tmp/ +--- -# From host to container -sudo incus exec system -- bash -c 'tar xzf /tmp/bots.tar.gz -C /opt/gbo/data/' +## MinIO (Drive) Operations -# Clear compiled cache -sudo incus exec system -- find /opt/gbo/data -name "*.ast" -delete -sudo incus exec system -- find /opt/gbo/work -name "*.ast" -delete -``` +All bot files live in MinIO buckets. Use the `mc` CLI at `/opt/gbo/bin/mc` from inside the `drive` container. The bucket structure per bot is: `{bot}.gbai/` as root, `{bot}.gbai/{bot}.gbdialog/` for BASIC scripts, `{bot}.gbai/{bot}.gbot/` for config.csv, and `{bot}.gbai/{bot}.gbkb/` for knowledge base folders. -### Snapshots +Common mc commands: `mc ls local/` lists all buckets; `mc ls local/salesianos.gbai/` lists a bucket; `mc cat local/.../start.bas` prints a file; `mc cp local/.../file /tmp/file` downloads; `mc cp /tmp/file local/.../file` uploads (this triggers DriveMonitor recompile); `mc stat local/.../config.csv` shows ETag and metadata; `mc mb local/newbot.gbai` creates a bucket; `mc rb local/oldbot.gbai` removes an empty bucket. -```bash -# List snapshots -sudo incus snapshot list system +If mc is not found, use the full path `/opt/gbo/bin/mc`. If alias `local` is not configured, check with `mc config host list`. If MinIO is not running, check with `sudo incus exec drive -- systemctl status minio`. -# Restore snapshot -sudo incus snapshot restore system -``` - -## DriveMonitor & Bot Configuration Sync - -### DriveMonitor Architecture - -DriveMonitor is a background service that synchronizes bot files from MinIO (S3-compatible storage) to the local filesystem and database. It monitors three directories per bot: - -| Directory | Purpose | Sync Behavior | -|-----------|---------|---------------| -| `{bot}.gbai/{bot}.gbdialog/` | BASIC scripts (.bas) | Downloads and compiles on change | -| `{bot}.gbai/{bot}.gbot/` | Configuration files | Syncs to `bot_configuration` table | -| `{bot}.gbkb/` | Knowledge base documents | Downloads and indexes for vector search | - -### Bot Configuration Database Tables - -#### `bot_configuration` (main config table) -```sql --- Location: botserver database -SELECT * FROM bot_configuration WHERE bot_id = ''; - --- Key columns: --- - bot_id: Bot UUID (link to bots table) --- - config_key: Configuration key (e.g., "llm-provider", "system-prompt") --- - config_value: Configuration value --- - config_type: Type (string, boolean, number) --- - is_encrypted: Whether value is encrypted --- - updated_at: Last modification timestamp -``` - -#### `gbot_config_sync` (sync tracking table) -```sql --- Location: botserver database --- Tracks config.csv sync status from bucket -SELECT * FROM gbot_config_sync g - JOIN bots b ON g.bot_id = b.id - WHERE b.name = 'salesianos'; - --- Key columns: --- - bot_id: Bot UUID --- - config_file_path: Path to config.csv in bucket --- - last_sync_at: Timestamp of last successful sync --- - file_hash: ETag/MD5 of synced file --- - sync_count: Number of times synced -``` - -### config.csv Sync Process - -**File Locations:** -- Source: `{bot}.gbai/{bot}.gbot/config.csv` in MinIO bucket -- Sync method: DriveMonitor → ConfigManager → `bot_configuration` table -- Sync frequency: Every 10 seconds (DriveMonitor periodic check) - -**Sync Trigger Conditions:** -1. File ETag changes in MinIO -2. Initial DriveMonitor startup -3. Manual botserver restart - -**CSV Format:** -```csv -llm-provider,groq -llm-api-key,sk-xxx -llm-url,http://localhost:8085 -system-prompt-file,PROMPT.md -theme-color1,#cc0000 -theme-title,MyBot -whatsapp-id,botname -``` - -### Checking Bot Configuration Status - -#### Method 1: Query bot_configuration table -```bash -# Get all config for a bot -sudo incus exec tables -- psql -h localhost -U postgres -d botserver -c " - SELECT b.name, bc.config_key, bc.config_value, bc.updated_at - FROM bot_configuration bc - JOIN bots b ON bc.bot_id = b.id - WHERE b.name = 'salesianos' - ORDER BY bc.config_key; -" - -# Get specific LLM provider config -sudo incus exec tables -- psql -h localhost -U postgres -d botserver -c " - SELECT config_key, config_value, updated_at - FROM bot_configuration - WHERE bot_id = ( - SELECT id FROM bots WHERE name = 'salesianos' - ) - AND config_key LIKE 'llm-%' - ORDER BY config_key; -" -``` - -#### Method 2: Check DriveMonitor sync status -```bash -# Check if config.csv has been synced -sudo incus exec tables -- psql -h localhost -U postgres -d botserver -c " - SELECT b.name, gcs.last_sync_at, gcs.sync_count, gcs.config_file_path - FROM gbot_config_sync gcs - JOIN bots b ON gcs.bot_id = b.id - WHERE b.name IN ('salesianos', 'default'); -" - --- Empty result = DriveMonitor hasn't synced config.csv yet --- If sync_count = 0, config.csv exists but hasn't been processed -``` - -#### Method 3: Direct MinIO inspection -```bash -# Check if config.csv exists in bucket -sudo incus exec drive -- /opt/gbo/bin/mc ls local/salesianos.gbai/salesianos.gbot/ - -# View config.csv contents -sudo incus exec drive -- /opt/gbo/bin/mc cat local/salesianos.gbai/salesianos.gbot/config.csv - -# Check file ETag (for sync comparison) -sudo incus exec drive -- /opt/gbo/bin/mc stat local/salesianos.gbai/salesianos.gbot/config.csv -``` - -### DriveMonitor Debugging Logs - -#### Key log patterns to monitor -```bash -# Monitor DriveMonitor activity in real-time -sudo incus exec system -- tail -f /opt/gbo/logs/out.log | grep -E "(DRIVE_MONITOR|check_gbot|config)" - -# Check for config.csv sync attempts -sudo incus exec system -- grep "check_gbot" /opt/gbo/logs/out.log | tail -20 - -# Check for config synchronization -sudo incus exec system -- grep "sync_gbot_config" /opt/gbo/logs/out.log | tail -20 - -# Check for DriveMonitor errors -sudo incus exec system -- grep -i "drive.*error" /opt/gbo/logs/err.log | tail -20 -``` - -#### Expected successful sync logs -``` -check_gbot: Checking bucket salesianos.gbai for config.csv changes -check_gbot: Found config.csv at path: salesianos.gai/salesianos.gbot/config.csv -info config:Synced config.csv for bot - updated 3 keys -``` - -#### Error patterns and meanings -``` -# Config.csv not found in bucket -check_gbot: Config file not found or inaccessible: path/to/config.csv - -# Sync to database failed -error config:Failed to sync_gbot_config: - -# DriveMonitor not running -(no check_gbot logs in output.log) - -# MinIO connection failed -error drive_monitor:S3/MinIO unavailable for bucket -``` - -### Common Issues and Fixes - -#### Issue 1: config.csv not syncing to database - -**Symptoms:** -- `gbot_config_sync` table empty (0 rows) -- LLM provider changes in bucket not reflected in bot behavior -- Database shows old configuration values - -**Diagnosis:** -```bash -# 1. Check if config.csv exists in bucket -sudo incus exec drive -- /opt/gbo/bin/mc ls local/salesianos.gbai/salesianos.gbot/ - -# 2. Check DriveMonitor logs for sync attempts -sudo incus exec system -- grep "check_gbot" /opt/gbo/logs/out.log | tail -10 - -# 3. Check if DriveMonitor is running for the bot -sudo incus exec system -- ps aux | grep botserver -``` - -**Root Causes:** -1. config.csv missing from `{bot}.gai/{bot}.gbot/` folder -2. DriveMonitor not started for the bot -3. MinIO connection issues -4. Database write permissions - -**Fixes:** -```bash -# Case 1: Create missing config.csv -sudo incus exec drive -- bash -c ' -cat > /tmp/config.csv << EOF -llm-provider,groq -llm-api-key,your-api-key -llm-url,http://localhost:8085 -system-prompt-file,PROMPT.md -theme-color1,#cc0000 -theme-title,Salesianos -EOF -/opt/gbo/bin/mc cp /tmp/config.csv local/salesianos.gbai/salesianos.gbot/config.csv -' - -# Case 2: Restart botserver to reinitialize DriveMonitor -sudo incus exec system -- systemctl restart botserver - -# Case 3: Force immediate sync by touching config.csv -sudo incus exec drive -- /opt/gbo/bin/mc cp local/salesianos.gbai/salesianos.gbot/config.csv local/salesianos.gbai/salesianos.gbot/config.csv -``` - -#### Issue 2: LLM provider changes not taking effect - -**Symptoms:** -- config.csv shows correct provider (e.g., groq) -- Bot still uses old provider -- Database shows old value - -**Diagnosis:** -```bash -# Compare bucket vs database -BUCKET_PROVIDER=$(sudo incus exec drive -- /opt/gbo/bin/mc cat local/salesianos.gbai/salesianos.gbot/config.csv | grep "^llm-provider" | cut -d',' -f2) -DB_PROVIDER=$(sudo incus exec tables -- psql -h localhost -U postgres -d botserver -t -c " - SELECT config_value FROM bot_configuration - WHERE bot_id = (SELECT id FROM bots WHERE name = 'salesianos') - AND config_key = 'llm-provider'; -") - -echo "Bucket: $BUCKET_PROVIDER" -echo "Database: $DB_PROVIDER" - -# Check last sync time -sudo incus exec tables -- psql -h localhost -U postgres -d botserver -t -c " - SELECT last_sync_at FROM gbot_config_sync - WHERE bot_id = (SELECT id FROM bots WHERE name = 'salesianos'); -" -``` - -**Fix:** -```bash -# If sync is stale (> 10 minutes), restart DriveMonitor -sudo incus exec system -- systemctl restart botserver - -# Or manually update config value in database (temporary fix) -sudo incus exec tables -- psql -h localhost -U postgres -d botserver -c " - UPDATE bot_configuration - SET config_value = 'groq', updated_at = NOW() - WHERE bot_id = (SELECT id FROM bots WHERE name = 'salesianos') - AND config_key = 'llm-provider'; -" -``` - -#### Issue 3: DriveMonitor not checking for changes - -**Symptoms:** -- No new log entries after 30 seconds -- File changes in bucket not detected -- Bot compilation not happening after .bas file updates - -**Diagnosis:** -```bash -# Check DriveMonitor loop logs -sudo incus exec system -- tail -100 /opt/gbo/logs/out.log | grep "DRIVE_MONITOR.*Inside monitoring loop" - -# Check if is_processing flag is stuck -sudo incus exec system -- tail -100 /opt/gbo/logs/out.log | grep -E "(is_processing|monitoring loop)" -``` - -**Fix:** -```bash -# Restart botserver to clear stuck state -sudo incus exec system -- systemctl restart botserver - -# Monitor startup logs to verify DriveMonitor started -sudo incus exec system -- tail -50 /opt/gbo/logs/out.log | grep "Drive Monitor" -``` - -### Database Schema Reference - -#### List all bot databases -```bash -sudo incus exec tables -- psql -h localhost -U postgres -d postgres -c "\l" | grep bot_ -``` - -#### List tables in a specific bot database -```bash -sudo incus exec tables -- psql -h localhost -U postgres -d bot_salesianos -c "\dt" -``` - -#### List botserver management tables -```bash -sudo incus exec tables -- psql -h localhost -U postgres -d botserver -c "\dt" | grep -E "(bot|config|sync)" -``` - -### Connection Methods Summary - -| Method | Use Case | Command Pattern | -|--------|-----------|-----------------| -| **SSH to host** | Initial access, file transfer | `ssh admin@63.141.255.9` | -| **incus exec** | Execute inside container | `sudo incus exec system -- command` | -| **psql direct** | Database queries from container | `sudo incus exec tables -- psql ...` | -| **mc (MinIO CLI)** | Inspect buckets, copy files | `sudo incus exec drive -- /opt/gbo/bin/mc ...` | -| **HTTP/curl** | Service health checks | `curl http://:5858/health` | -| **journalctl** | Systemd service logs | `sudo incus exec system -- journalctl -u botserver` | +--- ## Vault Security Architecture -### Overview +HashiCorp Vault is the single source of truth for all secrets. Botserver reads `VAULT_ADDR` and `VAULT_TOKEN` from `/opt/gbo/bin/.env` at startup, initializes a TLS/mTLS client, then reads credentials from Vault paths. If Vault is unavailable, it falls back to defaults. The `.env` file must only contain `VAULT_*` variables plus `PORT`, `DATA_DIR`, `WORK_DIR`, and `LOAD_ONLY`. -The production environment uses **HashiCorp Vault** as the centralized secrets management system. All sensitive credentials (database passwords, API keys, tokens) are stored in Vault, NEVER in code or environment files. +**Global Vault paths:** `gbo/tables` holds PostgreSQL credentials; `gbo/drive` holds MinIO access key and secret; `gbo/cache` holds Valkey password; `gbo/llm` holds LLM URL and API keys; `gbo/directory` holds Zitadel config; `gbo/email` holds SMTP credentials; `gbo/vectordb` holds Qdrant config; `gbo/jwt` holds JWT signing secret; `gbo/encryption` holds the master encryption key. Organization-scoped secrets follow patterns like `gbo/orgs/{org_id}/bots/{bot_id}` and tenant infrastructure uses `gbo/tenants/{tenant_id}/infrastructure`. -### Vault Connection Flow +**Credential resolution:** For any service, botserver checks the most specific Vault path first (org+bot level), falls back to a default bot path, then falls back to the global path, and only uses environment variables as a last resort in development. -``` -1. botserver starts - ↓ -2. Reads VAULT_ADDR, VAULT_TOKEN from .env - ↓ -3. Initializes VaultClient with TLS/mTLS - ↓ -4. Reads secrets from Vault paths (gbo/tables, gbo/drive, etc.) - ↓ -5. Falls back to defaults if Vault unavailable -``` +**Verify Vault health:** `sudo incus exec vault -- curl -k -sf https://localhost:8200/v1/sys/health` should return JSON with `"sealed":false`. To read a secret: set `VAULT_ADDR`, `VAULT_TOKEN`, and `VAULT_CACERT` then run `vault kv get secret/gbo/tables`. To test from the system container, use curl with `--cacert /opt/gbo/conf/system/certificates/ca/ca.crt` and `-H "X-Vault-Token: "`. -### Environment Variables (Allowed) +**init.json** is stored at `/opt/gbo/bin/botserver-stack/conf/vault/vault-conf/init.json` and contains the root token and 5 unseal keys (3 needed to unseal). Never commit this file to git. Store it encrypted in a secure location. -**File Location:** `/opt/gbo/bin/.env` (system container) +**Vault troubleshooting — cannot connect:** Check that the vault container's systemd unit is running, verify the token in `.env` is not expired with `vault token lookup`, confirm the CA cert path in `.env` matches the actual file location, and test network connectivity from system to vault container. To generate a new token: `vault token create -policy="botserver" -ttl="8760h" -format=json` then update `.env` and restart botserver. -```bash -# Vault Connection (MANDATORY for production) -VAULT_ADDR=https://:8200 -VAULT_TOKEN= -VAULT_CACERT=/opt/gbo/conf/system/certificates/ca/ca.crt +**Vault troubleshooting — secrets missing:** Run `vault kv get secret/gbo/tables` (and other paths) to check if secrets exist. If a path returns NOT FOUND, add secrets with `vault kv put secret/gbo/tables host= port=5432 database=botserver username=gbuser password=` and similar for other paths. -# Optional: Skip TLS verification (NOT recommended for production) -VAULT_SKIP_VERIFY=false +**Vault sealed after restart:** Run `vault operator unseal `, repeat with key2 and key3 (3 of 5 keys from init.json), then verify with `vault status`. -# Optional: Use mTLS certificates -VAULT_CLIENT_CERT=/opt/gbo/conf/system/certificates/botserver/client.crt -VAULT_CLIENT_KEY=/opt/gbo/conf/system/certificates/botserver/client.key +**TLS certificate errors:** Confirm `/opt/gbo/conf/system/certificates/ca/ca.crt` exists in the system container. If missing, copy it from the vault container using `incus file pull vault/opt/gbo/conf/vault/ca.crt /tmp/ca.crt` then place it at the expected path. -# Optional: Cache TTL in seconds (default: 300) -VAULT_CACHE_TTL=300 +**Vault snapshots:** Stop vault, run `sudo incus snapshot create vault backup-$(date +%Y%m%d-%H%M)`, start vault. Restore with `sudo incus snapshot restore vault ` while vault is stopped. -# Server Configuration -PORT=5858 -DATA_DIR=/opt/gbo/data/ -WORK_DIR=/opt/gbo/work/ -LOAD_ONLY=default,salesianos -``` +--- -**Security Rule:** -- **ONLY** `VAULT_*` environment variables are allowed in `.env` -- All other secrets MUST come from Vault -- Hardcoded secrets in code are FORBIDDEN (see AGENTS.md) +## Troubleshooting Quick Reference -### Vault Secret Paths Structure +**GLIBC mismatch (`GLIBC_2.39 not found`):** The binary was compiled on the CI runner (glibc 2.41) not inside the system container (glibc 2.36). The CI workflow must SSH into the system container to build. Check `botserver.yaml` to confirm this. -#### System-Wide Paths (Global) +**botserver won't start:** Run `sudo incus exec system -- ldd /opt/gbo/bin/botserver | grep "not found"` to check for missing libraries. Run `sudo incus exec system -- timeout 10 /opt/gbo/bin/botserver 2>&1` to see startup errors. Confirm `/opt/gbo/data/` exists and is accessible. -| Path | Purpose | Example Keys | -|------|---------|---------------| -| `gbo/tables` | Database (PostgreSQL) | host, port, database, username, password | -| `gbo/drive` | MinIO (Object Storage) | host, accesskey, secret | -| `gbo/cache` | Valkey (Redis) | host, port, password | -| `gbo/directory` | Zitadel (Auth) | url, project_id, client_id, client_secret | -| `gbo/email` | SMTP Email | smtp_host, smtp_port, smtp_user, smtp_password | -| `gbo/llm` | LLM Configuration | url, model, openai_key, anthropic_key | -| `gbo/vectordb` | Qdrant (Vector DB) | url, api_key | -| `gbo/jwt` | JWT Signing | secret | -| `gbo/meet` | Jitsi Meet | url, app_id, app_secret | -| `gbo/alm` | ALM Repository | url, token | -| `gbo/encryption` | Encryption Keys | master_key | -| `gbo/system/observability` | Monitoring | url, org, bucket, token | -| `gbo/system/security` | Security Policies | require_auth, anonymous_paths | -| `gbo/system/cloud` | Cloud Config | region, access_key, secret_key | -| `gbo/system/app` | Application Settings | url, environment | -| `gbo/system/models` | BotModels API | url | +**botui can't reach botserver:** Check that the `ui.service` systemd file has `BOTSERVER_URL=http://localhost:5858` — not the external HTTPS URL. Fix with `sed -i 's|BOTSERVER_URL=.*|BOTSERVER_URL=http://localhost:5858|'` on the service file, then `systemctl daemon-reload` and `systemctl restart ui`. -#### Organization-Specific Paths +**Suggestions not showing:** Confirm bot `.bas` files exist under `/opt/gbo/data/.gbai/.gbdialog/`. Check logs for compilation errors. Clear the AST cache in `/opt/gbo/work/` and restart botserver. -| Path Pattern | Purpose | -|--------------|---------| -| `gbo/orgs/{org_id}/config` | Organization configuration | -| `gbo/orgs/{org_id}/bots/{bot_id}` | Bot-specific secrets | -| `gbo/orgs/{org_id}/users/{user_id}` | User-specific secrets | -| `gbo/tenants/{tenant_id}/infrastructure` | Tenant database/cache/drive | -| `gbo/tenants/{tenant_id}/config` | Tenant configuration | +**IPv6 DNS timeouts on external APIs (Groq, Cloudflare):** The container's DNS may return AAAA records without IPv6 connectivity. The container should have `IPV6=no` in its network config and `gai.conf` set appropriately. Check for `RES_OPTIONS=inet4` in `botserver.service` if issues persist. -### Credential Resolution Hierarchy - -For bot email configuration (example): -``` -1. Check gbo/orgs/{org_id}/bots/{bot_id}/email -2. Fallback: gbo/bots/default/email -3. Fallback: gbo/email -4. Fallback: Environment variables (development only) -``` - -### Vault Client Initialization (Code Reference) - -**File:** `botserver/src/core/secrets/mod.rs` - -```rust -// SecretsManager::from_env() reads: -// - VAULT_ADDR (required) -// - VAULT_TOKEN (required) -// - VAULT_CACERT (optional, has default) -// - VAULT_SKIP_VERIFY (optional, default: false) -// - VAULT_CLIENT_CERT (optional, mTLS) -// - VAULT_CLIENT_KEY (optional, mTLS) -// - VAULT_CACHE_TTL (optional, default: 300s) - -impl SecretsManager { - pub fn from_env() -> Result { - let addr = env::var("VAULT_ADDR").unwrap_or_default(); - let token = env::var("VAULT_TOKEN").unwrap_or_default(); - - if token.is_empty() || addr.is_empty() { - // Vault not configured - use environment variables directly - warn!("Vault not configured. Using environment variables directly."); - return Ok(Self { client: None, enabled: false, ... }); - } - - // Initialize VaultClient with TLS - let client = VaultClient::new(settings)?; - Ok(Self { client: Some(client), enabled: true, ... }) - } -} -``` - -### Vault Operations - Production Usage - -#### Read Secrets from Vault - -```bash -# From system container (using vault CLI) -sudo incus exec system -- bash -c ' - export VAULT_ADDR=https://10.157.134.250:8200 - export VAULT_TOKEN= - export VAULT_CACERT=/opt/gbo/conf/system/certificates/ca/ca.crt - - # Read database secrets - vault kv get -field=password secret/gbo/tables - vault kv get secret/gbo/tables - - # Read drive secrets - vault kv get secret/gbo/drive - - # Read LLM configuration - vault kv get secret/gbo/llm -' -``` - -#### Read Secrets via HTTP API (from any container) - -```bash -sudo incus exec system -- curl -sf \ - --cacert /opt/gbo/conf/system/certificates/ca/ca.crt \ - -H "X-Vault-Token: " \ - https://10.157.134.250:8200/v1/secret/data/gbo/drive | jq -``` - -#### Verify Vault Health - -```bash -sudo incus exec vault -- curl -k -sf https://localhost:8200/v1/sys/health - -# Expected output: -# {"initialized":true,"sealed":false,"standby":false,"performance_standby":false,"replication_performance_mode":"disabled","replication_dr_mode":"disabled","server_time_utc":"2026-04-10T13:55:00.123Z"} -``` - -### init.json (Vault Initialization Data) - -**Location:** `/opt/gbo/bin/botserver-stack/conf/vault/vault-conf/init.json` - -**Purpose:** Stores Vault unseal keys and root token (created during Vault initialization) - -**Contents:** -```json -{ - "recovery_keys_b64": [], - "recovery_keys_hex": [], - "recovery_keys_shares": 0, - "recovery_keys_threshold": 0, - "root_token": "", - "unseal_keys_b64": ["<5 unseal keys base64-encoded>"], - "unseal_keys_hex": ["<5 unseal keys hex-encoded>"], - "unseal_shares": 5, - "unseal_threshold": 3 -} -``` - -**Security Notes:** -- `root_token`: Used to authenticate to Vault as admin -- `unseal_keys`: Required to unseal Vault after restart (5 keys, need 3 to unseal) -- **CRITICAL:** Store `init.json` in a secure, encrypted location -- Never commit `init.json` to git or store in repo - -### Troubleshooting Vault Connection - -#### Issue 1: Botserver cannot connect to Vault - -**Symptoms:** -- Logs show "Vault connection failed" -- Secrets fall back to defaults -- Bot cannot authenticate to database - -**Diagnosis:** -```bash -# Check Vault is running -sudo incus exec vault -- systemctl status vault - -# Check Vault health -sudo incus exec vault -- curl -k -sf https://localhost:8200/v1/sys/health - -# Check .env has Vault credentials -sudo incus exec system -- grep "^VAULT_" /opt/gbo/bin/.env - -# Test Vault connection from system container -sudo incus exec system -- bash -c ' - curl -k -sf --cacert /opt/gbo/conf/system/certificates/ca/ca.crt \ - -H "X-Vault-Token: $(grep VAULT_TOKEN /opt/gbo/bin/.env | cut -d= -f2)" \ - https://10.157.134.250:8200/v1/secret/data/gbo/tables -' -``` - -**Common Causes:** -1. Vault service not running (vault container stopped) -2. `VAULT_TOKEN` expired or invalid -3. TLS certificate path incorrect or CA certificate missing -4. Network connectivity between system and vault containers - -**Fix:** -```bash -# 1. Restart Vault if stopped -sudo incus exec vault -- systemctl restart vault - -# 2. Generate new token if expired -sudo incus exec vault -- bash -c ' - export VAULT_ADDR=https://localhost:8200 - export VAULT_TOKEN= - vault token create -policy="botserver" -ttl="8760h" -format=json | jq -r .auth.client_token -' - -# 3. Update .env with new token -sudo incus exec system -- sed -i "s|VAULT_TOKEN=.*|VAULT_TOKEN=|" /opt/gbo/bin/.env - -# 4. Restart botserver -sudo incus exec system -- systemctl restart botserver -``` - -#### Issue 2: Secrets not being read from Vault - -**Symptoms:** -- Logs show "Vault read failed for 'gbo/drive'" -- Services use default credentials -- DriveMonitor cannot access MinIO - -**Diagnosis:** -```bash -# Check if Vault has secrets configured -sudo incus exec system -- bash -c ' - export VAULT_ADDR=https://10.157.134.250:8200 - export VAULT_TOKEN=$(grep VAULT_TOKEN /opt/gbo/bin/.env | cut -d= -f2) - export VAULT_CACERT=/opt/gbo/conf/system/certificates/ca/ca.crt - - echo "=== Database Secrets ===" - vault kv get secret/gbo/tables || echo "NOT FOUND" - - echo "=== Drive Secrets ===" - vault kv get secret/gbo/drive || echo "NOT FOUND" - - echo "=== LLM Secrets ===" - vault kv get secret/gbo/llm || echo "NOT FOUND" -' -``` - -**Fix - Adding Secrets to Vault:** -```bash -sudo incus exec vault -- bash -c ' - export VAULT_ADDR=https://localhost:8200 - export VAULT_TOKEN= - - # Add database secrets - vault kv put secret/gbo/tables \ - host= \ - port=5432 \ - database=botserver \ - username=gbuser \ - password= - - # Add drive (MinIO) secrets - vault kv put secret/gbo/drive \ - host= \ - port=9100 \ - accesskey= \ - secret= - - # Add LLM secrets - vault kv put secret/gbo/llm \ - url=http://localhost:8085 \ - model=gpt-4 \ - openai_key= \ - anthropic_key= -' -``` - -#### Issue 3: Vault sealed after restart - -**Symptoms:** -- All Vault operations fail -- botserver cannot read secrets -- Logs show "Vault is sealed" - -**Diagnosis:** -```bash -sudo incus exec vault -- curl -k -sf https://localhost:8200/v1/sys/health | jq .sealed -``` - -**Fix - Unseal Vault:** -```bash -sudo incus exec vault -- bash -c ' - # Need 3 of 5 unseal keys from init.json - vault operator unseal - vault operator unseal - vault operator unseal - - # Verify unsealed - vault status -' -``` - -#### Issue 4: TLS certificate errors - -**Symptoms:** -- "certificate verify failed" errors -- TLS handshake failures -- curl: (60) SSL certificate problem - -**Diagnosis:** -```bash -sudo incus exec system -- bash -c ' - # Check CA certificate exists - ls -la /opt/gbo/conf/system/certificates/ca/ca.crt - - # Test certificate - openssl x509 -in /opt/gbo/conf/system/certificates/ca/ca.crt -text -noout -' -``` - -**Fix:** -```bash -# If CA cert is missing, copy from vault container -sudo incus exec vault -- cp /opt/gbo/conf/vault/ca.crt /tmp/ - -sudo incus exec system -- mkdir -p /opt/gbo/conf/system/certificates/ca/ -sudo incus exec system -- bash -c ' - # Copy certificate from vault container - incus file pull vault/opt/gbo/conf/vault/ca.crt /tmp/ca.crt - cp /tmp/ca.crt /opt/gbo/conf/system/certificates/ca/ - chmod 644 /opt/gbo/conf/system/certificates/ca/ca.crt -' -``` - -### Security Best Practices - -1. **Never commit secrets to git** - - No API keys, passwords, tokens in code - - Use Vault for ALL sensitive data - - Init secrets from `SecretsManager::from_env()` - -2. **Use Vault for all service credentials** - - Database passwords: `gbo/tables` - - MinIO keys: `gbo/drive` - - LLM API keys: `gbo/llm` - - Email passwords: `gbo/email` - -3. **Rotate credentials regularly** - - Generate new tokens/keys periodically - - Update Vault using `vault kv put` - - No need to restart services (next read gets new values) - -4. **Enable TLS/mTLS in production** - - Always use `VAULT_CACERT` - - Enable mTLS for critical services: `VAULT_CLIENT_CERT` + `VAULT_CLIENT_KEY` - - Never use `VAULT_SKIP_VERIFY=true` in production - -5. **Limit token lifetimes** - - Root token: single use or very short TTL - - Service tokens: limited to needed time (e.g., 8760h = 1 year) - - Generate new tokens when old ones expire - -6. **Audit Vault access** - ```bash - # Check recent Vault operations - sudo incus exec vault -- vault audit list - sudo incus exec vault -- vault audit file /var/log/vault_audit.log - ``` - -### Vault Backup & Recovery - -#### Backup Vault Data - -```bash -# Snapshot vault container (includes all secrets) -sudo incus snapshot create vault backup-$(date +%Y%m%d-%H%M) - -# Export Vault config (init.json with unseal keys) -sudo incus exec vault -- cat /opt/gbo/bin/botserver-stack/conf/vault/vault-conf/init.json > /tmp/vault-init.json - -# Backup all secrets (JSON format) -sudo incus exec vault -- bash -c ' - export VAULT_ADDR=https://localhost:8200 - export VAULT_TOKEN= - - # Backup each path - for path in gbo/tables gbo/drive gbo/cache gbo/llm; do - vault kv get -format=json secret/$path > /tmp/vault-$path.json - done -' -``` - -#### Restore from Snapshot - -```bash -# Stop vault -sudo incus exec vault -- systemctl stop vault - -# Restore snapshot -sudo incus snapshot restore vault - -# Start vault -sudo incus exec vault -- systemctl start vault - -# Wait for Vault to be ready -sleep 10 - -# Verify health -sudo incus exec vault -- curl -k -sf https://localhost:8200/v1/sys/health -``` - -## Troubleshooting - -### GLIBC Version Mismatch - -**Symptom**: `GLIBC_2.39 not found` or `GLIBC_2.38 not found` - -**Cause**: Binary compiled on CI runner (glibc 2.41) but runs in system container (glibc 2.36) - -**Fix**: CI workflow must build inside the system container. Check `botserver.yaml` uses SSH to build in container. - -### botserver Not Starting - -```bash -# Check binary -sudo incus exec system -- ldd /opt/gbo/bin/botserver | grep "not found" - -# Check direct execution -sudo incus exec system -- timeout 10 /opt/gbo/bin/botserver 2>&1 - -# Check data directory -sudo incus exec system -- ls -la /opt/gbo/data/ -``` - -### botui Can't Reach botserver - -```bash -# Check BOTSERVER_URL -sudo incus exec system -- grep BOTSERVER_URL /etc/systemd/system/ui.service - -# Must be http://localhost:5858, NOT https://system.example.com -# Fix: -sudo incus exec system -- sed -i 's|BOTSERVER_URL=.*|BOTSERVER_URL=http://localhost:5858|' /etc/systemd/system/ui.service -sudo incus exec system -- systemctl daemon-reload -sudo incus exec system -- systemctl restart ui -``` - -### Suggestions Not Showing - -```bash -# Check bot files exist -sudo incus exec system -- ls -la /opt/gbo/data/.gbai/.gbdialog/ - -# Check for compilation errors -sudo incus exec system -- tail -50 /opt/gbo/logs/out.log | grep -i "error\|fail\|compile" - -# Clear cache and restart -sudo incus exec system -- find /opt/gbo/work -name "*.ast" -delete -sudo incus exec system -- systemctl restart botserver -``` - -### IPv6 DNS Issues - -**Symptom**: External API calls (Groq, Cloudflare) timeout - -**Cause**: Container DNS returns AAAA records but no IPv6 connectivity - -**Fix**: Container has `IPV6=no` in network config and `gai.conf` labels. If issues persist, check `RES_OPTIONS=inet4` in botserver.service. - -### Vault Connection & Service Discovery Issues - -**Symptom**: Logs show `Failed to read data directory ` or `Config scan failed` - -**Cause**: Botserver is using hardcoded development paths instead of production paths - -**Fix**: - -1. **Check current configuration**: - ```bash - # Check .env file - sudo incus exec system -- cat /opt/gbo/bin/.env - - # Check data directory - sudo incus exec system -- ls -la /opt/gbo/data/ - sudo incus exec system -- ls -la /opt/gbo/work/ - ``` - -2. **Verify Vault connection**: - ```bash - # Test Vault from system container - sudo incus exec system -- curl -k -sf https://:8200/v1/sys/health - - # Check Vault token - sudo incus exec system -- grep VAULT_TOKEN /opt/gbo/bin/.env - ``` - -3. **Check service discovery**: - ```bash - # Check if botserver is reading Vault secrets - sudo incus exec system -- tail -100 /opt/gbo/logs/out.log | grep -i vault - - # Check for service configuration errors - sudo incus exec system -- tail -100 /opt/gbo/logs/err.log | grep -i "config\|service" - ``` - -4. **Fix data directory paths**: - - Ensure botserver uses `/opt/gbo/data/` instead of development paths - - Update configuration if hardcoded paths exist - - Restart botserver after fixing - -5. **Verify all services are accessible**: - ```bash - # Check PostgreSQL - sudo incus exec system -- pg_isready -h -p 5432 - - # Check Valkey - sudo incus exec system -- redis-cli -h -a ping - - # Check MinIO - sudo incus exec system -- curl -sf http://:9100/minio/health/live - ``` - -6. **Update botserver configuration**: - - Ensure botserver reads from `/opt/gbo/bin/.env` for Vault configuration - - Verify service discovery uses Vault to get service endpoints - - Check that data directory is set to `/opt/gbo/data/` in configuration - - Update systemd service if needed: - ```bash - sudo incus exec system -- cat /etc/systemd/system/botserver.service - # Ensure EnvironmentFile=/opt/gbo/bin/.env is present - ``` - -7. **Test after fixes**: - ```bash - # Restart botserver - sudo incus exec system -- systemctl restart botserver - - # Wait for startup - sleep 10 - - # Check logs for errors - sudo incus exec system -- tail -50 /opt/gbo/logs/err.log - - # Verify health endpoint - curl -sf http://:5858/health - ``` - -### Vault Connection Errors - -**Symptom**: `Vault connection failed` or `Vault token invalid` - -**Fix**: -```bash -# Check Vault is running -sudo incus exec vault -- systemctl status vault - -# Check Vault health -sudo incus exec vault -- curl -k -sf https://localhost:8200/v1/sys/health - -# Verify token is valid -sudo incus exec system -- bash -c ' - export VAULT_ADDR=https://:8200 - export VAULT_TOKEN= - export VAULT_CACERT=/opt/gbo/conf/system/certificates/ca/ca.crt - vault token lookup -' - -# If token is invalid, generate new one -sudo incus exec vault -- bash -c ' - export VAULT_ADDR=https://localhost:8200 - export VAULT_TOKEN= - vault token create -policy="botserver" -ttl="8760h" -' - -# Update .env with new token -sudo incus exec system -- sed -i 's|VAULT_TOKEN=.*|VAULT_TOKEN=|' /opt/gbo/bin/.env -sudo incus exec system -- systemctl restart botserver -``` - -### Service Discovery Failures - -**Symptom**: `Service not found` or `Failed to connect to service` - -**Fix**: -```bash -# Check if service is running -sudo incus exec tables -- systemctl status postgresql -sudo incus exec cache -- systemctl status valkey -sudo incus exec drive -- systemctl status minio - -# Check if service is accessible from system container -sudo incus exec system -- nc -zv 5432 # PostgreSQL -sudo incus exec system -- nc -zv 6379 # Valkey -sudo incus exec system -- nc -zv 9100 # MinIO - -# Check Vault has service configuration -sudo incus exec system -- bash -c ' - export VAULT_ADDR=https://:8200 - export VAULT_TOKEN= - export VAULT_CACERT=/opt/gbo/conf/system/certificates/ca/ca.crt - vault kv list secret/botserver -' - -# If service config is missing, add it (see Vault Configuration section) -``` - -### Monitoring & Verification - -**Check botserver is working correctly**: -```bash -# Health check -curl -sf http://:5858/health - -# Check logs for errors -sudo incus exec system -- tail -100 /opt/gbo/logs/err.log | grep -i "error\|fail" - -# Check logs for successful service connections -sudo incus exec system -- tail -100 /opt/gbo/logs/out.log | grep -i "connected\|service\|vault" - -# Verify data directory is correct -sudo incus exec system -- tail -100 /opt/gbo/logs/out.log | grep -i "data\|work" - -# Should show /opt/gbo/data/ and /opt/gbo/work/, not development paths -``` - -**Expected log output**: -``` -info vault:Connected to Vault at https://:8200 -info service_discovery:Loaded service configuration from Vault -info database:Connected to PostgreSQL at :5432 -info cache:Connected to Valkey at :6379 -info storage:Connected to MinIO at http://:9100 -info watcher:Watching data directory /opt/gbo/data -info botserver:BotServer started successfully on port 5858 -``` - -**If logs show errors**: -1. Check Vault connection (see Vault Connection Errors section) -2. Check service accessibility (see Service Discovery Failures section) -3. Fix data directory paths (see Fix Development Paths in Production section) -4. Restart botserver and verify again - -### Vault Backup & Restore - -**Create Vault snapshot**: -```bash -# Stop Vault -sudo incus exec vault -- systemctl stop vault - -# Create snapshot -sudo incus snapshot create vault manual-$(date +%Y-%m-%d-%H%M) - -# Start Vault -sudo incus exec vault -- systemctl start vault - -# Verify -sudo incus snapshot list vault -``` - -**Restore Vault from snapshot**: -```bash -# Stop Vault -sudo incus exec vault -- systemctl stop vault - -# List snapshots -sudo incus snapshot list vault - -# Restore from latest snapshot -sudo incus snapshot restore vault - -# Start Vault -sudo incus exec vault -- systemctl start vault - -# Verify Vault is running -sudo incus exec vault -- systemctl status vault -sudo incus exec vault -- curl -k -sf https://localhost:8200/v1/sys/health -``` - -**Automated snapshots**: -```bash -# Create cron job for daily snapshots -sudo incus exec vault -- bash -c 'cat > /etc/cron.daily/vault-snapshot << EOF -#!/bin/bash -systemctl stop vault -incus snapshot create vault daily-$(date +\%Y\%m\%d) -systemctl start vault -EOF -chmod +x /etc/cron.daily/vault-snapshot' -``` - -### Update Botserver for Production - -**Required changes in botserver code**: - -1. **Read configuration from Vault**: - - Add Vault client initialization - - Read service endpoints from Vault - - Read secrets from Vault - - Fallback to environment variables if Vault is unavailable - -2. **Use production paths**: - - Remove hardcoded development paths - - Use environment variables for data directory - - Default to `/opt/gbo/data/` for production - -3. **Update .env file**: - ```bash - # /opt/gbo/bin/.env - VAULT_ADDR=https://:8200 - VAULT_TOKEN= - VAULT_CACERT=/opt/gbo/conf/system/certificates/ca/ca.crt - DATA_DIR=/opt/gbo/data/ - WORK_DIR=/opt/gbo/work/ - PORT=5858 - ``` - -4. **Update systemd service**: - ```bash - sudo incus exec system -- cat > /etc/systemd/system/botserver.service << 'EOF' - [Unit] - Description=BotServer Service - After=network.target - - [Service] - User=root - Group=root - WorkingDirectory=/opt/gbo/bin - EnvironmentFile=/opt/gbo/bin/.env - ExecStart=/opt/gbo/bin/botserver --noconsole - Restart=always - RestartSec=5 - StandardOutput=append:/opt/gbo/logs/out.log - StandardError=append:/opt/gbo/logs/err.log - - [Install] - WantedBy=multi-user.target - EOF - - sudo incus exec system -- systemctl daemon-reload - sudo incus exec system -- systemctl restart botserver - ``` - -5. **Deploy updated botserver**: - ```bash - # Push changes to ALM - cd botserver && git push alm main && git push origin main - - # CI will build and deploy automatically - # Or manually deploy (see Manual Deploy section) - ``` - -## Security - -- **NEVER** push secrets to git -- **NEVER** commit files to root with credentials -- **Vault** is single source of truth for secrets -- **CI/CD** is the only deployment method — never manually scp binaries -- **ALM** is production — ask before pushing +**Logs show development paths instead of `/opt/gbo/data/`:** Botserver is using hardcoded dev paths. Check `.env` has `DATA_DIR=/opt/gbo/data/` and `WORK_DIR=/opt/gbo/work/`, verify the systemd unit has `EnvironmentFile=/opt/gbo/bin/.env`, and confirm Vault is reachable so service discovery works. Expected startup log lines include `info watcher:Watching data directory /opt/gbo/data` and `info botserver:BotServer started successfully on port 5858`. \ No newline at end of file diff --git a/prompts/container.md b/prompts/automate-incus.md similarity index 100% rename from prompts/container.md rename to prompts/automate-incus.md diff --git a/prompts/folha.md b/prompts/folha.md index 3bec6bd..71afeb1 100644 --- a/prompts/folha.md +++ b/prompts/folha.md @@ -1,7 +1,7 @@ -# SEPLAGSE - Detecção de Desvios na Folha +# detector - Detecção de Desvios na Folha ## Objetivo -- Bot seplagse deve usar start.bas para inserir dados via init_folha.bas +- Bot detector deve usar start.bas para inserir dados via init_folha.bas - detecta.bas deve detectar anomalias nos dados inseridos ## ✅ Status Atual @@ -23,13 +23,13 @@ Filtro adicionado para `REM ` e `REM\t` no `compile_tool_script`: ``` ### Arquivos Envolvidos (VERIFICADOS) -- `/opt/gbo/data/seplagse.gbai/seplagse.gbdialog/start.bas` ✅ OK +- `/opt/gbo/data/detector.gbai/detector.gbdialog/start.bas` ✅ OK - Contém botões de sugestão: detecta e init_folha -- `/opt/gbo/data/seplagse.gbai/seplagse.gbdialog/init_folha.bas` ✅ OK +- `/opt/gbo/data/detector.gbai/detector.gbdialog/init_folha.bas` ✅ OK - 4 INSERT statements para dados de exemplo -- `/opt/gbo/data/seplagse.gbai/seplagse.gbdialog/detecta.bas` ✅ OK +- `/opt/gbo/data/detector.gbai/detector.gbdialog/detecta.bas` ✅ OK - Usa DETECT keyword -- `/opt/gbo/data/seplagse.gbai/seplagse.gbdialog/tables.bas` ✅ OK +- `/opt/gbo/data/detector.gbai/detector.gbdialog/tables.bas` ✅ OK - TABLE folha_salarios definida ### Botserver (RODANDO) @@ -40,7 +40,7 @@ Filtro adicionado para `REM ` e `REM\t` no `compile_tool_script`: ## Próximos Passos (Pendentes) 1. **Testar via navegador** - Necessário instalar Playwright browsers - - Navegar para http://localhost:3000/seplagse + - Navegar para http://localhost:3000/detector - Clicar em "⚙️ Inicializar Dados de Teste" - Verificar se INSERT funciona - Clicar em "🔍 Detectar Desvios na Folha" @@ -50,10 +50,10 @@ Filtro adicionado para `REM ` e `REM\t` no `compile_tool_script`: - Alguns warnings de código podem precisar ser corrigidos ## Cache -- AST limpo: `rm ./botserver-stack/data/system/work/seplagse.gbai/seplagse.gbdialog/*.ast` +- AST limpo: `rm ./botserver-stack/data/system/work/detector.gbai/detector.gbdialog/*.ast` - Reiniciado: `./restart.sh` - Botserver: ✅ Rodando ## Arquivos de Trabalho -- Work directory: `./botserver-stack/data/system/work/seplagse.gbai/seplagse.gbdialog/` +- Work directory: `./botserver-stack/data/system/work/detector.gbai/detector.gbdialog/` - Todos os arquivos BASIC estão presentes e parecem válidos diff --git a/prompts/htmlview.md b/prompts/htmlview.md deleted file mode 100644 index de5dc48..0000000 --- a/prompts/htmlview.md +++ /dev/null @@ -1 +0,0 @@ -AYVRWxru3Ciwlw7E GXmWnXQYXjn1OoK4kWnY3579FJVYTGBT \ No newline at end of file diff --git a/prompts/nodrive.md b/prompts/nodrive.md deleted file mode 100644 index e84d21f..0000000 --- a/prompts/nodrive.md +++ /dev/null @@ -1,46 +0,0 @@ -# Progress: Removendo aws-sdk-s3 do default bundle - -## Goal -Remover `aws-sdk-s3` (~120MB) do bundle default `["chat", "automation", "cache", "llm"]` e fazer compilar com: -```bash -cargo check -p botserver --no-default-features --features "chat,automation,cache,llm" -``` - -## ✅ COMPLETED - -1. **Cargo.toml** - Features separadas: `drive` (S3) vs `local-files` (notify) -2. **main.rs** - `pub mod drive` com `#[cfg(any(feature = "drive", feature = "local-files"))]` -3. **state.rs** - `NoDrive` struct adicionada -4. **multimedia.rs** - `DefaultMultimediaHandler` com cfg gates (drive vs no-drive) -5. **drive/mod.rs** - Módulos condicionais: - - `#[cfg(feature = "drive")] pub mod document_processing;` - - `#[cfg(feature = "drive")] pub mod drive_monitor;` - - `#[cfg(feature = "drive")] pub mod vectordb;` - - `#[cfg(feature = "local-files")] pub mod local_file_monitor;` - - Todas ~21 funções com `#[cfg(feature = "drive")]` -6. **multimedia.rs - upload_media** - Duas implementações separadas com cfg gates: - - `#[cfg(feature = "drive")]` - Usa S3 client - - `#[cfg(not(feature = "drive"))]` - Usa armazenamento local - -## ✅ VERIFIED - -```bash -cargo check -p botserver --no-default-features --features "chat,automation,cache,llm" -``` - -**Resultado:** ✅ Build limpo (apenas warnings, 0 erros) -**Tempo de compilação:** 2m 29s - -## Arquivo Não Fixado (opcional) - -### auto_task/app_generator.rs -- `ensure_bucket_exists` method never used (warning, não impede compilação) -- Método já está com `#[cfg(feature = "drive")]` (correto) - -## Resumo - -O `aws-sdk-s3` foi removido com sucesso do bundle default. O sistema agora suporta dois modos: -- **Com feature "drive"**: Usa S3 (aws-sdk-s3 ~120MB) -- **Sem feature "drive"**: Usa armazenamento local (notify ~2MB) - -O build padrão agora é leve (~120MB a menos) e funciona sem dependências de AWS. diff --git a/prompts/switcher.md b/prompts/switcher.md new file mode 100644 index 0000000..22a5c84 --- /dev/null +++ b/prompts/switcher.md @@ -0,0 +1,434 @@ +# SWITCHER Feature - Response Format Modifiers + +## Overview +Add a switcher interface that allows users to toggle response modifiers that influence how the AI generates responses. Unlike suggestions (which are one-time actions), switchers are persistent toggles that remain active until deactivated. + +## Location +`botui/ui/suite/chat/` - alongside existing suggestion buttons + +## Syntax + +### Standard Switcher (predefined prompt) +``` +ADD SWITCHER "tables" AS "Tabelas" +``` + +### Custom Switcher (with custom prompt) +``` +ADD SWITCHER "sempre mostrar 10 perguntas" AS "Mostrar Perguntas" +``` + +## What Switcher Does + +The switcher: +1. **Injects the prompt** into every LLM request +2. **The prompt** can be: + - **Standard**: References a predefined prompt by ID (`"tables"`, `"cards"`, etc.) + - **Custom**: Any custom instruction string (`"sempre mostrar 10 perguntas"`) +3. **Influences** the AI response format +4. **Persists** until toggled OFF + +## Available Standard Switchers + +| ID | Label | Color | Description | +|----|--------|--------|-------------| +| tables | Tabelas | #4CAF50 | Format responses as tables | +| infographic | Infográfico | #2196F3 | Visual, graphical representations | +| cards | Cards | #FF9800 | Card-based layout | +| list | Lista | #9C27B0 | Bulleted lists | +| comparison | Comparação | #E91E63 | Side-by-side comparisons | +| timeline | Timeline | #00BCD4 | Chronological ordering | +| markdown | Markdown | #607D8B | Standard markdown | +| chart | Gráfico | #F44336 | Charts and diagrams | + +## Predefined Prompts (Backend) + +Each standard ID maps to a predefined prompt in the backend: + +``` +ID: tables +Prompt: "REGRAS DE FORMATO: SEMPRE retorne suas respostas em formato de tabela HTML usando , , , ,
, . Cada dado deve ser uma célula. Use cabeçalhos claros na primeira linha. Se houver dados numéricos, alinhe à direita. Se houver texto, alinhe à esquerda. Use cores sutis em linhas alternadas (nth-child). NÃO use markdown tables, use HTML puro." + +ID: infographic +Prompt: "REGRAS DE FORMATO: Crie representações visuais HTML usando SVG, progress bars, stat cards, e elementos gráficos. Use elementos como: para gráficos,
para barras de progresso, ícones emoji, badges coloridos. Organize informações visualmente com grids, flexbox, e espaçamento. Inclua legendas e rótulos visuais claros." + +ID: cards +Prompt: "REGRAS DE FORMATO: Retorne informações em formato de cards HTML. Cada card deve ter:
. Dentro do card use: título em

ou , subtítulo em

style="color:#666", ícone emoji ou ícone SVG no topo, badges de status. Organize cards em grid usando display:grid ou flex-wrap." + +ID: list +Prompt: "REGRAS DE FORMATO: Use apenas listas HTML:

    para bullets e
      para números numerados. Cada item em
    1. . Use sublistas aninhadas quando apropriado. NÃO use parágrafos de texto, converta tudo em itens de lista. Adicione ícones emoji no início de cada
    2. quando possível. Use classes CSS para estilização: .list-item, .sub-list." + +ID: comparison +Prompt: "REGRAS DE FORMATO: Crie comparações lado a lado em HTML. Use grid de 2 colunas:
      . Cada lado em uma
      com borda colorida distinta. Use headers claros para cada lado. Adicione seção de "Diferenças Chave" com bullet points. Use cores contrastantes para cada lado (ex: azul vs laranja). Inclua tabela de comparação resumida no final." + +ID: timeline +Prompt: "REGRAS DE FORMATO: Organize eventos cronologicamente em formato de timeline HTML. Use
      com border-left vertical. Cada evento em
      com: data em , título em

      , descrição em

      . Adicione círculo indicador na timeline line. Ordene do mais antigo para o mais recente. Use espaçamento claro entre eventos." + +ID: markdown +Prompt: "REGRAS DE FORMATO: Use exclusivamente formato Markdown padrão. Sintaxe permitida: **negrito**, *itálico*, `inline code`, ```bloco de código```, # cabeçalhos, - bullets, 1. números, [links](url), ![alt](url), | tabela | markdown |. NÃO use HTML tags exceto para blocos de código. Siga estritamente a sintaxe CommonMark." + +ID: chart +Prompt: "REGRAS DE FORMATO: Crie gráficos e diagramas em HTML SVG. Use elementos SVG: , para gráficos de linha, para gráficos de barra, para gráficos de pizza, para gráficos de área. Inclua eixos com labels, grid lines, legendas. Use cores distintas para cada série de dados (ex: vermelho, azul, verde). Adicione tooltips com valores ao hover. Se o usuário pedir gráfico de pizza com "pizza vermelha", use fill="#FF0000" no SVG." +``` + +## UI Design + +### HTML Structure +```html +

      +
      Formato:
      +
      + +
      +
      +``` + +### Placement +Position the switchers container **above** the suggestions container: +```html +
      +
      +
      + +
      +``` + +### CSS Styling + +#### Container +```css +.switchers-container { + display: flex; + align-items: center; + gap: 12px; + padding: 8px 16px; + flex-wrap: wrap; + background: rgba(0, 0, 0, 0.02); + border-top: 1px solid rgba(0, 0, 0, 0.05); +} + +.switchers-label { + font-size: 13px; + font-weight: 600; + color: #666; + text-transform: uppercase; + letter-spacing: 0.5px; +} +``` + +#### Switcher Chips (Toggle Buttons) +```css +.switchers-chips { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.switcher-chip { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + border-radius: 20px; + border: 2px solid transparent; + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + background: rgba(0, 0, 0, 0.05); + color: #666; + user-select: none; +} + +.switcher-chip:hover { + background: rgba(0, 0, 0, 0.08); + transform: translateY(-1px); +} + +.switcher-chip.active { + border-color: currentColor; + background: currentColor; + color: white; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); +} + +.switcher-chip-icon { + font-size: 14px; +} +``` + +## JavaScript Implementation + +### State Management +```javascript +// Track active switchers +var activeSwitchers = new Set(); + +// Switcher definitions (from ADD SWITCHER commands in start.bas) +var switcherDefinitions = [ + { + id: 'tables', + label: 'Tabelas', + icon: '📊', + color: '#4CAF50' + }, + { + id: 'infographic', + label: 'Infográfico', + icon: '📈', + color: '#2196F3' + }, + { + id: 'cards', + label: 'Cards', + icon: '🃏', + color: '#FF9800' + }, + { + id: 'list', + label: 'Lista', + icon: '📋', + color: '#9C27B0' + }, + { + id: 'comparison', + label: 'Comparação', + icon: '⚖️', + color: '#E91E63' + }, + { + id: 'timeline', + label: 'Timeline', + icon: '📅', + color: '#00BCD4' + }, + { + id: 'markdown', + label: 'Markdown', + icon: '📝', + color: '#607D8B' + }, + { + id: 'chart', + label: 'Gráfico', + icon: '📉', + color: '#F44336' + } +]; +``` + +### Render Switchers +```javascript +function renderSwitchers() { + var container = document.getElementById("switcherChips"); + if (!container) return; + + container.innerHTML = switcherDefinitions.map(function(sw) { + var isActive = activeSwitchers.has(sw.id); + return ( + '
      ' + + '' + sw.icon + '' + + '' + sw.label + '' + + '
      ' + ); + }).join(''); + + // Add click handlers + container.querySelectorAll('.switcher-chip').forEach(function(chip) { + chip.addEventListener('click', function() { + toggleSwitcher(this.getAttribute('data-switch-id')); + }); + }); +} +``` + +### Toggle Handler +```javascript +function toggleSwitcher(switcherId) { + if (activeSwitchers.has(switcherId)) { + activeSwitchers.delete(switcherId); + } else { + activeSwitchers.add(switcherId); + } + renderSwitchers(); +} +``` + +### Message Enhancement +When sending a user message, prepend active switcher prompts: + +```javascript +function sendMessage(messageContent) { + // ... existing code ... + + var content = messageContent || input.value.trim(); + if (!content) return; + + // Prepend active switcher prompts + var enhancedContent = content; + if (activeSwitchers.size > 0) { + // Get prompts for active switchers from backend + var activePrompts = []; + activeSwitchers.forEach(function(id) { + // Backend has predefined prompts for each ID + activePrompts.push(getSwitcherPrompt(id)); + }); + + // Inject prompts before user message + if (activePrompts.length > 0) { + enhancedContent = activePrompts.join('\n\n') + '\n\n---\n\n' + content; + } + } + + // Send enhanced content + addMessage("user", content); + + if (ws && ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ + bot_id: currentBotId, + user_id: currentUserId, + session_id: currentSessionId, + channel: "web", + content: enhancedContent, + message_type: MessageType.USER, + timestamp: new Date().toISOString(), + })); + } +} + +function getSwitcherPrompt(switcherId) { + // Get predefined prompt from backend or API + // For example, tables ID maps to: + // "REGRAS DE FORMATO: SEMPRE retorne suas respostas em formato de tabela HTML..." + var switcher = switcherDefinitions.find(function(s) { return s.id === switcherId; }); + if (!switcher) return ""; + + // This could be fetched from backend or stored locally + return SWITCHER_PROMPTS[switcherId] || ""; +} +``` + +## Bot Integration (start.bas) + +The bot receives the switcher prompt injected into the user message and simply passes it to the LLM. + +### Example in start.bas + +```basic +REM Switcher prompts are automatically injected by frontend +REM Just pass user_input to LLM - no parsing needed! + +REM If user types: "mostra os cursos" +REM And "Tabelas" switcher is active +REM Frontend sends: "REGRAS DE FORMATO: SEMPRE retorne suas respostas em formato de tabela HTML... --- mostra os cursos" + +REM Bot passes directly to LLM: +response$ = CALL_LLM(user_input) + +REM The LLM will follow the REGRAS DE FORMATO instructions +``` + +### Multiple Active Switchers + +When multiple switchers are active, all prompts are injected: + +```basic +REM Frontend injects multiple REGRAS DE FORMATO blocks +REM Example with "Tabelas" and "Gráfico" active: +REM +REM "REGRAS DE FORMATO: SEMPRE retorne suas respostas em formato de tabela HTML... +REM REGRAS DE FORMATO: Crie gráficos e diagramas em HTML SVG... +REM --- +REM mostra os dados de vendas" + +REM Bot passes to LLM: +response$ = CALL_LLM(user_input) +``` + +## Implementation Steps + +1. ✅ Create prompts/switcher.md (this file) +2. ⬜ Define predefined prompts in backend (map IDs to prompt strings) +3. ⬜ Add HTML structure to chat.html (switchers container) +4. ⬜ Add CSS styles to chat.css (switcher chip styles) +5. ⬜ Add switcher definitions to chat.js +6. ⬜ Implement renderSwitchers() function +7. ⬜ Implement toggleSwitcher() function +8. ⬜ Modify sendMessage() to prepend switcher prompts +9. ⬜ Update salesianos bot start.bas to use ADD SWITCHER commands +10. ⬜ Test locally with all switcher options +11. ⬜ Verify multiple switchers can be active simultaneously +12. ⬜ Test persistence across page refreshes (optional - localStorage) + +## Testing Checklist + +- [ ] Switchers appear above suggestions +- [ ] Switchers are colorful and match their defined colors +- [ ] Clicking a switcher toggles it on/off +- [ ] Multiple switchers can be active simultaneously +- [ ] Active switchers have distinct visual state (border, background, shadow) +- [ ] Formatted responses match the selected format +- [ ] Toggling off removes the format modifier +- [ ] Works with empty active switchers (normal response) +- [ ] Works in combination with suggestions +- [ ] Responsive design on mobile devices + +## Files to Modify + +1. `botui/ui/suite/chat/chat.html` - Add switcher container HTML +2. `botui/ui/suite/chat/chat.css` - Add switcher styles +3. `botui/ui/suite/chat/chat.js` - Add switcher logic +4. `botserver/bots/salesianos/start.bas` - Add ADD SWITCHER commands + +## Example start.bas + +```basic +USE_WEBSITE("https://salesianos.br", "30d") + +USE KB "carta" +USE KB "proc" + +USE TOOL "inscricao" +USE TOOL "consultar_inscricao" +USE TOOL "agendamento_visita" +USE TOOL "informacoes_curso" +USE TOOL "documentos_necessarios" +USE TOOL "contato_secretaria" +USE TOOL "calendario_letivo" + +ADD_SUGGESTION_TOOL "inscricao" AS "Fazer Inscrição" +ADD_SUGGESTION_TOOL "consultar_inscricao" AS "Consultar Inscrição" +ADD_SUGGESTION_TOOL "agendamento_visita" AS "Agendar Visita" +ADD_SUGGESTION_TOOL "informacoes_curso" AS "Informações de Cursos" +ADD_SUGGESTION_TOOL "documentos_necessarios" AS "Documentos Necessários" +ADD_SUGGESTION_TOOL "contato_secretaria" AS "Falar com Secretaria" +ADD_SUGGESTION_TOOL "segunda_via" AS "Segunda Via de Boleto" +ADD_SUGGESTION_TOOL "calendario_letivo" AS "Calendário Letivo" +ADD_SUGGESTION_TOOL "outros" AS "Outros" + +ADD SWITCHER "tables" AS "Tabelas" +ADD SWITCHER "infographic" AS "Infográfico" +ADD SWITCHER "cards" AS "Cards" +ADD SWITCHER "list" AS "Lista" +ADD SWITCHER "comparison" AS "Comparação" +ADD SWITCHER "timeline" AS "Timeline" +ADD SWITCHER "markdown" AS "Markdown" +ADD SWITCHER "chart" AS "Gráfico" + +TALK "Olá! Sou o assistente virtual da Escola Salesiana. Como posso ajudá-lo hoje com inscrições, visitas, informações sobre cursos, documentos ou calendário letivo? Você pode também escolher formatos de resposta acima da caixa de mensagem." +``` + +## Notes + +- Switchers are **persistent** until deactivated +- Multiple switchers can be active at once +- Switcher prompts are prepended to user messages with "---" separator +- The backend (LLM) should follow these format instructions +- UI should provide clear visual feedback for active switchers +- Color coding helps users quickly identify active formats +- Standard switchers use predefined prompts in backend +- Custom switchers allow any prompt string to be injected diff --git a/setup_zitadel.sh b/setup_zitadel.sh new file mode 100755 index 0000000..3a9f35e --- /dev/null +++ b/setup_zitadel.sh @@ -0,0 +1,194 @@ +#!/bin/bash + +# Script para configurar domínios, organizações e usuários no Zitadel via API +# Uso: ./setup_zitadel.sh + +PAT_TOKEN=$1 +INSTANCE_ID="367250249682552560" +BASE_URL="http://10.157.134.240:8080" + +if [ -z "$PAT_TOKEN" ]; then + echo "ERRO: É necessário fornecer um PAT token válido" + echo "Uso: $0 " + echo "" + echo "Para obter um PAT token, acesse:" + echo "https://login.pragmatismo.com.br/ui/console" + echo "Login: admin / Admin123!" + echo "Depois vá em Profile -> Personal Access Tokens -> New" + exit 1 +fi + +echo "=== Configuração do Zitadel via API ===" +echo "" + +# 1. Adicionar domínios à instância +echo "1. Adicionando domínios à instância..." + +echo " a) Adicionando domínio pragmatismo.com.br..." +curl -s -X PUT "$BASE_URL/management/v1/instances/$INSTANCE_ID/domains/pragmatismo.com.br" \ + -H "Authorization: Bearer $PAT_TOKEN" \ + -H "Host: 10.157.134.240" \ + -H "Content-Type: application/json" \ + -d '{ + "isVerified": true, + "isPrimary": true, + "generator": false + }' | python3 -m json.tool + +echo "" +echo " b) Adicionando domínio salesianos.br..." +curl -s -X PUT "$BASE_URL/management/v1/instances/$INSTANCE_ID/domains/salesianos.br" \ + -H "Authorization: Bearer $PAT_TOKEN" \ + -H "Host: 10.157.134.240" \ + -H "Content-Type: application/json" \ + -d '{ + "isVerified": true, + "isPrimary": false, + "generator": false + }' | python3 -m json.tool + +echo "" + +# 2. Criar organização Pragmatismo +echo "2. Criando organização Pragmatismo..." +PRAGMATISMO_ORG_RESPONSE=$(curl -s -X POST "$BASE_URL/management/v1/orgs" \ + -H "Authorization: Bearer $PAT_TOKEN" \ + -H "Host: 10.157.134.240" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Pragmatismo" + }') + +echo "$PRAGMATISMO_ORG_RESPONSE" | python3 -m json.tool +PRAGMATISMO_ORG_ID=$(echo "$PRAGMATISMO_ORG_RESPONSE" | python3 -c "import sys, json; print(json.load(sys.stdin)['id'])") +echo " Org ID: $PRAGMATISMO_ORG_ID" + +echo "" + +# 3. Adicionar domínio pragmatismo.com.br à organização Pragmatismo +echo "3. Adicionando domínio pragmatismo.com.br à organização Pragmatismo..." +curl -s -X POST "$BASE_URL/management/v1/orgs/$PRAGMATISMO_ORG_ID/domains" \ + -H "Authorization: Bearer $PAT_TOKEN" \ + -H "Host: 10.157.134.240" \ + -H "Content-Type: application/json" \ + -d '{ + "domain": "pragmatismo.com.br", + "isVerified": true, + "isPrimary": true + }' | python3 -m json.tool + +echo "" + +# 4. Criar organização Salesianos +echo "4. Criando organização Salesianos..." +SALESIANOS_ORG_RESPONSE=$(curl -s -X POST "$BASE_URL/management/v1/orgs" \ + -H "Authorization: Bearer $PAT_TOKEN" \ + -H "Host: 10.157.134.240" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Salesianos" + }') + +echo "$SALESIANOS_ORG_RESPONSE" | python3 -m json.tool +SALESIANOS_ORG_ID=$(echo "$SALESIANOS_ORG_RESPONSE" | python3 -c "import sys, json; print(json.load(sys.stdin)['id'])") +echo " Org ID: $SALESIANOS_ORG_ID" + +echo "" + +# 5. Adicionar domínio salesianos.br à organização Salesianos +echo "5. Adicionando domínio salesianos.br à organização Salesianos..." +curl -s -X POST "$BASE_URL/management/v1/orgs/$SALESIANOS_ORG_ID/domains" \ + -H "Authorization: Bearer $PAT_TOKEN" \ + -H "Host: 10.157.134.240" \ + -H "Content-Type: application/json" \ + -d '{ + "domain": "salesianos.br", + "isVerified": true, + "isPrimary": true + }' | python3 -m json.tool + +echo "" + +# 6. Criar usuário rodriguez@pragmatismo.com.br +echo "6. Criando usuário rodriguez@pragmatismo.com.br..." +RODRIGUEZ_USER_RESPONSE=$(curl -s -X POST "$BASE_URL/management/v1/users" \ + -H "Authorization: Bearer $PAT_TOKEN" \ + -H "Host: 10.157.134.240" \ + -H "Content-Type: application/json" \ + -d '{ + "userName": "rodriguez", + "email": "rodriguez@pragmatismo.com.br", + "profile": { + "firstName": "Rodriguez", + "lastName": "Pragmatismo" + }, + "isEmailVerified": true, + "password": "imago10$", + "passwordChangeRequired": false + }') + +echo "$RODRIGUEZ_USER_RESPONSE" | python3 -m json.tool +RODRIGUEZ_USER_ID=$(echo "$RODRIGUEZ_USER_RESPONSE" | python3 -c "import sys, json; print(json.load(sys.stdin)['userId'])") +echo " User ID: $RODRIGUEZ_USER_ID" + +echo "" + +# 7. Adicionar rodriguez à organização Pragmatismo com roles +echo "7. Adicionando rodriguez à organização Pragmatismo com roles..." +curl -s -X POST "$BASE_URL/management/v1/orgs/$PRAGMATISMO_ORG_ID/members/$RODRIGUEZ_USER_ID" \ + -H "Authorization: Bearer $PAT_TOKEN" \ + -H "Host: 10.157.134.240" \ + -H "Content-Type: application/json" \ + -d '{ + "roles": ["ORG_OWNER"] + }' | python3 -m json.tool + +echo "" + +# 8. Criar usuário marcelo.alves@salesianos.br +echo "8. Criando usuário marcelo.alves@salesianos.br..." +MARCELO_USER_RESPONSE=$(curl -s -X POST "$BASE_URL/management/v1/users" \ + -H "Authorization: Bearer $PAT_TOKEN" \ + -H "Host: 10.157.134.240" \ + -H "Content-Type: application/json" \ + -d '{ + "userName": "marcelo.alves", + "email": "marcelo.alves@salesianos.br", + "profile": { + "firstName": "Marcelo", + "lastName": "Alves" + }, + "isEmailVerified": true, + "password": "imago10$", + "passwordChangeRequired": false + }') + +echo "$MARCELO_USER_RESPONSE" | python3 -m json.tool +MARCELO_USER_ID=$(echo "$MARCELO_USER_RESPONSE" | python3 -c "import sys, json; print(json.load(sys.stdin)['userId'])") +echo " User ID: $MARCELO_USER_ID" + +echo "" + +# 9. Adicionar marcelo à organização Salesianos com roles +echo "9. Adicionando marcelo à organização Salesianos com roles..." +curl -s -X POST "$BASE_URL/management/v1/orgs/$SALESIANOS_ORG_ID/members/$MARCELO_USER_ID" \ + -H "Authorization: Bearer $PAT_TOKEN" \ + -H "Host: 10.157.134.240" \ + -H "Content-Type: application/json" \ + -d '{ + "roles": ["ORG_OWNER"] + }' | python3 -m json.tool + +echo "" +echo "=== Configuração concluída! ===" +echo "" +echo "Resumo:" +echo "- Domínios adicionados: pragmatismo.com.br, salesianos.br" +echo "- Organização Pragmatismo criada (ID: $PRAGMATISMO_ORG_ID)" +echo "- Organização Salesianos criada (ID: $SALESIANOS_ORG_ID)" +echo "- Usuário rodriguez@pragmatismo.com.br criado (ID: $RODRIGUEZ_USER_ID)" +echo "- Usuário marcelo.alves@salesianos.br criado (ID: $MARCELO_USER_ID)" +echo "" +echo "Senha para ambos os usuários: imago10$" +echo "" +echo "Para fazer login, acesse: https://login.pragmatismo.com.br/ui/console"