47a183aa65bdf17b7c8d601f7efe97cbde515e2a
578 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
47a183aa65 |
feat(system-admin): implement bootstrap for system admin promotion and enhance system settings management
- Added WEKNORA_BOOTSTRAP_SYSTEM_ADMIN_EMAIL environment variable to promote a specified user to system admin on startup. - Introduced a new bootstrap process in `bootstrap.go` to handle the promotion logic. - Updated `.env.example` to document the new environment variable and its behavior. - Created new views for managing system administrators and system settings, including listing, promoting, and revoking admin privileges. - Enhanced the frontend to reflect the new system admin features, including UI elements for admin management and settings configuration. - Updated API interfaces to support system admin functionalities, ensuring proper data handling and user management. |
||
|
|
e6ee87759d |
fix(agent): preserve agent intent prompt whitespace and add tests
The intent prompt override logic in query_understand applied strings.TrimSpace to the value before assigning it as the system prompt override, which silently stripped trailing newlines and intentional formatting from agent-supplied prompts. Use TrimSpace only to detect emptiness (so whitespace-only strings still fall back to the global default) while passing the raw string through verbatim. Extract the resolution into applyIntentPromptOverride and add unit tests covering agent-wins, whitespace preservation, blank fallback, no-config, and global-only paths. |
||
|
|
11c3236e52 |
feat(retriever): add OpenSearch driver skeleton + interface stubs (PR 2a of 3)
First half of the gated OpenSearch k-NN driver introduced in PR 1 (#1445) by way of #1440. PR 2a ships a hollow, interface-compliant shell of the `internal/application/repository/retriever/opensearch/` package — every behavioural method (Save / BatchSave / DeleteBy* / Retrieve, plus the previously-stubbed CopyIndices / BatchUpdate* / EstimateStorageSize / swapToVersion) returns `ErrFeatureNotEnabled` or a conservative sentinel value. PR 2b lands the real read/write implementations in dedicated files (`query.go` + `retrieve.go` + `crud.go`) and replaces the stubs accordingly. Strict feature-gate (unchanged from PR 1): no entry is added to validEngineTypes / GetVectorStoreTypes / retrieverEngineMapping / BuildEnvVectorStores / container env path / engine factory switch, so the driver remains unreachable. Attempting to register an `engine_type=opensearch` VectorStore continues to fail with the existing "not a valid engine type" error. What lands in PR 2a ------------------- Driver skeleton (6 production files + 2 test files, ~1170 + ~1115 LoC): - `repository.go` — Repository struct + NewRepository constructor that validates cluster reachability + OS version (2.4+ / 3.x; primary tested 3.3.2) + k-NN plugin presence on every cluster node. sync.Once-guarded ensureReady(ctx, dim) for lazy per- dimension index creation, with transient errors not cached so a momentary cluster blip does not permanently poison a dim. sanitizeIndexName enforces a strict OS-compatible name spec. probeVersion uses robust strings.Split/Atoi parsing for pre-release suffixes and missing-patch versions. EngineType returns the PR 1 constant; Support returns [keywords, vector]. - `transport.go` — newOpenSearchClient ships TLS posture (MinVersion TLS 1.2, opt-in InsecureSkipVerify, forward-secrecy- only cipher list) and transport tuning for the driver. Caller exists only in PR 3 (container.go + engine_factory.go); PR 2a remains gated dead code. - `mapping.go` — buildIndexMapping(cfg, dim) produces the full knn_vector + HNSW + content-analyzer mapping with every *_id field as an explicit keyword and source_type as integer. buildKeywordsMapping ships the dim-less keyword-only index mapping used by the no-embedding save path. createIndexAndAlias creates <alias>_v1 and aliases <alias> to it, with best-effort orphan cleanup and mapping-drift detection. - `config.go` — internalCfg (value type) applying OpenSearch defaults (hnsw_m=16, ef_construction=100, ef_search=100, shards=4, replicas=1, engine=lucene). - `errors.go` — nine sentinels (ErrIndexNotFound, ErrDimensionMismatch, ErrAuth, ErrTransport, ErrVersionUnsupported, ErrConfigInvalid, ErrFeatureNotEnabled, ErrBatchTooLarge, ErrCircuitBreaker). Repository never imports apperrors; PR 3's engine factory wraps these to typed AppError 2200/2201. - `stubs.go` — every behavioural method returns ErrFeatureNotEnabled. EstimateStorageSize returns a conservative HNSW lower-bound estimate (not 0) so the Phase 2 KB-delete guard fails-closed for non-empty KBs. Tests (~1115 LoC, 50 cases): - `repository_test.go` — interface satisfaction, sentinel mapping, sanitizeIndexName positive/negative matrix, semver parsing (pre-release / missing-patch), buildIndexMapping JSON shape pin (Lucene + Faiss + Keywords), probeVersion matrix (OS 1.x / 2.2 / 2.5 / 2.11 / 3.x / 3.0.0-rc1 / ES rejection), probeKNNPlugin multi-node coverage, ensureReady concurrency + per-dim isolation + transient retry, NewRepository storeID validation, all 11 stubs (CopyIndices, BatchUpdate*, EstimateStorageSize, SwapToVersion + Save / BatchSave / Retrieve / DeleteBy*), wrapTransport sentinel mapping + leak guard, isNotFound / isAlreadyExistsError, drainAndClose / limitedDecode helpers. - `transport_test.go` — TLS defaults / opt-in InsecureSkipVerify / TLS 1.2 pinning / cipher list / transport tuning. Single dependency addition: github.com/opensearch-project/ opensearch-go/v4 v4.6.0 in go.mod/go.sum. SDK quirks discovered (opensearch-go v4.6.0) -------------------------------------------- PR 2a includes the workarounds for two of three SDK limitations that landed during full implementation (the third, Refresh:*bool, only affects the delete path that ships in PR 2b): - AliasExists method passes dataPointer=nil to its internal do(), which means non-2xx responses come back as a plain *errors.errorString ("status: 404 Not Found") rather than as *opensearch.StructError. aliasExists therefore inspects resp.StatusCode directly (resp is returned even when err is non-nil) and only falls back to wrapTransport for the "no response at all" case. - sync.OnceReset is not in the standard library; the keyword-only index uses a mutex + ready/err flag pattern so transient failures can be retried by the next caller. The per-dimension path uses the `once map[int]*sync.Once` delete-and-recreate trick. Test fixes folded in -------------------- While doing a full `go test ./...` against PR 1-merged main, two deterministic regressions surfaced that block a clean run-everything signal. Both are unrelated to the driver and are folded into PR 2a so the PR's own CI run is green: (1) Follow-up to #1445 — fanout test missed the new normalizer policy (internal/application/service/knowledgebase_search_fanout_test.go, +46 / -6). #1445 changed EngineAwareNormalizer for ES / ElasticFaiss / OpenSearch / Weaviate / Postgres / SQLite / Qdrant / TencentVectorDB / Doris from (score+1)/2 to clamp01 passthrough (those engines surface non-negative cosine to the normalizer per Lucene script_score non-negative invariant for ES, k-NN plugin SpaceType.COSINESIMIL.scoreTranslation for OpenSearch, engine-internal or IR-normalized conversions for the rest). Milvus is now the only engine that still surfaces raw signed cosine in [-1, 1]. TestRetrieveFromStores_MixedEngine_Normalizes still asserted the old cosine-shift behaviour for ES (raw -0.4 → expected 0.3) which under passthrough now becomes clamp01(-0.4) = 0. The normalizer's own _test.go was updated at #1445 time, but this fan-out integration test was not. Fix: rewrite the godoc to spell out the two engine groups; restate sub-case 2 as ES passthrough on a production-possible mid-range cosine (0.3 → 0.3, PG out-ranks ES); add sub-case 3 pinning the cosine-shift branch via Milvus -0.4 → 0.3. (2) Pre-existing — SSRF whitelist singleton race surfaced by this run (internal/utils/security.go + internal/utils/security_test.go + internal/infrastructure/web_search/searxng_test.go, +33 / -9). loadSSRFWhitelist in internal/utils/security.go is cached via sync.Once on first call. The internal reset helper resetSSRFWhitelistForTest was unexported, so tests in other packages could not reset and saw whatever whitelist was cached by the first sync.Once.Do() in the same test binary. In internal/infrastructure/web_search/, TestValidateProxyURL runs before TestValidateSearxngBaseURL alphabetically and exercises ValidateURLForSSRF with no SSRF_WHITELIST set, caching an empty whitelist; the later setenv in searxng_test then has no effect and 127.0.0.1 is rejected with "hostname 127.0.0.1 is restricted". Pre-existing on main; surfaced now because this PR was the first to do a full `go test ./...` run on top of #1445. Fix: capitalize the helper to ResetSSRFWhitelistForTest (the ForTest suffix is the test-only contract); update in-package callers; in web_search/searxng_test.go import internal/utils and call ResetSSRFWhitelistForTest around the env mutation in both TestValidateSearxngBaseURL and TestSearxngProvider_Search. No production code path changes. Roadmap ------- - PR 2b (next, depends on this PR) — read/write implementations: query.go + retrieve.go + crud.go land their real bodies; stubs for Save / BatchSave / DeleteBy* / Retrieve in stubs.go are removed; corresponding CRUD/retrieve/filter test cases (~430 LoC) join repository_test.go. - PR 3 — activation switch + async paths (CopyIndices, BatchUpdate*, large-batch async deletes) + i18n + docker-compose dev profile. After PR 3 merges, the OpenSearch driver becomes reachable via either `engine_type=opensearch` VectorStore or `RETRIEVE_DRIVER=opensearch` env. Backward compatibility ---------------------- - New package — additive only. No existing file modified except go.mod / go.sum, the two test files in (1)/(2), and the test-only export rename in utils/security.go. - Driver is unreachable: no registry path activates it. - No SQL migration. - The PR 1 normalizer case for OpenSearch remains unreachable here (no driver instance produces a result yet). Test plan --------- - [x] go build ./... clean - [x] go vet ./... clean - [x] go test -race -count=1 ./internal/application/repository/retriever/opensearch/... passes - [x] grep -r "case types.OpenSearchRetrieverEngineType" internal/ shows only PR 1's normalizer case + this driver's EngineType() and tests — no activation path. - [x] grep -r "case \"opensearch\"" internal/ shows no hits. |
||
|
|
80a69ae6f3 |
fix(pipeline): fallback to raw retrieval results when rerank API fails
When the rerank model returns an error (e.g. 401 Unauthorized), the pipeline previously discarded all retrieved candidates and returned empty results to the caller. Now p.rerank returns ([]RankResult, error) to distinguish API failure from threshold-filtered empty results. On API error, the pipeline falls back to the original retrieval candidates (directLoad + candidatesToRerank) and continues to CHUNK_MERGE/FILTER_TOP_K, so users still get results even when the rerank model is misconfigured. |
||
|
|
e6a469631b |
feat(agent): add intent prompt customization in agent editor
Allow per-intent system prompt overrides for non-retrieval intents in normal mode. Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
09892ef763 |
fix(doris): inline LIMIT/OFFSET as literals and enable parameter interpolation
Problem: Doris does not support ? placeholders for LIMIT and OFFSET clauses, causing VectorRetrieve, KeywordsRetrieve, and CopyIndices queries to fail at runtime with SQL parse errors. Root cause: 1. Query builders passed TopK/pageSize/offset as ? bind parameters (valid in MySQL/PostgreSQL but not in Doris's SQL dialect) 2. Doris MySQL driver was not configured to interpolate parameters, preventing automatic client-side placeholder replacement Fix: 1. Enable parameter interpolation in Doris connection DSN: - Add &interpolateParams=true to mysql.Open() DSN in container.go - This enables driver-level parameter substitution at query time 2. Inline LIMIT/OFFSET as string literals in Doris queries: - VectorRetrieve: LIMIT ? → LIMIT %d (params.TopK) - KeywordsRetrieve: LIMIT ? → LIMIT %d (params.TopK) - CopyIndices: LIMIT ? OFFSET ? → LIMIT %d OFFSET %d (pageSize, offset) - Remove inlined values from args slice Result: Queries are now built with literal LIMIT/OFFSET values instead of placeholders, compatible with Doris's SQL parser. |
||
|
|
e9be53e830 |
fix: respect multi-turn-disabled flag in KnowledgeQA pipeline
When a custom agent has `MultiTurnEnabled=false`, `applyAgentOverridesToChatManage`
sets `chatManage.MaxRounds = 0` to signal "no history". Two pipeline plugins
mistreated this zero value as "use the global default" and silently re-loaded
session history into the LLM context:
- `PluginLoadHistory` fell back to `Conversation.MaxRounds` when
`chatManage.MaxRounds == 0`.
- `PluginQueryUnderstand.loadHistory` had the same fallback, and even when
`LOAD_HISTORY` was skipped it would re-populate `chatManage.History`,
leaking previous turns into rewrite, image analysis, and the final answer.
The RAG branch in `session_knowledge_qa` also added `LOAD_HISTORY`
unconditionally, unlike the pure-chat branch which guarded it with `hasHistory`.
This change:
- Treats `chatManage.MaxRounds <= 0` as an explicit disable in both plugins;
no fallback to global config.
- Makes the RAG pipeline consistent with the pure-chat path by gating
`LOAD_HISTORY` on `hasHistory`.
- Removes the duplicated `Current Time: {{current_time}}` line from
`agent_system_prompt.yaml`. The agent already receives a fresh
`<runtime_context><current_time>` block with each turn from
`observe.buildRuntimeContextBlock`, so the static placeholder was
redundant.
The ReAct agent path (`session_agent_qa`) already checked `MultiTurnEnabled`
directly and is not affected.
Closes #1479
|
||
|
|
31471f5efa |
refactor(knowledgeService): update checkStorageEngineConfigured method for improved clarity and functionality
- Refactored checkStorageEngineConfigured to be a method of knowledgeService, enhancing encapsulation and readability. - Updated logic to allow fallback to global file service when no storage provider is configured at the KB or tenant level, improving error handling. - Added detailed comments to clarify the method's behavior and internal logic, ensuring better understanding for future maintenance. |
||
|
|
6331cce23d | fix: skip empty milvus enabled status groups | ||
|
|
744a367f16 |
feat(retriever): add OpenSearch type prep ahead of Phase 3 driver
Lays type-system groundwork for the upcoming OpenSearch k-NN driver (Phase 3 PR 2, see Tencent/WeKnora#1440), with strict feature-gate: this PR ships only inert constants, schema extensions, and an unreachable normalizer case. No path activates an OpenSearch VectorStore yet — creation continues to fail with "not a valid engine type" until the activation switch lands in Phase 3 PR 3. Changes: - OpenSearchRetrieverEngineType constant ("opensearch") in internal/types/retriever.go. Not added to validEngineTypes / GetVectorStoreTypes / retrieverEngineMapping yet (gated). - ConnectionConfig.InsecureSkipVerify (bool, default false) in internal/types/vectorstore.go, placed inside the // Common section because it is a cross-driver TLS option. Distinct from the Qdrant-specific UseTLS, which enables TLS on gRPC — InsecureSkipVerify only controls verification of an already-TLS HTTPS connection. AES-GCM Value/Scan round-trips the field as plaintext (it travels alongside the encrypted Password / APIKey but is not sensitive itself). - VectorStoreFieldInfo gains four optional fields: Immutable (bool), Min/Max (*float64), Enum ([]string). All omitempty so existing UI schema entries serialize identically. The fields will drive the UI in Phase 3 PR 3 (read-only on Edit + range/enum constraints). - Six new AuditAction constants under vector_store.* and opensearch.* namespaces. Definitions only — emission lands in Phase 3 PR 3. - EngineAwareNormalizer.Normalize is restructured to group engines by the effective score range observed by the normalizer (not the theoretical raw cosine range): Range [-1, 1] (raw cosine, mapped via (score + 1) / 2): - Milvus (COSINE metric mode). Milvus docs explicitly state the COSINE metric range is [-1, 1]. Range [0, 1] (passthrough — already on the target scale): - Elasticsearch v8 / ElasticFaiss. The driver issues a cosineSimilarity(...) script_score script, and Lucene rejects negative final scores ("Final relevance scores from the script_score query cannot be negative" — ES docs); the effective range observed by the normalizer is therefore [0, 1] for IR-normalized embeddings. ES was previously grouped with Milvus and over-corrected via (score + 1) / 2, which inflated every ES result by 50% in mixed-engine RRF fusion. - OpenSearch. The k-NN plugin's SpaceType.COSINESIMIL.scoreTranslation maps the underlying Lucene/Faiss distance (1 - cosine) to (1 + cosine) / 2 ∈ [0, 1] before the score reaches us. Source: github.com/opensearch-project/k-NN at src/main/java/org/opensearch/knn/index/SpaceType.java (COSINESIMIL enum, scoreTranslation method). - Weaviate. The driver requests `certainty`, defined by Weaviate as (2 - distance) / 2 = (1 + cosine) / 2, intrinsically [0, 1]. - Postgres pgvector / SQLite sqlite-vec / Qdrant / TencentVectorDB / Doris. The driver computes (1 - cosine_distance) or normalized inner_product, whose theoretical range is [-1, 1]. The IR-normalized positive-component unit vectors WeKnora targets (BGE / OpenAI text-embedding-3 / Cohere / sentence-transformers) keep the observed range in [0, 1]; negative-cosine embedding models would silently clamp to 0 downstream — explicitly documented as the IR-normalization caveat in the struct godoc. Dead enum references (ElasticFaiss, Infinity) are flagged in the godoc with a pointer to internal/types/vectorstore.go's existing "legacy/experimental, no standalone deployable instance" annotation. Their case labels are kept for switch exhaustiveness. Test coverage: - OpenSearch constant wire value + collision check against existing 10 engines + gated invariant (NOT in validEngineTypes). - ConnectionConfig.InsecureSkipVerify backward-compat (missing JSON field deserializes as false) + round-trip + AES-GCM coexistence with encrypted Password / APIKey. - VectorStoreFieldInfo omitempty preservation + new-field serialization + *float64 pointer distinction (min=0 vs nil). - AuditAction dot-namespace convention enforcement, prefix invariants for vector_store.* and opensearch.*, no wire-string collisions, exact wire-value pins for the six new constants. - EngineAwareNormalizer: - CosineRange retains its (score + 1) / 2 coverage but now only for Milvus. - UnitInterval now covers the full passthrough group (ES / ElasticFaiss / OpenSearch / Weaviate / Postgres / SQLite / Qdrant / Infinity / TencentVectorDB / Doris). - New TestEngineAwareNormalizer_ElasticsearchCosinePassthrough is an explicit regression guard for the score-range correction: cos=0.5 maps to 0.5 (not (0.5 + 1) / 2 = 0.75 as an earlier draft assumed). - OpenSearch passthrough across (0 / 0.5 / 0.75 / 1) + engine drift (1.0001) + defensive negative + ±Inf / NaN edges + keyword passthrough + nil-ctx safety. Backward compatibility is preserved at every layer: - All new struct fields are omitempty / pointer-tagged so existing rows and existing wire formats remain unchanged. - Normalizer's new OpenSearch case is unreachable until the driver lands. The ES regrouping changes the post-normalization value for every ES vector search result (a correctness fix, not a feature) — ES vector retrieval is currently the only production path affected. - AuditAction constants emit no audit_log rows in this PR. - engine_type=opensearch VectorStore creation still rejected (gated). |
||
|
|
8b7d00b260 |
feat: expose KB ↔ vector store binding in list, editor, and detail UI
Backend ------- ListKnowledgeBases now enriches each row with the resolved vector_store metadata (name / source / engine_type / status) via a new buildKBListResponse helper. Store views are batch-resolved once per request through BatchResolveStoreView so an N-KB list costs one vector-store service call rather than N — closing the N+1 limitation flagged in #1372's known-limitations section. Cross-tenant shared KBs continue to render via SharedStoreDisplay so the owning tenant's store inventory cannot be correlated across rows; the underlying vector_store_id UUID is stripped from those responses. Resolver failures degrade gracefully: bound KBs render as unavailable instead of breaking the list. Test coverage pins the env / bound / shared distinction, the batch-call-count invariant, and the graceful-failure path. Frontend -------- KB editor modal gains a new "Vector Store" section. Create mode shows a dropdown that combines the system default (env store) and the tenant's configured user stores, fetched once at mount via the existing listVectorStores API. Edit mode shows the bound store read-only via a new VectorStoreBadge component with an explicit immutability hint — matching the backend's `<-:create` GORM tag and the service-layer UpdateKnowledgeBaseRequest DTO that already omit the field. KB list cards surface a small engine-type badge for own-tenant bound KBs, and a warning badge when the bound store is unavailable. Env-bound and shared KBs render no badge (visual noise control). KB detail header shows the bound store via the same VectorStoreBadge component; shared KBs fall through to the badge's internal "shared" branch with no name / engine / id rendered. The KB editor's create-time error handler translates the typed ErrVectorStoreBindingInvalid (2200) and ErrVectorStoreUnavailable (2201) into localized messages and jumps the user back to the VectorStore section so they can pick a different store or fall back to the system default. The KB row type gains five optional fields (vector_store_id / name / engine_type / source / status). i18n: 18 new keys added to en-US, ko-KR, zh-CN; ru-RU receives English placeholders pending translation (consistent with prior PRs in this locale). Part of #993 (Phase 2: Per-KB VectorStore Binding). Phase 2 roadmap item: PR 5 (KB binding UI + list-response enrichment). Depends on #994, #1310, #1372, #1386 (all backend in the Phase 2 series). |
||
|
|
c0e4a1d2f1 |
fix(summary): preserve image caption/OCR text in document summaries
Documents whose only payload is an embedded image (e.g. a docx with a single picture) intermittently produced the refusal line "No textual content was extractable from this document." even though the vision model had successfully extracted a caption. Three coordinated fixes: - Clarify the summary prompt that text inside `<image_caption>` and `<image_ocr>` is first-class extracted content, not an image reference, so the model only triggers the empty-content branch when the body is genuinely textless. - For image-dominated documents (real text < 200 runes after stripping image markup) include OCR alongside captions so screenshots and scanned figures contribute their actual content; text-heavy documents continue to use caption-only enrichment to avoid OCR noise from incidental figures. - Add `EnrichContentCaptionAndOCR` which embeds caption + OCR text inline next to the original Markdown image link, deliberately omitting the `<image url=...>` and `<image_original>` wrapper blocks. Those wrappers carry only opaque export hashes that consume tokens and have been observed to retrigger the LLM's "image reference with no extracted text" heuristic. |
||
|
|
d3832edea4 | fix(repository): qualify tenant_id with table name to resolve ambiguous column reference | ||
|
|
0a9a920460 | perf(repository): exclude embedding field from Elasticsearch search response to reduce size | ||
|
|
e1ce4270d0 | fix(repository): correct query syntax to get correctly built-in models | ||
|
|
bb95488ac8 | fix(knowledge): complete indexed documents immediately | ||
|
|
fd3d9f547c |
feat(search): fan-out KB retrieval across bound vector stores
Multi-KB hybrid search now groups KBs by their bound VectorStore (partition key (storeID, owner_tenant_id)), retrieves in parallel via errgroup with a SetLimit(4) cap and a per-group timeout (MULTI_STORE_RETRIEVE_TIMEOUT_SEC, default 30s), and merges results. When the collected results span more than one engine type, an EngineAwareNormalizer rescales vector scores to [0, 1]; keyword (BM25) scores pass through to the existing RRF fusion. Single-group calls take the fast path with zero fan-out overhead, preserving today's behavior for deployments where every KB has vector_store_id = NULL. Embedding-model consistency is now enforced explicitly via ResolveEmbeddingModelKeys. Multi-KB searches across KBs whose resolved model identities differ return BadRequest instead of silently producing incomparable scores. Cross-tenant Organization-shared KBs are preserved by partitioning on KB.TenantID so the factory's ownership lookup runs against the source tenant. Foreign-tenant KB UUIDs injected via the request body are rejected via kbShareService.HasTenantKBPermission (Plan 3 of #1303, 3-D capped) before any retrieval; rejected scopes surface as 404 to avoid leaking foreign KB existence. Service-layer typed AppErrors (ErrVectorStoreBindingInvalid 2200 / ErrVectorStoreUnavailable 2201) are mapped from PR2 sentinel hierarchy and preserved end-to-end: the iterative FAQ path returns them rather than swallowing, and the HybridSearch handler routes typed AppErrors to the client unchanged instead of downgrading to 500. Part of #993 (Phase 2: Per-KB VectorStore Binding). Phase 2 roadmap item: PR 4 (Multi-store fan-out search). Depends on #994, #1310, #1372. |
||
|
|
e62c0563aa |
doris: add configurable compatibility modes and guard mode switches
Problem:
The hard-coded Doris vector function implementation (cosine_distance_approximate with
UNIQUE KEY ANN tables) fails on SelectDB 4.0.2-rc01 and other Doris builds lacking that
specific function support. Users had no way to adapt without code changes.
Root cause:
There is assumption all Doris deployments support the same vector function API, but different
builds (Doris OSS, SelectDB, Doris Cloud) ship with different function variants and table
key constraints. No capability detection or user configuration existed.
Solution:
Implement DORIS_COMPAT_MODE environment variable with three modes:
* auto (default/recommended): probe Doris server on first use to detect available vector
functions; prefer inner_product_duplicate (modern Doris 4.0+), fall back to legacy
(older builds lacking inner_product_approximate)
* legacy: hard-set to cosine_distance_approximate + UNIQUE KEY (for older Doris/SelectDB
builds without inner_product_approximate support)
* inner_product_duplicate: hard-set to inner_product_approximate + DUPLICATE KEY
(for modern Doris 4.0+ and current SelectDB with normalized embeddings)
Implementation details:
- add compat.go with one-time mode resolution (sync.Once) and capability probing
- inspect existing weknora_embeddings_* table DDL via SHOW CREATE TABLE to detect and
enforce schema compatibility; prevents silent mismatches
- fail fast with clear error message when configured mode does not match existing tables,
with explicit remediation steps (recreate tables or change env var)
- branch all query paths (inner_product_approximate vs cosine_distance_approximate),
DDL generation (DUPLICATE KEY vs UNIQUE KEY), write paths (embed normalization),
and chunk updates (Stream Load vs read-modify-write) by resolved compat mode
- add comprehensive repository tests for mode selection, auto-detection, and mismatch
scenarios; all tests pass
- expose DORIS_COMPAT_MODE in docker-compose.yml with auto as default
- document in .env.example with clear mode decision guidance
- log all mode decisions (requested, detected, probed, final) at INFO/WARN level
Key guarantee:
⚠️ DORIS_COMPAT_MODE is NOT interchangeable after embedding tables are created.
App will reject mode switches that conflict with existing table layout, preventing
silent data mismatches and query failures.
|
||
|
|
7bb38f81b6 |
feat(session): persist last request state for UI restoration
Sessions now record the input-bar state used for the most recent QA request (agent, model, KB scope, web search). The chat UI hydrates those settings on session reopen so users see the same configuration they used last time, instead of the global default. The state is stored in the existing sessions.agent_config JSONB column to avoid a new migration. Frontend snapshots the user's global defaults on session enter and restores them on session leave, so opening an old session does not pollute new-chat defaults. |
||
|
|
0cdb922d84 |
feat(tenant): remember user's last-active workspace across logins
Until now a fresh login (new device, expired refresh token, cleared
browser) always dropped the user into their home tenant, even if they
spend most of their time in a peer workspace. The session-local
X-Tenant-ID override in localStorage gave a "same browser sticky"
effect, but never crossed devices or new sessions.
This adds a small server-side preference, `users.preferences.
last_active_tenant_id`, persisted in the existing jsonb column (no
new migration), and threads it through:
* Backend
* `UserPreferences` gains `LastActiveTenantID *uint64` with sentinel
semantics (`*0` from the PATCH endpoint = clear preference).
* `resolveLoginTenantID` validates the stored id (tenant still
exists + active membership, or CanAccessAllTenants) and falls
back to home on any failure, best-effort clearing the stale
preference so subsequent logins don't pay for it again.
* `Login` and `LoginWithOIDC` resolve once and use the result for
both the JWT `tenant_id` claim and the returned `active_tenant`,
keeping the two in sync. `RefreshToken` rides through
`GenerateTokens` so refresh rotations also land the user back in
their preferred tenant instead of bouncing to home.
* `UpdateUserPreferences` learns to merge the new key.
* `PUT /auth/me/preferences` accepts the new field.
* Frontend
* `Login.vue` now writes the user's HOME tenant id into
`user.tenant_id` (matching the field's documented semantics) and
expresses any active-vs-home divergence via `setSelectedTenant`,
so `useHomeTenant` and the "current"/"home" badges stay correct
after the backend honours a remembered preference. `App.vue`'s
OIDC sync does the same reconciliation.
* `TenantSelector`, `UserMenu` and the post-tenant-create handlers
fire `persistLastActiveTenantPreference` after every successful
user-initiated switch (switching to home sends `0` to clear).
The call is raced against the existing reload-grace window so
most writes finish before the page tears down; lost writes are
recoverable on the next switch.
No new UI. Users will simply notice that, after re-logging in on
another device, they land back in the workspace they were last
using rather than always in their home tenant.
Note: `make docs` is unrelated-broken on `main` (audit_log.go
references `errors.AppError` which swag can't resolve), so the
Swagger artifacts under docs/ are intentionally not regenerated
in this PR. The handler code is the source of truth.
|
||
|
|
b3dbf86bf5 | feat(obs): 支持华为云obs存储 | ||
|
|
bbcedbee63 |
feat(tests): enhance vector store tests with fake Elasticsearch server and add user pinning methods to knowledge base repository
Introduced a new helper function to create a fake Elasticsearch server for testing, improving the reliability of connection probes in vector store tests. Additionally, added methods for setting and listing user-specific knowledge base pins in the knowledge base repository, aligning with recent user pinning features. Refs: #1303 |
||
|
|
7528175a74 |
feat(knowledge-base): implement per-user pinning for knowledge bases
Refactored the pinning mechanism for knowledge bases to be user-specific, allowing any user with read access to pin knowledge bases independently. This change includes the introduction of a new `user_kb_pins` table to manage pin states, replacing the previous tenant-wide pinning system. Updated relevant API endpoints and service logic to support this new functionality, ensuring that the UI reflects the user's personal pin status. Additionally, adjusted the knowledge base listing to sort by user-specific pin status, enhancing the user experience by prioritizing pinned items. Refs: #1303 |
||
|
|
68ac7fb973 |
feat(user-preferences): implement user preferences management with backend support
Added functionality for managing user preferences, including the ability to update preferences via a new API endpoint. The preferences are now stored server-side, allowing for synchronization across devices. Updated relevant components to handle user preferences, including the UserMenu and settings views. Enhanced internationalization support for error messages related to preference updates. |
||
|
|
14f62a6e14 |
fix(audit_log): prevent deadlock in Stop() when Start() is never called
Updated the AuditLogRetentionRunner to include a 'started' flag, allowing Stop() to return immediately if Start() was never invoked. This change prevents potential deadlocks during graceful shutdown. Additionally, added tests to ensure that Stop() behaves correctly when called before Start() and when the service is nil. Refs: #1303 |
||
|
|
884ac44283 |
feat(invitations): implement pending invitations feature with polling and dialog
Added functionality for managing pending invitations, including a polling mechanism to update the invitation count in real-time. Introduced a new dialog component for users to view and respond to their invitations without navigating away from their current context. Updated API endpoints for fetching and managing invitations, and enhanced the user interface with relevant internationalization support. Refs: #1303 |
||
|
|
4b4225a79f |
feat(custom_agent): enhance custom agent service with KB sharing support
Updated the customAgentService to include a new KBShareService interface for handling cross-tenant knowledge base sharing. Implemented logic to group knowledge base IDs by effective tenant, ensuring correct querying of shared KBs. Adjusted the GetSuggestedQuestions method to accommodate these changes, improving the handling of FAQ and document chunks for shared knowledge bases. Refs: #1303 |
||
|
|
8332a0dab6 |
feat(favorites): implement user favorites and recents functionality
Introduced a new API client for managing user favorites, allowing users to list, add, and remove starred resources. Enhanced the sidebar to display favorites and recents, improving user navigation. Updated internationalization files to include relevant labels and tooltips for the new features across multiple languages. Refs: #1303 |
||
|
|
dc8d033b4d |
feat(organization): transition membership model to tenant-level and enhance invite functionality
Updated the organization membership model to operate at the tenant level, aligning with Plan 3. This change introduces new interfaces and modifies existing ones to accommodate tenant-centric operations, including the `InviteMemberRequest` and `TenantInviteCandidate`. The search functionality for inviting members has been updated to reflect this shift, allowing for tenant-based searches instead of user-based. Additionally, internationalization files have been updated to ensure consistent messaging across languages regarding tenant membership and roles. Refs: #1303 |
||
|
|
ff1f03ce0d |
feat(rbac): daily retention sweep for audit_logs (default 90 days)
The audit_logs table grows monotonically until something purges it. PR 6 explicitly listed retention as out of scope for v1; this picks that up. Adds an `audit.retention_days` config (default 90, 0 disables) and a small background goroutine `AuditLogRetentionRunner` that fires once ~10 minutes after boot and then every 24h, calling `AuditLogService.Purge` which DELETEs rows older than the cutoff in a single statement. The dependency surface is intentionally minimal — no robfig/cron, no asynq, no migration — because retention has no wall-clock alignment requirement. Wiring is the same shape as the existing data source scheduler: container `Provide(NewAuditLogRetentionRunner)`, an `Invoke` that calls `Start`, and a `ResourceCleaner.RegisterWithName` for graceful shutdown. Defaults preserve operator intent: - `audit:` section omitted from YAML -> retention_days=90. - explicit `audit.retention_days: 0` in YAML -> purge disabled. - `WEKNORA_AUDIT_RETENTION_DAYS=N` env override (incl. N=0). - `retention_days < 0` is rejected by ValidateConfig. Tests cover the service Purge contract (no-op when disabled, cutoff math, error propagation) and the runner lifecycle (Start no-op when disabled, idempotent Start/Stop, ticker cadence, runOnce swallows errors). 10 new tests, all green. docs/rbac.md updated with the new YAML / env / behaviour. Refs: #1303 |
||
|
|
a16fe83034 |
fix(rbac): pin organization owner tenant in DB instead of deriving at runtime
Plan 3's isOwnerTenant resolved "is this tenant the owner tenant?" by looking up the owner user's CURRENT tenant via userRepo. That breaks silently the moment the owner user is moved to another tenant: the formerly-protected owning tenant becomes removable (can be kicked from OTM, role-downgraded), and the new tenant the owner moved to is treated as untouchable even though it isn't even in OTM. Promote owner-tenant to a first-class column on `organizations`: - New migration 000046 adds owner_tenant_id BIGINT NOT NULL with index. Backfill: (1) owner user's current tenant when known, (2) earliest Admin tenant in OTM as fallback for orphans, then (3) RAISE EXCEPTION on anything still NULL — refuse to silently ship a bogus value. SQLite init schema mirrors the column. - CreateOrganization now sets OwnerTenantID at create time so the pinned value is correct for all new orgs. - isOwnerTenant becomes a pure equality check on the persisted column — no userRepo lookup, no error tuple. Fail-closed on a zero column value (legacy data not yet backfilled or unit tests bypassing the migration) so the worst case is "membership table frozen for that org" rather than "owner tenant is removable", matching the prior fail-closed-on-error semantics. - RemoveTenantMember / UpdateTenantMemberRole simplify to a single bool check. |
||
|
|
f6684f226b |
fix(rbac): allow source-tenant Admin+ to manage their cross-tenant shares
Plan 3 of #1303 lifts share ownership to the tenant level, but RemoveShare / UpdateSharePermission still gated on the original sharer user. If that user left the tenant or moved on, the source tenant lost the ability to revoke its own share — only the target org's admin could clean up, which is governance theatre rather than ownership. Extend the authz envelope on KB-share and agent-share mutations: (1) original sharer (unchanged); (2) source-tenant Admin+ — new, encodes "ownership lives in the tenant" by reading callerTenantRole from ctx; (3) target-org admin (unchanged). Factored out as kbShareService.callerCanManageShare so Update and Remove cannot drift apart. |
||
|
|
eda6b1011a |
fix(rbac): close org-tenant gate gaps from Plan 3 review
Three follow-ups from #1350 review: 1. DELETE /organizations/:id/members/:tenant_id was un-gated, so a tenant Viewer could remove their own tenant from an org via the members endpoint and bypass the Admin+ requirement that POST /:id/leave already enforces. Gate it with g.Admin() — symmetric with /leave. 2. isOwnerTenant() returned (false, err) when the owner user lookup failed, and the two call sites only checked the bool. A deleted / unreadable owner user would silently fall through and let admins remove or re-role the would-be owner tenant. Make the helper fail-closed: any lookup error now returns (true, err), and both call sites refuse the destructive op on error. 3. POST /:id/request-upgrade was un-gated. Approval of the request changes the whole tenant's org role, so it is in the same trust tier as join/leave/create. Add g.Admin(). |
||
|
|
928f5abe3f |
feat(rbac): lift org membership from per-user to per-tenant (Plan 3, #1303)
Before this change, the `organization_members` table keyed on user_id — joining an org as a Viewer in tenant T meant only that one user got visibility on shared KBs/agents, while the rest of T (including its Owner) saw nothing. The mismatch surfaced as confusing UX bugs: "my colleague shared this with us, why can't I see it?" This change replaces `organization_members` with `organization_tenant_members`. Membership and Org-RBAC role now key on (org_id, tenant_id); `representative_user_id` stays as a display-only label of the user who first brought the tenant in. Permission resolution gains a 3-D cap so tenant-RBAC stays honest under cross-tenant sharing: effective = min(share.Permission, tenant_org_role, tenant_role_cap) where tenant_role_cap pins tenant Viewers to OrgRoleViewer regardless of the org-level grant. A Viewer in their own tenant cannot edit a shared KB even if the org granted Editor — preserves the "Viewer = read-only everywhere" promise. Migration 000045 backfills `organization_tenant_members` from the old table by collapsing each (org, tenant) group to its max role, then renames the old table to `organization_members_pre_plan3` for safe rollback. Sqlite mirror (Lite mode) is fresh-bootstrap: the init schema replaces the old table directly. Routes: member-management URLs change from `:user_id` to `:tenant_id`. Org create / join / leave now require Admin+ in the caller's tenant — joining an Org affects everyone in T, so the decision can't sit with a Viewer. Frontend: OrganizationSettingsModal + store update to address members by tenant_id. |
||
|
|
1e8b5c8d40 |
refactor(api): drop deprecated tenant ConversationConfig field and KV write path
The Tenant.ConversationConfig field was marked deprecated when
CustomAgent (builtin-quick-answer) replaced the per-tenant defaults
path, but the JSONB column, GET/PUT KV handlers, write helpers and
read fall-throughs all stayed wired. This had two concrete failure
modes:
- Input-field.vue silently PUT-ed per-user model picks back into
/tenants/kv/conversation-config, which requires Admin+ — Viewers
and Contributors got a 403 every time they switched chat models.
- The same write also overwrote the tenant-shared default for every
other user, turning a per-user UX into a per-tenant footgun.
Remove the field, the struct, the validate helper, the PUT case and
the two dead Summary helpers in session/helpers.go. KnowledgeSearch
loses its tenant-conversation fallback (one chunk-tool reader); the
global config + hardcoded defaults still cover it. session_agent_qa
already required agents to declare their own rerank_model_id, so no
behaviour change there.
Frontend follows:
- Input-field.vue: per-user 'last selected chat model' moves to
localStorage, no API call.
- AgentEditorModal: defaults for builtin-quick-answer creation now
come from prompt-templates (system/context/rewrite/fallback) and
retrieval-config (top_k / threshold), the two endpoints that
actually own those settings now.
- AgentSettings.vue: orphan settings page with no router/import
referencing it; deleted.
- api/system: getConversationConfig / updateConversationConfig and
the ConversationConfig interface go away.
Net: 137 insertions, 2761 deletions.
|
||
|
|
766a6c482f |
feat(rbac): per-tenant audit log for RBAC events (PR 6, #1303)
PR 1-5 of the multi-tenant RBAC series enforce the role matrix; what they don't do is keep a durable record of who did what. Today the RBAC middleware logs "[rbac] role insufficient ..." to stderr - perishable, not searchable. Operators cannot answer "who removed Alice from tenant T-foo on Tuesday" or "did we 403 a probing user 200 times last hour" after the fact. This PR adds a generic per-tenant audit log that: - Captures mutating RBAC actions (member add/remove/role-change/leave) durably from the tenant_member service after the repo write succeeds. - Captures enforcement rejections (RequireRole / RequireOwnershipOrRole denies under EnableRBAC=true) via a gin-context-injected hook. - Sliding-window dedup (1 minute) on (tenant, actor, action, path) for denied events so a probing client cannot flood audit_logs. - Default-on - no extra env flag. Schema is generic enough for future KB / agent / datasource events to reuse via new action namespaces without another migration. Schema (migration 000044) mirrors wiki_log_entries' shape: a single audit_logs table with action, target_type/id, request path/method, outcome, and a JSONB details column. Three indexes: (tenant_id, id DESC) for the per-tenant cursor feed, actor_user_id for "what did Alice do", (tenant_id, action) for both the action filter and the dedup lookup. Sqlite mirror added for Lite mode. Wiring is nil-safe: the audit field on tenantMemberService and the gin-context lookup in rbac.go both tolerate a nil service so existing tests construct without a stub and dormant deployments degrade gracefully if the provider is misconfigured. The query API is GET /tenants/:id/audit-log, gated by Admin+ on top of the existing PathTenantMatch (denial histories should not surface to ordinary members). Cursor-paginated by descending id (sidesteps the duplicate-timestamp tie-breaking that BIGSERIAL trivially solves). Frontend: TenantMembers.vue gets a t-tabs wrapper with Members (existing) and Audit log (new) tabs; the audit tab is gated to Admin+ to mirror the server, lazy-loads on first open, paginates via the next_cursor pattern, and renders coloured chips per action/outcome so an operator can scan for anomalies. i18n keys added in all four locales. Tests: 14 new tests across service (6), middleware (4), and handler (4). Service tests pin the dedup window behaviour (writes again after expiry, dedup is per-actor-per-path, degrades on lookup error). Middleware tests pin both reject sites' audit hook and the "dormant mode does not fire" + "nil audit does not panic" invariants. Handler tests pin the cursor envelope and that an empty page returns next_cursor=0. Out of scope: per-action ACL on the audit log, retention/TTL job, KB/agent/datasource event wiring (schema supports it - just no emitters yet), async writes, frontend filter UI. |
||
|
|
d42a41b784 |
feat(rbac): per-KB ownership for knowledge / chunks / wiki pages (#1303)
PR 1–4 of the multi-tenant RBAC series gated KB-level and Agent-level
mutations behind a "creator OR Admin+" check via OwnedKBOrAdmin /
OwnedAgentOrAdmin. The layer underneath — knowledge entries, chunks,
wiki pages — was still gated only by Contributor, which is wider than
intended: any tenant Contributor could edit/delete documents in a
private KB they didn't own. PR 2 / wiki author left explicit follow-up
breadcrumbs at the three call sites in router.go acknowledging this gap.
This PR closes those breadcrumbs by reusing the existing scaffolding:
- KnowledgeService.GetOwningKBCreatorID walks knowledge_id -> kb_id ->
KB.CreatorID, tenant-scoped through GetKnowledgeByID. The chain lives
in the service layer so the handler-side lookups stay pure adapters.
- Three new CreatorLookup closures (KBCreatorLookupFromKnowledgeID,
KBCreatorLookupFromKnowledgeIDParam, KBCreatorLookupFromKBPath) are
added in handler/rbac_lookups.go alongside the existing
KBCreatorLookup / AgentCreatorLookup. Each translates the repository
sentinels into middleware.ErrResourceNotFound and re-checks tenant
scope as defence-in-depth, mirroring KBCreatorLookup exactly.
- router/rbac.go gains three guard factories — OwnedKnowledgeKBOrAdmin,
OwnedChunkKBOrAdmin, OwnedWikiKBOrAdmin — wired through the same
rbacGuards bundle.
- Mutating routes in RegisterKnowledgeRoutes / RegisterChunkRoutes /
RegisterWikiPageRoutes switch from g.Contributor() to the new
ownership guards. Cross-KB batch operations (POST /knowledge/move,
POST /knowledge/batch-delete, PUT /knowledge/tags) stay
Contributor-gated since there's no single owning KB to check
against. KB-scoped uploads (POST /knowledge-bases/:id/knowledge/...)
reuse the existing OwnedKBOrAdmin because the URL :id is the KB id
directly. The chunks.DELETE("/by-id/:id/questions") route addresses
chunks by their own id with no knowledge id in the URL, so it stays
Contributor-gated as a documented carve-out.
No schema changes: documents inside a shared KB are KB-owned content,
not personal uploads — that's how the product is positioned and matches
the original "per-KB-ownership granularity" wording from the PR 2
breadcrumbs. Legacy/built-in KBs with empty CreatorID surface as
tenant-owned (Admin+ only), same rule as built-in agents.
Tests:
- 9 new test cases in handler/rbac_lookups_test.go covering the three
new lookups: NotFoundMapsToSentinel (each repository sentinel),
CrossTenantIsHiddenAsNotFound, OwnerMatchReturnsCreatorID,
MissingTenantContext (fail-closed), and ChunkRouteShape (param-name
regression guard).
- 5 new test cases in service/knowledge_owner_chain_test.go covering
the chain helper: knowledge-not-found, kb-not-found, nil-kb-without-
err, happy path, and legacy KB with empty CreatorID.
Build, vet, and the targeted ./internal/middleware/...
./internal/handler/... ./internal/router/...
./internal/application/service test runs are clean. Pre-existing
TestCreateStore_* / TestOssEnsureBucket_CreateFails failures on main
are unrelated and reproduce on a clean checkout.
|
||
|
|
543e9709be |
fix(rbac): address PR 3 + leave-endpoint review feedback
Security - Reject URL :id when it does not match the caller's active tenant on every /tenants/:id/members/* and /tenants/:id/leave endpoint. Without this an Owner of tenant A could drive operations on tenant B simply by changing the URL, because RequireRole resolves the caller's role against the active tenant from context, not the URL param. The cross-tenant superuser carve-out mirrors middleware/rbac.go (CanAccessAllTenants + EnableCrossTenantAccess required). - AddMember no longer records the synthetic "system-<tenantID>" caller (X-API-Key auth path) as invited_by, matching the same filter KB.CreatorID got in PR 2. - AuthHandler now logs a loud one-shot error when constructed with a nil/incomplete config so a wiring bug that silently disables the invite_only gate is visible at boot. Race fixes - New repo helpers DemoteOwnerAtomically / RemoveOwnerAtomically wrap the count-other-owners + write step in a transaction that holds SELECT … FOR UPDATE locks on every other active Owner row. The service routes Owner demotion / Owner removal through them, closing the TOCTOU window where two concurrent demotes of two different Owners could each observe count=2 and both commit, leaving the tenant ownerless. Performance - ListMembers hydration now batches user lookups via a new GetUsersByIDs interface method (one SELECT … WHERE id IN (…) instead of N round-trips). Failure degrades to "no email/username" rather than dropping rows. Frontend - canManage also fires for cross-tenant superusers (authStore.canAccessAllTenants), mirroring the backend carve-out so the UI controls match what the server actually allows. Tests - Cross-tenant URL guard regression cases for List / Add / Update / Remove / Leave. - Superuser bypass tests (flag on -> service reached, flag off -> 403). - Batched lookup test ensures GetUsersByIDs is called exactly once and GetUserByID is never used as a fallback. - Synthetic caller invited_by filter regression test. - Service-layer atomic last-Owner regression tests for both UpdateRole and RemoveMember. |
||
|
|
8b71def60b |
fix(rbac): address PR 2 review feedback
Middleware - RequireOwnershipOrRole now skips the creator lookup entirely when EnableRBAC is off, eliminating a hidden per-request DB roundtrip in the rollout window. - New ErrResourceNotFound sentinel lets lookups signal "not visible to this tenant"; the middleware passes the request through so handlers can return a real 404 instead of a misleading 403. - Transient lookup failures now surface as 503 (was 403) so monitoring can tell denied-by-policy from cannot-verify-right-now. - Cross-tenant superusers (CanAccessAllTenants) bypass tenant role gates, restoring DELETE /tenants/:id and ResetAPIKey for org operators that resolve to Admin in foreign tenants. - Construction-time warning fires once when cfg is nil/incomplete, so a wiring bug that disables enforcement globally is visible at boot. Route coverage - Add role guards to FAQ, KnowledgeTag, WikiPage, Skill, WebSearch, WebSearchProvider, WeKnoraCloud, Session, Chat, and Message route groups so the matrix actually matches the PR description. Service layer - KB and agent creators are no longer recorded as the synthetic system-<tenantID> user used by the X-API-Key path; resources stay tenant-owned. New types.IsSyntheticUserID helper centralises the prefix check. - KBCreatorLookup and AgentCreatorLookup re-check tenant scope so a same-ID resource in another tenant cannot piggyback through the ownership shortcut. Docs - CustomAgent.RunnableByViewer comment now matches enforcement: it only blocks Viewer-driven agent-mode runs, not plain RAG/Wiki QA. Tests - New cases for the 503/404 paths, lookup-skip behaviour, cross-tenant superuser bypass, the lookup helpers (not-found, cross-tenant, built-in agent, missing tenant context), and IsSyntheticUserID. |
||
|
|
9e3c92ca95 |
feat(rbac): enforce tenant role matrix on routes (PR 2 of 3, #1303)
Build on PR 1's tenant_members schema by enforcing the role matrix on
every authenticated route and at agent run time. Enforcement is gated
behind cfg.Tenant.EnableRBAC so the change ships dormant — the same
code paths log the would-be rejection during the rollout window and
start blocking once operators flip the flag.
Middleware (internal/middleware/rbac.go):
- RequireRole(min, cfg): role-only gate using TenantRole.HasPermission.
- RequireOwnershipOrRole(min, lookup, cfg): allows the original creator
through even when their role is below min, otherwise falls back to
the role check. Lookup errors fail closed; an empty creator_id is
treated as tenant-owned and requires the role.
- Both honour cfg.Tenant.EnableRBAC for the rollout window.
Creator lookups (internal/handler/rbac_lookups.go):
- KBCreatorLookup resolves :id -> KnowledgeBase.CreatorID.
- AgentCreatorLookup resolves :id -> CustomAgent.CreatedBy; built-in
agents (IsBuiltin=true) return ("", nil) so they remain Admin+ only.
- Compile-time guards keep the methods aligned with CreatorLookup.
Route guards (internal/router/rbac.go + router.go):
- rbacGuards bundles the matrix into Viewer/Contributor/Admin/Owner
helpers plus OwnedKBOrAdmin / OwnedAgentOrAdmin. Each Register*
function takes *rbacGuards so route lines remain readable.
- KB / Agent / Knowledge / Chunk / Tenant / Model / MCPService /
VectorStore / DataSource / IMChannel routes annotated per the PR 1
design doc matrix. Tenant-wide infra (models, vector stores, MCP,
data sources, IM channels) reads as Viewer+ and writes as Admin+;
per-resource mutations on KB / Agent route through the
ownership-or-Admin guard.
Resource ownership (services):
- CreateKnowledgeBase / CopyKnowledgeBase populate CreatorID from the
authenticated user, so the OwnedKBOrAdmin guard has a creator to
match.
- CreateAgent / CopyAgent populate CreatedBy similarly. Built-in
agents stay tenant-owned.
Agent run-time gate (internal/handler/session/qa.go):
- AgentQA refuses to launch the agent engine for Viewers when the
target agent has RunnableByViewer=false. Same EnableRBAC gating as
the middleware so it's a no-op until enforcement is turned on.
Tests:
- 12 unit tests cover RequireRole / RequireOwnershipOrRole, including
the fail-open and fail-closed branches and the lookup-error path.
|
||
|
|
af23483433 |
fix(auth): honour JWT tenant_id claim in middleware
POST /auth/switch-tenant mints an access token whose `tenant_id` claim
points to the target tenant, but the middleware previously discarded
that claim entirely:
user, err := userService.ValidateToken(ctx, token) // returned only *User
targetTenantID := user.TenantID // always the home tenant
The session therefore stayed scoped to the user's home tenant despite
the supposedly fresh token, making the new endpoint a no-op once
the request hit any tenant-scoped query.
This change widens the contract:
- UserService.ValidateToken now returns (user, activeTenantID, error)
where activeTenantID is the JWT's `tenant_id` claim, falling back to
user.TenantID when the claim is absent (backward compatibility with
pre-RBAC tokens still in the wild).
- middleware/auth.go uses activeTenantID as the default target before
applying any X-Tenant-ID header override, and recomputes
crossTenantSwitch from `activeTenantID != user.TenantID` so the
resolveTenantRole branches stay correct for switch-tenant sessions.
- The non-middleware /auth/validate handler in handler/auth.go absorbs
the new return value (it only cares about user identity).
Tests:
- internal/application/service/user_jwt_test.go pins the JWT->tenant-id
projection (`tenantIDFromClaims`) — claim present beats fallback,
zero/negative/string claims fall back, int64/uint64 are accepted for
test-built tokens.
Required for PR 2 multi-tenant integration tests, where a single user
needs to switch between Owner-in-A and Viewer-in-B sessions and observe
distinct authority on each side.
|
||
|
|
f3d1210577 |
refactor(rbac): address PR 1 review feedback
Tighten correctness, security and observability of the tenant RBAC foundation introduced in 3c4cdd70 in response to internal review. Bugfixes - custom_agents.runnable_by_viewer: drop the GORM `default:true` tag. GORM treats a plain bool's zero value as "unspecified" and silently rewrites it to the column default on insert, which would prevent admins from ever creating an agent with viewer-runtime disabled. The column-level DEFAULT TRUE on the database side still backstops rows inserted by raw SQL / migrations. - TenantMemberRepository.HasAnyMembers: replace `Limit(1).Count(&n)` with `Take(&probe)`. GORM's Count ignores Limit and scans the whole table, which matters because this runs on the auth middleware's hot path for users without a cached membership. - service/user.LoginWithOIDC: include User, Tenant and Memberships in the OIDCCallbackResponse so the OIDC flow returns the same shape as password login (previously User and friends were left nil). Security - middleware/auth.resolveTenantRole: restrict orphan-tenant auto-promotion to the caller's home tenant. Cross-tenant superuser visits and JWTs minted for foreign tenants no longer trigger silent Owner grants. Two regression tests pin the new behaviour. - service/user.synthFallbackMembership: default the synthesised role to Viewer instead of Owner. The login response only feeds UI; the backend re-derives the real role from tenant_members on every request, so showing a Viewer UI is preferable to a misleading Owner UI that would surface admin controls the backend will then 403. - migrations/versioned/000043_tenant_rbac.down.sql: gate the destructive drop behind a `weknora.allow_destructive_migration` GUC so an accidental `migrate down` cannot wipe role data in production. - frontend/src/stores/auth.ts: comment currentTenantRole as rendering-only so future PRs cannot mistake the localStorage-backed value for an authorisation signal. API contract - Memberships in LoginResponse and OIDCCallbackResponse now serialises without omitempty, and buildMembershipsForUser / synthFallbackMembership always return a non-nil slice — frontend can rely on JSON `[]`. - handler/auth.GetCurrentUser includes memberships so /auth/me refreshes recover currentTenantRole after a page reload. Performance - Add TenantService.GetTenantsByIDs (with the matching repo method) and use it inside buildMembershipsForUser to batch tenant-name lookups instead of issuing one query per membership. Observability - Replace the remaining `log.Printf` calls in middleware/auth with contextual logger.Warnf / logger.Infof so role resolution events ride the standard request-id / Langfuse trace plumbing. The successful auto-promote line is tagged `[audit]` for log mining. Tests - internal/application/service/tenant_member_test.go: hand-rolled fakeTenantMemberRepo plus 11 cases covering AddMember validation, EnsureOwner idempotence, "cannot demote/remove the last Owner", same-role no-op, ErrInvalidTenantRole / ErrMembershipNotFound, and the TenantRole.HasPermission lattice. - internal/middleware/auth_role_test.go: fakeMemberService plus 8 cases pinning every branch of resolveTenantRole, including the two H1 regressions (cross-tenant superuser must not auto-promote; orphan auto-promote requires the home tenant). |
||
|
|
7f7a48880f |
feat(rbac): add tenant_members schema and auth wiring (PR 1 of 3, #1303)
Lay the foundation for tenant-level RBAC without enforcing it yet. Schema, types, middleware lookup, and login-flow shape changes ship now; RequireRole/route annotations and member-management endpoints follow in PR 2 and PR 3. Highlights: - New tenant_members table (Postgres + SQLite) with (user_id, tenant_id) unique key and (owner|admin|contributor|viewer) role values. Migration 000043 backfills one row per existing user, marking the earliest-created active user in each tenant as Owner and the rest as Contributor. - KnowledgeBase.creator_id and CustomAgent.runnable_by_viewer added so PR 2 can wire ownership-based and Viewer-runtime checks. - Auth middleware loads the active TenantMember row, sets a new TenantRoleContextKey, and falls back via three escape hatches: cross-tenant superusers (CanAccessAllTenants) get Admin in the target tenant; orphan tenants (zero members) auto-promote the first authenticating human to Owner; and when tenant.enable_rbac is false (the v1 default) unmatched lookups fail open with Admin to preserve current behaviour. - Register / OIDC provisioning now insert an Owner membership for the registrant via TenantMemberService.EnsureOwner. - LoginResponse renamed tenant -> active_tenant and gained a memberships array. POST /auth/switch-tenant issues a fresh token pair scoped to a target membership; GET /auth/config exposes auth.registration_mode for the frontend register-tab gating. - New config keys: auth.registration_mode (self_serve | invite_only, defaults to self_serve) and tenant.enable_rbac (defaults to false). Both have WEKNORA_* env overrides. - Frontend store persists memberships and exposes currentTenantRole; Login.vue calls /auth/config and hides the Register tab in invite_only mode. Go SDK types updated to match the new LoginResponse shape. |
||
|
|
3214e3d952 | fix(agent): exclude processing docs from prompt | ||
|
|
0e8de6192c |
feat(knowledge-base): validate vector store bindings on create, copy, and delete
Wires KnowledgeBase.VectorStoreID and the ownership-aware retrieve factory into the user-facing knowledge-base lifecycle: - POST /knowledge-bases validates the requested vector_store_id against the caller's tenant scope and the engine registry. New error codes ErrVectorStoreBindingInvalid (2200) and ErrVectorStoreUnavailable (2201) distinguish the typed branches without echoing UUIDs to the client. - GET / POST / PUT / PUT-pin responses embed the bound store's display metadata (name, source, engine_type, status) without exposing any connection credentials. Cross-tenant shared KBs receive a suppressed payload (vector_store_id stripped, source="shared") so operator-chosen store names cannot be enumerated across tenants. - POST /knowledge-bases/copy synchronously rejects clones whose target has a different embedding model or vector store, before the async clone task is enqueued. The async clone worker re-applies the same checks for defense in depth. - DELETE /vector-stores/:id refuses to remove a store with bound KBs, inside a transaction that row-locks the store on PostgreSQL and serializes via WAL on SQLite. unregister-from-registry is wrapped in defer/recover so a panic surfaces as a structured warning instead of silently leaking a stale engine. - vector_store_id is immutable after creation. The GORM <-:create tag blocks every ORM update path; the service-layer DTO omits the field entirely; a reflection-based regression test catches any future maintainer who adds it back to either layer. - Empty-string vector_store_id is normalized to nil at both the create path and inside SharesStoreWith, so rows persisted by callers that did not run Normalize first cannot trip false same-store comparisons. Part of #993. Depends on #994 and #1310. |
||
|
|
b3004f73d4 |
test(repo): add wiki_config + indexing_strategy to sqlite test DDL
The in-memory SQLite DDL in `knowledgebase_sqlite_test.go` mirrors the
`knowledge_bases` columns the tests need to insert against. It missed two
columns that the `KnowledgeBase` struct has been carrying for a while:
- `wiki_config` (gorm:"column:wiki_config;type:json")
- `indexing_strategy` (gorm:"column:indexing_strategy;type:json")
Without those columns the `INSERT INTO knowledge_bases (...)` statement
GORM emits fails with:
table knowledge_bases has no column named wiki_config
…which cascades to every TestKnowledgeBase_VectorStoreID_* test
failing on a fresh `go test ./internal/application/repository/`.
Reproduce on main (before this commit):
go test ./internal/application/repository/ -count=1
...
--- FAIL: TestKnowledgeBase_VectorStoreID_Save_Immutable (0.00s)
knowledgebase_sqlite_test.go:91:
Received unexpected error:
table knowledge_bases has no column named wiki_config
FAIL github.com/Tencent/WeKnora/internal/application/repository
After this commit:
ok github.com/Tencent/WeKnora/internal/application/repository
The fix is two `TEXT NULL` column additions to the inline DDL. No
production code, no migration, no schema change — strictly aligning the
test scaffold with the model fields that already exist.
|
||
|
|
7643dd457e |
refactor(credentials): introduce /credentials subresource pattern
Replace the legacy "redacted placeholder + Clear* boolean" pattern with
dedicated per-resource credential subresources across MCP services,
Models, Web search providers, and Data sources.
Why
---
The previous design had three problems:
1. Main PUT body carried secret fields. The frontend echoed back a
redacted "***" placeholder, and a fragile MergeUpdate / IsRedactedOrEmpty
defense in the service layer tried to detect "user did not change this"
vs "user wants to clear this". A regression in that defense (or a new
frontend forgetting it) silently overwrites the stored secret with the
placeholder.
2. The "remove this credential" UX was a red checkbox under a pre-filled
password input. Three intents (preserve / replace / clear) collapsed
onto one field, and credential changes were bundled with unrelated
config edits in the same submit. Users wiped working keys by mistake.
3. Secret presence was inferred from "did the response come back with a
'***' placeholder", which couples the contract to a magic string.
Design
------
Each of the four resources now exposes:
PUT /{resource}/{id}/credentials # write one or more fields
DELETE /{resource}/{id}/credentials/{field} # clear a single field
"Is this configured?" metadata travels on the main resource response as
a typed map (dto.MCPServiceResponse.Credentials etc.) — no separate GET
endpoint. The frontend reads the configured boolean from the main GET
and never sees secret values at all.
Main PUT endpoints now ignore any secret fields in the request body and
log a deprecation warning if they appear, so legacy clients fail loudly
rather than overwriting silently.
Frontend
--------
- New reusable <CredentialResource> component renders a three-state card
(unconfigured / configured / editing) and drives the dedicated
endpoints. Used by MCP, Model, Web search; DataSource has a bespoke
card with the same behaviour because its credentials are a single
atomic per-connector map.
- Cancel from edit mode now restores state synchronously from the meta
prop. The previous async refresh() was a no-op while state was still
'editing', leaving the input frozen open.
- Remove is single-click + toast. The danger-themed button is the
deterrent; a modal confirm adds friction without adding safety (the
plaintext is irrecoverable client-side either way — recovery means
re-typing).
DTOs (internal/handler/dto/) are deliberately separate from the GORM
models so "no secret in response" is a compile-time invariant: a future
contributor cannot leak a secret without explicitly adding the field to
the DTO, which is review-able in a single diff.
Storage is unchanged — credentials still live in the existing jsonb /
parameters columns. No schema migration.
Cleanup
-------
- types.MCPService / types.Model / types.WebSearchProviderEntity /
types.DataSourceConfig: drop ClearAPIKey / ClearToken / ClearAppSecret
/ ClearCredentials boolean fields, MergeUpdate(), RedactSensitiveData().
- utils/types/secret.go: drop PreserveIfRedacted / IsRedactedOrEmpty.
RedactedSecretPlaceholder constant is retained because VectorStore
still uses the old pattern and is out of scope here.
- Frontend hasExistingApiKey / clearApiKey / convertToLegacyFormat
redaction handling removed; i18n keys renamed secret -> credential.
|
||
|
|
d869cc1ff6 |
fix: redact sensitive fields and prevent credential loss on edit
Closes #988 Unify the "write-only secrets" pattern across MCP services, Models, WebSearch providers, and DataSources. Fix two related defects: 1. GET responses leaked plaintext credentials — GetMCPServiceByID while List masked, all Model / WebSearchProvider / DataSource GET responses unconditionally. 2. When the frontend echoed a masked placeholder back on save, the update path overwrote the stored secret with the mask — silently wiping the real credential. MCP surfaced this most visibly: ListMCPServices masked Token to "first4****last4" and McpServiceDialog round-tripped that masked string back into the DB. - internal/types/secret.go introduces the shared sentinel RedactedSecretPlaceholder = "***" and two helpers: - IsRedactedOrEmpty(s): true for "" or the sentinel - PreserveIfRedacted(incoming, existing): merge predicate - Each resource adds a RedactSensitiveData (mutates-in-place) or Redacted (returns-a-copy) method and a MergeUpdate contract: empty or placeholder preserves, any other value replaces, and a write-only Clear* bool flag (gorm:"-", json omitempty) wipes with precedence over preserve / replace. Clear* never persists. - Every GET / List / Create / Update response path now redacts. VectorStore's pre-existing "***" literal is refactored onto the shared constant for consistency. All four dialogs — McpServiceDialog, ModelEditorDialog, WebSearchSettings, DataSourceEditorDialog — never pre-fill secret inputs. The stored-state signal is collapsed into the input placeholder: "•••••••• (Enter new value to replace)" when the server reports a stored value, falling back to the connector-specific placeholder otherwise. The input itself carries the "something is there" cue, matching how password dots render. An opt-in "Remove this credential" checkbox appears in edit mode when a value is stored; checking it disables the input and triggers a danger-themed confirmation dialog before submit. DataSource rolls the clear up to a connector-level "Remove all credentials" flag since its credentials are per-connector atomic bundles (e.g. Feishu app_id + app_secret). i18n updates across en-US, zh-CN, ko-KR, ru-RU: secret.storedPlaceholder / clearHint / confirmClearTitle / confirmClearBody. - MCP update handler's manual map-to-struct projection was silently dropping clear_api_key / clear_token / custom_headers on partial updates. Pull them off the map explicitly. - MCP configChanged detection now snapshots URL / StdioConfig / TransportType before the merge so connection recycling still triggers on real transport changes (not just whenever AuthConfig is present). - MCP CustomHeaders: nil preserves existing, non-nil replaces. Was always overwriting, silently wiping headers on any auth-bearing update. - GetMCPServiceByID now redacts (was plaintext while List masked). - WebSearchProvider update preserves Name / Description when empty instead of clobbering them. - WebSearchProvider TestProviderRaw rejects the "***" placeholder up front with an actionable message rather than forwarding it to the upstream provider API as a literal key. - Model builtin hideSensitiveInfo now preserves non-secret metadata (SupportsVision, Provider, ExtraConfig, InterfaceType, ParameterSize, EmbeddingParameters) that were previously zeroed out for builtin model cards. - Replace local equalStringSlices / equalStringMaps helpers with stdlib slices.Equal / maps.Equal (Go 1.21+). - internal/types/{secret,mcp,model,web_search_provider,datasource}_test.go: table-driven coverage of merge, redact, clear, and write-only flag invariants for every resource. - internal/application/service/mcp_service_test.go: full service layer with a fake repo; covers ListMCPServices / GetMCPServiceByID redaction, UpdateMCPService partial-update honoring of Clear*, CustomHeaders preserve-on-nil / replace-on-non-nil, and clear-flag precedence over submitted values. Additive only. No changes to request or response JSON shape beyond the write-only Clear* flags (omitted by default via omitempty). No DB schema changes. No SDK struct field removals. Existing clients that don't know about the new flags continue to function unchanged. - DataSource.Config jsonb at-rest encryption. Pre-existing residual risk; tracked as a follow-up. - Renaming VectorStore.MaskSensitiveFields to match the new RedactSensitiveData / Redacted naming convention. Left alone to minimize diff; a follow-up rename PR can unify. |
||
|
|
06feee0fee | feat: modify the skills registration directory | ||
|
|
8f4e5a459f | fix(session): scope session access by user |