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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ jobs:
cmd /c mklink /D "$localData" "$tempData" | Out-Null
Write-Host "rustc: $(rustc --version)"
Write-Host "cargo: $(cargo --version)"
- run: cargo test --all-targets -- --test-threads=4 --nocapture
- run: cargo test --lib --tests --bins --examples -- --test-threads=4 --nocapture

fmt:
name: Rustfmt
Expand Down
14 changes: 14 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,13 @@ Skill Runtime 全生命周期已落地(含依赖管理 Schema v15),Schema
- Schema 迁移前自动生成 `backup-YYYYMMDD-HHMMSS.db` 快照
- Registry 支持 `export`/`import` 用于用户自主备份

## 许可证策略

- **主许可证**: AGPL-3.0-or-later (`LICENSE`)
- **商业授权**: 双许可模式,闭源/专有 SaaS 使用需联系作者 (`LICENSE-COMMERCIAL.md`)
- **Cargo.toml**: `license = "AGPL-3.0-or-later"`
- **SPDX 头**: 新增源文件应在顶部包含 AGPL-3.0 声明(见 `LICENSE` 末尾 "How to Apply" 部分)

## 架构状态(Wave 15b 完成)

| 维度 | 状态 |
Expand Down Expand Up @@ -103,6 +110,13 @@ grep -rn "dirs::data_local_dir\|std::env::var_os\|std::env::var(\"LOCALAPPDATA\"
- 文件系统测试必须使用 `tempfile` + 注入式路径,禁止直接操作 `%LOCALAPPDATA%` 或 `~/.config`。
- Tantivy / SQLite 文件系统测试必须获取 `SEARCH_TEST_LOCK`(或同等级串行化机制)。

**子规则(来自 PR #4 教训)**:
- **R2.1 禁止 `DEVBASE_DATA_DIR` 全局注入**:并行测试中 `std::env::set_var("DEVBASE_DATA_DIR", ...)` 导致竞态;必须使用 `TempStorageBackend` 注入式替代。
- **R2.2 Windows 路径双端规范化**:`TempDir` 可能返回短文件名(`TEMP~1`),而 `dunce::canonicalize` 返回长文件名;路径比较前必须对**双方**调用 `dunce::canonicalize`。
- **R2.3 `git2` 测试显式身份 + 显式分支**:
- CI runner 无全局 `user.name`/`user.email` → `repo.signature()` 会 panic;必须改用 `git2::Signature::now("Test", "test@example.com")`。
- `git2::Repository::init` 的默认分支在不同平台可能为 `master` 或 `main`;必须显式 `repo.set_head("refs/heads/main")` 并 commit 到 `"refs/heads/main"`。

**Fitness Function**:
```bash
# 高并发下 100% 通过,无 flaky
Expand Down
9 changes: 4 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ resolver = "2"
version = "0.14.3"
authors = ["juice094 <160722440+juice094@users.noreply.github.com>"]
edition = "2024"
license = "MIT"
license = "AGPL-3.0-or-later"
repository = "https://github.com/juice094/devbase"


686 changes: 665 additions & 21 deletions LICENSE

Large diffs are not rendered by default.

53 changes: 53 additions & 0 deletions LICENSE-COMMERCIAL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Commercial License for devbase

## Dual Licensing Notice

devbase is licensed under the **GNU Affero General Public License v3.0 or later**
(AGPL-3.0+). A copy of the AGPL-3.0 license is included in the [`LICENSE`](./LICENSE)
file.

The AGPL-3.0 license grants broad freedoms for personal, academic, and
open-source use. However, it requires that any modified version deployed as a
network service (e.g., SaaS, hosted MCP server) must also be made available
under the AGPL-3.0 license, including full source code.

## Commercial Use & Alternative Licensing

If you wish to use devbase in a **closed-source product**, **proprietary SaaS**,
or any scenario where the AGPL-3.0 obligations are incompatible with your
business model, a commercial license is available.

Commercial licensing provides:

- **Exemption from AGPL-3.0 copyleft obligations** for your specific use case
- **Right to modify and distribute without source code disclosure**
- **Priority support and consultation** (subject to agreement terms)
- **Patent and liability protections** beyond the scope of AGPL-3.0

## Contact

To inquire about commercial licensing terms, pricing, or to request a
commercial license agreement, please contact:

**Email:** `juice094@protonmail.com`
**GitHub:** [@juice094](https://github.com/juice094)

Please include:
- Your company / organization name
- Intended use case (internal tool, SaaS product, embedded solution, etc.)
- Estimated deployment scale

## Contributor Notice

By contributing code, documentation, or other materials to the devbase
project, you agree that your contributions may be distributed under both the
AGPL-3.0 license and, at the discretion of the copyright holder, under
commercial license terms.

The copyright holder reserves the right to relicense future versions of
devbase under different terms. A Contributor License Agreement (CLA) may be
required for substantial contributions.

---

*Last updated: 2026-05-06*
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[![Version](https://img.shields.io/badge/version-v0.14.3-blue)](https://github.com/juice094/devbase/releases)
[![Tests](https://img.shields.io/badge/tests-390%20passed-brightgreen)](./AGENTS.md)
[![Clippy](https://img.shields.io/badge/clippy-0%20warnings-green)](./AGENTS.md)
[![License](https://img.shields.io/badge/license-MIT-orange)](./LICENSE)
[![License](https://img.shields.io/badge/license-AGPL--3.0-orange)](./LICENSE)
[![Rust](https://img.shields.io/badge/rust-1.94%2B-9cf)](https://www.rust-lang.org)

**本地优先的 AI Skill 编排基础设施**
Expand Down Expand Up @@ -349,4 +349,12 @@ token = "<YOUR_GITHUB_PAT>"

## 许可证

[MIT](./LICENSE)
本软件采用 **双许可证 (Dual License)** 模式:

- **开源使用**: [GNU Affero General Public License v3.0 or later (AGPL-3.0+)](./LICENSE)
- 适用于个人、学术、及遵守 AGPL-3.0 义务的开源项目
- **核心约束**: 若将修改版部署为网络服务(SaaS、托管 MCP Server 等),必须向用户公开完整源代码

- **商业使用**: 如需在闭源产品、专有 SaaS 或无法遵守 AGPL-3.0 的场景中使用,可联系作者获取商业授权
- 详见 [`LICENSE-COMMERCIAL.md`](./LICENSE-COMMERCIAL.md)
- 联系方式: `juice094@protonmail.com`
28 changes: 28 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Security Policy

## Supported Versions

The following versions of devbase currently receive security updates:

| Version | Supported |
| ------- | ------------------ |
| 0.14.x | :white_check_mark: |
| < 0.14 | :x: |

## Reporting a Vulnerability

If you discover a security vulnerability in devbase, please report it responsibly.

**Preferred method:** Use [GitHub Private Vulnerability Reporting](https://github.com/juice094/devbase/security/advisories/new) if enabled. This allows you to disclose the issue privately and collaborate on a fix before public disclosure.

**Alternative method:** Open a GitHub Issue with the title prefix `[SECURITY]` and mark it with the `security` label. Please do not include exploit details in the initial public report — we will reach out to you for technical details.

## Disclosure Policy

- We aim to acknowledge receipt of a security report within **72 hours**.
- We will work to validate the vulnerability and provide a timeline for a fix within **7 days**.
- Once a fix is released, we will publish a security advisory and credit the reporter (with their permission).

## Scope

This security policy covers the devbase CLI, MCP server, TUI, and all first-party skills shipped in this repository. Third-party dependencies should be reported to their respective maintainers.
9 changes: 4 additions & 5 deletions spike/candle-embedding/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

43 changes: 16 additions & 27 deletions src/mcp/tests.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 juice094
use super::*;
use crate::storage::StorageBackend;

fn test_ctx() -> (crate::storage::AppContext, tempfile::TempDir) {
let backend = std::sync::Arc::new(crate::storage::TempStorageBackend::new());
let ctx = crate::storage::AppContext::with_storage(backend).unwrap();
// Return a dummy TempDir to keep call-site destructuring compatible.
let tmp = tempfile::tempdir().unwrap();
unsafe {
std::env::set_var("DEVBASE_DATA_DIR", tmp.path());
}
let ctx = crate::storage::AppContext::with_defaults().unwrap();
(ctx, tmp)
}

Expand Down Expand Up @@ -421,20 +421,14 @@ fn test_nl_filter_repos_fallback_finds_by_language() {

#[test]
fn test_nl_filter_repos_tantivy_finds_devbase() {
let _guard = NL_FILTER_TEST_LOCK.lock().unwrap();
let _search_guard = crate::search::SEARCH_TEST_LOCK.lock().unwrap();

let tmp = tempfile::tempdir().unwrap();
let old = std::env::var("DEVBASE_DATA_DIR").ok();
unsafe {
std::env::set_var("DEVBASE_DATA_DIR", tmp.path());
}
let backend = std::sync::Arc::new(crate::storage::TempStorageBackend::new());
let index_path = backend.index_path().unwrap();

// Ensure DB schema exists in temp dir
let conn = crate::registry::WorkspaceRegistry::init_in_memory().unwrap();
// Ensure DB schema exists
let conn = crate::registry::WorkspaceRegistry::init_db_with(&*backend).unwrap();

// Populate Tantivy index with devbase doc
let (index, _reader) = crate::search::init_index().unwrap();
let (index, _reader) = crate::search::init_index_at(&index_path).unwrap();
let mut writer = crate::search::get_writer(&index).unwrap();
let schema = index.schema();
crate::search::add_repo_doc(
Expand All @@ -461,20 +455,15 @@ fn test_nl_filter_repos_tantivy_finds_devbase() {
remotes: vec![],
}];

let results =
crate::mcp::tools::repo::nl_filter_repos("developer workspace", &repos, &conn).unwrap();
let results = crate::mcp::tools::repo::nl_filter_repos_at(
&index_path,
"developer workspace",
&repos,
&conn,
)
.unwrap();
assert!(!results.is_empty(), "tantivy path should find devbase");
assert_eq!(results[0].id, "devbase");

if let Some(v) = old {
unsafe {
std::env::set_var("DEVBASE_DATA_DIR", v);
}
} else {
unsafe {
std::env::remove_var("DEVBASE_DATA_DIR");
}
}
}

#[test]
Expand Down
21 changes: 6 additions & 15 deletions src/mcp/tools/relations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -325,11 +325,8 @@ mod tests {

#[tokio::test]
async fn test_relation_store_and_query_roundtrip() {
let tmp = tempfile::tempdir().unwrap();
unsafe {
std::env::set_var("DEVBASE_DATA_DIR", tmp.path());
}
let mut ctx = crate::storage::AppContext::with_defaults().unwrap();
let backend = std::sync::Arc::new(crate::storage::TempStorageBackend::new());
let mut ctx = crate::storage::AppContext::with_storage(backend).unwrap();

// Pre-seed entities to satisfy FK constraint
let conn = ctx.conn().unwrap();
Expand Down Expand Up @@ -394,11 +391,8 @@ mod tests {

#[tokio::test]
async fn test_relation_store_missing_required_fields() {
let tmp = tempfile::tempdir().unwrap();
unsafe {
std::env::set_var("DEVBASE_DATA_DIR", tmp.path());
}
let mut ctx = crate::storage::AppContext::with_defaults().unwrap();
let backend = std::sync::Arc::new(crate::storage::TempStorageBackend::new());
let mut ctx = crate::storage::AppContext::with_storage(backend).unwrap();

let tool = DevkitRelationStoreTool;
let result = tool
Expand All @@ -413,11 +407,8 @@ mod tests {

#[tokio::test]
async fn test_relation_query_bidirectional() {
let tmp = tempfile::tempdir().unwrap();
unsafe {
std::env::set_var("DEVBASE_DATA_DIR", tmp.path());
}
let mut ctx = crate::storage::AppContext::with_defaults().unwrap();
let backend = std::sync::Arc::new(crate::storage::TempStorageBackend::new());
let mut ctx = crate::storage::AppContext::with_storage(backend).unwrap();

let conn = ctx.conn().unwrap();
let now = chrono::Utc::now().to_rfc3339();
Expand Down
34 changes: 31 additions & 3 deletions src/mcp/tools/repo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use crate::registry::RepoEntry;
use crate::repository::health::HealthRepository;
use crate::repository::repo::RepoRepository;
use crate::repository::workspace::WorkspaceRepository;
use crate::storage::AppContext;
use crate::storage::{AppContext, StorageBackend};
use anyhow::Context;

#[derive(Clone)]
Expand Down Expand Up @@ -554,6 +554,34 @@ pub(crate) fn nl_filter_repos(
query: &str,
repos: &[RepoEntry],
conn: &rusqlite::Connection,
) -> anyhow::Result<Vec<RepoEntry>> {
let backend = crate::storage::DefaultStorageBackend {};
let index_path = match backend.index_path() {
Ok(p) => p,
Err(e) => {
tracing::warn!("Failed to resolve index path: {}", e);
// Fallback to non-Tantivy path
let q = query.to_lowercase();
let stars_cond = parse_stars_condition(&q);
let explicit_tag = extract_tag_from_query(&q);
let mut results = Vec::new();
for repo in repos {
if apply_nl_filters(repo, &q, stars_cond, explicit_tag.as_deref(), conn)? {
results.push(repo.clone());
}
}
return Ok(results);
}
};
nl_filter_repos_at(&index_path, query, repos, conn)
}

/// Filter repos using an explicit Tantivy index path, bypassing global storage backend.
pub(crate) fn nl_filter_repos_at(
index_path: &std::path::Path,
query: &str,
repos: &[RepoEntry],
conn: &rusqlite::Connection,
) -> anyhow::Result<Vec<RepoEntry>> {
let q = query.to_lowercase();
let stars_cond = parse_stars_condition(&q);
Expand All @@ -569,7 +597,7 @@ pub(crate) fn nl_filter_repos(
|| q.contains("uptodate");

// Try Tantivy search first if index is not empty
let use_tantivy = match crate::search::index_is_empty() {
let use_tantivy = match crate::search::index_is_empty_at(index_path) {
Ok(empty) => !empty,
Err(e) => {
tracing::warn!("Failed to check search index: {}", e);
Expand All @@ -579,7 +607,7 @@ pub(crate) fn nl_filter_repos(

if use_tantivy && !query.trim().is_empty() {
let limit = repos.len().max(1000);
match crate::search::search_repos(query, limit) {
match crate::search::search_repos_at(index_path, query, limit) {
Ok(search_results) => {
let repo_map: std::collections::HashMap<_, _> =
repos.iter().map(|r| (r.id.clone(), r)).collect();
Expand Down
Loading
Loading