diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 05367b2..0e90aac 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,6 +13,7 @@ jobs: runs-on: windows-latest timeout-minutes: 45 permissions: + actions: read contents: write steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 @@ -28,6 +29,24 @@ jobs: if ((git rev-parse origin/main).Trim() -ne $env:GITHUB_SHA) { throw "release tag must point to the current main commit" } + - name: Require successful exact-SHA CI + env: + GH_TOKEN: ${{ github.token }} + shell: pwsh + run: | + $ciRuns = @( + gh run list --commit $env:GITHUB_SHA --workflow CI --limit 100 ` + --json databaseId,headSha,conclusion,status,url,workflowName,createdAt | ConvertFrom-Json + ) + $successfulRuns = @($ciRuns | Where-Object { + $_.workflowName -eq "CI" -and + $_.headSha -eq $env:GITHUB_SHA -and + $_.status -eq "completed" -and + $_.conclusion -eq "success" + }) + if ($successfulRuns.Count -lt 1) { + throw "exact-SHA CI did not complete successfully" + } - uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e with: enable-cache: true @@ -46,7 +65,7 @@ jobs: --commit $env:GITHUB_SHA ` --tree $tree ` --tag $env:GITHUB_REF_NAME ` - --version 0.4.0 ` + --version 0.4.1 ` --url "$env:GITHUB_SERVER_URL/$env:GITHUB_REPOSITORY/releases/tag/$env:GITHUB_REF_NAME" ` --ci-url "$env:GITHUB_SERVER_URL/$env:GITHUB_REPOSITORY/actions/runs/$env:GITHUB_RUN_ID" ` --output .hermes/desktop-runtime-v1/runtime/release-identity.json @@ -84,7 +103,7 @@ jobs: if ($manifestNames.Count -ne 3 -or ($manifestNames | Select-Object -Unique).Count -ne 3) { throw "expected exactly three unique checksum-bound payloads" } - - name: Publish verified assets + - name: Upload verified assets to draft release env: GH_TOKEN: ${{ github.token }} shell: pwsh @@ -97,4 +116,77 @@ jobs: (Resolve-Path "release-assets/release-identity.json").Path, (Resolve-Path "release-assets/SHA256SUMS.txt").Path ) - gh release create $env:GITHUB_REF_NAME $releaseAssets --verify-tag --title "ArcheAxis OS $env:GITHUB_REF_NAME" --generate-notes + gh release create $env:GITHUB_REF_NAME $releaseAssets --draft --verify-tag --title "ArcheAxis OS $env:GITHUB_REF_NAME" --generate-notes + - name: Read back and verify draft release assets + env: + GH_TOKEN: ${{ github.token }} + shell: pwsh + run: | + $expectedAssets = @( + "ArcheAxis.OS-Windows-x64-setup.exe", + "cognitive_loop_os-0.4.1-py3-none-any.whl", + "release-identity.json", + "SHA256SUMS.txt" + ) | Sort-Object + $release = gh release view $env:GITHUB_REF_NAME --json tagName,targetCommitish,isDraft,assets,url | ConvertFrom-Json + if ($release.tagName -ne $env:GITHUB_REF_NAME) { throw "release tag readback mismatch" } + if ($release.isDraft -ne $true) { throw "release draft state mismatch" } + $acceptedReleaseTargets = @("main", $env:GITHUB_SHA) + if ($release.targetCommitish -notin $acceptedReleaseTargets) { + throw "release target commit is neither main nor exact workflow SHA" + } + if ((git rev-list -n 1 "$($release.tagName)^{}").Trim() -ne $env:GITHUB_SHA) { + throw "release tag target does not equal the exact workflow SHA" + } + $publicAssets = @($release.assets | ForEach-Object name | Sort-Object) + if (Compare-Object $expectedAssets $publicAssets) { + throw "public asset set differs from expected release asset set" + } + $downloadRoot = ".hermes/task-runtime/release-download-readback" + New-Item -ItemType Directory -Force $downloadRoot | Out-Null + gh release download $env:GITHUB_REF_NAME --dir $downloadRoot + foreach ($asset in $release.assets) { + if (-not $asset.digest -or -not $asset.digest.StartsWith("sha256:")) { + throw "provider digest is missing for $($asset.name)" + } + $downloadedDigest = (Get-FileHash "$downloadRoot/$($asset.name)" -Algorithm SHA256).Hash.ToLowerInvariant() + if ($asset.digest.ToLowerInvariant() -ne "sha256:$downloadedDigest") { + throw "provider digest differs from downloaded asset for $($asset.name)" + } + } + $manifest = Get-Content "$downloadRoot/SHA256SUMS.txt" | ForEach-Object { + if ($_ -notmatch '^([0-9a-fA-F]{64}) (.+)$') { throw "invalid downloaded SHA256SUMS line: $_" } + [pscustomobject]@{ digest = $Matches[1].ToLowerInvariant(); name = $Matches[2] } + } + $expectedPayloads = @($expectedAssets | Where-Object { $_ -ne "SHA256SUMS.txt" } | Sort-Object) + $manifestPayloads = @($manifest | ForEach-Object name | Sort-Object) + if (Compare-Object $expectedPayloads $manifestPayloads) { + throw "downloaded checksum payload set differs from expected payload set" + } + foreach ($entry in $manifest) { + $actual = (Get-FileHash "$downloadRoot/$($entry.name)" -Algorithm SHA256).Hash.ToLowerInvariant() + if ($actual -ne $entry.digest) { throw "downloaded SHA-256 mismatch for $($entry.name)" } + $asset = @($release.assets | Where-Object name -eq $entry.name) + if ($asset.Count -ne 1 -or -not $asset[0].digest) { throw "missing provider digest for $($entry.name)" } + if ($asset[0].digest.ToLowerInvariant() -ne "sha256:$actual") { throw "provider digest mismatch for $($entry.name)" } + } + $identity = Get-Content "$downloadRoot/release-identity.json" -Raw | ConvertFrom-Json + $tree = (git rev-parse "$env:GITHUB_SHA^{tree}").Trim() + if ($identity.release.tag -ne $env:GITHUB_REF_NAME -or $identity.source.commit -ne $env:GITHUB_SHA) { + throw "downloaded release identity does not bind tag and exact commit" + } + if ($identity.source.tree -ne $tree) { + throw "downloaded release identity tree mismatch" + } + if ($identity.source.ci_url -ne "$env:GITHUB_SERVER_URL/$env:GITHUB_REPOSITORY/actions/runs/$env:GITHUB_RUN_ID") { + throw "downloaded release identity CI URL mismatch" + } + if ($identity.source.ci_run -ne [int64]$env:GITHUB_RUN_ID -or $identity.release.url -ne $release.url) { + throw "downloaded release identity provenance readback mismatch" + } + - name: Publish verified draft release + env: + GH_TOKEN: ${{ github.token }} + shell: pwsh + run: | + gh release edit $env:GITHUB_REF_NAME --draft=false diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..adde69a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,20 @@ +# Changelog + +All notable release changes are recorded here. Dates and publication status +must be read from Git/GitHub; a source entry does not itself prove publication. + +## [Unreleased] + +- The source Release Manifest remains `unreleased / public=false` until a + release artifact receives an injected and verified public identity. +- Added release-truth, licensing, third-party, and security documentation. +- Added a post-publication gate that reads back the exact public asset set, + provider digests, release identity, and downloaded SHA-256 payloads. + +## [0.4.0] - historical release + +`v0.4.0` is retained as historical publication evidence. Readback found +incomplete checksum payload coverage: the public installer name did not match +its checksum-manifest name and an extra public payload was absent from the +manifest. The tag, Release, and assets are intentionally preserved; remediation +must use a new version. This entry makes no signature claim. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a0931e1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 DTALEX66 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 6543029..47133cf 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,8 @@ ArcheAxis OS 以受治理的统一知识底座连接个人学习链和 AI 使用 - **个人学习与 AI 使用的双向反馈**:学习笔记、纠错、练习和人工审核不会自动提升为事实;AI 的来源、Claim、解释、任务结果和 Lesson 同样必须先经 Candidate 治理。 - **可治理的本地运行时**:SQLite 持久化、Outbox/Receipt、失败不改状态、重试与回读,以及不暴露内部审计 ID 的公开投影。 - **桌面 Workspace A1**:默认 Apple-light,Violet Core 保留为暗色主题;一级 Rail、动态二级导航、上下文与证据检查器和真实活动坞均已接入。真实 Chromium 验证壳层交互后方可发布;Tauri WebView 点击级证据和公开发布资产仍未闭环,不能用 Chromium 结果替代。 -- **当前版本**:`0.4.0`;Release Manifest 仍为 `unreleased / public=false`,公开 Alpha/Beta/Stable 发布尚未宣告。 +- **当前版本**:`0.4.1`;Release Manifest 仍为 `unreleased / public=false`,公开 Alpha/Beta/Stable 发布尚未宣告。 +- **发布真相**:`v0.4.0` 是保留且不可原地改写的 historical release,但具有 **incomplete checksum payload coverage**;它不证明完整发布完整性。`v0.4.1` 只有在 merge-SHA CI、tag、公开资产集合、下载后 SHA-256、release identity 与 installer lifecycle 全部回读通过后才可宣称发布。 Research candidate 仍必须经过人工审查和来源独立性验证,不能自动当作 verified truth。产品定位见 [`docs/PRODUCT_POSITIONING.md`](docs/PRODUCT_POSITIONING.md);当前事实、限制和验证证据见 [`docs/PROJECT_STATUS.md`](docs/PROJECT_STATUS.md) 与 [`docs/VERIFICATION_POLICY.md`](docs/VERIFICATION_POLICY.md)。 @@ -190,3 +191,10 @@ pre-commit run --all-files 开发中只运行受影响的定向测试;diff 冻结后运行一次必要完整门禁,推送后以一次 GitHub CI 为准。详细触发规则见 [`docs/VERIFICATION_POLICY.md`](docs/VERIFICATION_POLICY.md)。 CI 使用 `pyproject.toml` 作为依赖与工具配置单一事实源;`requirements.txt` 仅作为兼容安装清单并与核心依赖保持同步。 + +## 许可、安全与变更记录 + +- 项目许可:[`LICENSE`](LICENSE) +- 第三方组件说明:[`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md) +- 漏洞报告:[`SECURITY.md`](SECURITY.md) +- 发布历史与已知完整性限制:[`CHANGELOG.md`](CHANGELOG.md) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..2e7ee66 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,29 @@ +# Security Policy + +## Supported versions + +Security fixes are developed against the current `main` branch and the newest +published release, when one exists. The historical `v0.4.0` release is retained +for provenance but does not have complete checksum payload coverage. It must +not be treated as a fully integrity-verified distribution. + +## Reporting a vulnerability + +Do not open a public issue containing exploit details, credentials, personal +data, or private paths. Use GitHub's private security-advisory reporting for +this repository. If that facility is unavailable, open a minimal public issue +asking the maintainer to enable a private reporting channel, without including +the vulnerability details. + +Include the affected version or commit, impact, reproduction preconditions, +and the smallest non-sensitive proof available. Never attach real credentials, +database contents, personal Vault data, or executable payloads. + +## Release-security boundary + +A successful build or CI run alone does not prove a public release. Release +claims require an exact main-commit tag, the required exact-SHA CI, an explicit +asset allowlist, checksum-manifest coverage, provider asset inventory readback, +downloaded SHA-256 verification, release-identity provenance, and installer +lifecycle verification. No release artifact is claimed to be cryptographically +signed unless a separate, verifiable signature and trust chain are published. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000..058997a --- /dev/null +++ b/THIRD_PARTY_NOTICES.md @@ -0,0 +1,35 @@ +# Third-Party Notices + +ArcheAxis OS / Cognitive-Loop-OS is licensed under the MIT License; see +[`LICENSE`](LICENSE). That license does not replace the licenses of third-party +packages, bundled binaries, fonts, or other components. + +## Declared direct Python dependencies + +The release dependency contract is `pyproject.toml` plus the exact resolved +`uv.lock`. At version 0.4.1 the direct runtime declarations are: + +`fastapi`, `python-multipart`, `uvicorn`, `pydantic`, `numpy`, `requests`, +`pyyaml`, `beautifulsoup4`, `defusedxml`, `apscheduler`, `sqlite-vec`, `loguru`, +`structlog`, `markitdown`, `trafilatura`, `networkx`, `litellm`, `pillow`, and +`pytesseract`. + +Optional or development groups additionally declare `setuptools`, +`playwright`, `httpx2`, `jinja2`, `jsonschema`, `pytest`, `ruff`, `tomli`, +`newspaper4k`, `readabilipy`, `youtube-transcript-api`, `mypy`, `pre-commit`, +`crawl4ai`, `langfuse`, and `promptfoo`. The desktop dependency contract is +`desktop/package.json`, `desktop/package-lock.json`, `desktop/src-tauri/Cargo.toml`, +and `desktop/src-tauri/Cargo.lock`. + +The lockfiles, not this summary, are authoritative for exact names, versions, +and transitive packages. Each component remains subject to its own upstream +license and notices. A redistributor must preserve those terms and audit the +exact built artifact; this document does not claim that every optional package +is bundled into every distribution. + +## External tools + +Some development or verification paths can invoke separately installed tools +such as Git, GitHub CLI, Tesseract OCR, Node.js/npm, Rust/Cargo, and NSIS. They +are not relicensed by this repository. Their presence in a build log is not +proof that they are included in a published asset. diff --git a/app/main.py b/app/main.py index 5d08b92..a92a7ae 100644 --- a/app/main.py +++ b/app/main.py @@ -42,7 +42,7 @@ async def core_runtime_lifespan(_: FastAPI): app = FastAPI( lifespan=core_runtime_lifespan, title="Cognitive-Loop-OS", - version=str(config.get("app.version", "0.4.0")), + version=str(config.get("app.version", "0.4.1")), description="AI cognitive runtime with an integrated Knowledge-Base. " "Absorbs Obsidian, Tana, Notion, Logseq, Roam, Heptabase, GraphRAG, Zettelkasten.", docs_url="/docs", @@ -362,7 +362,7 @@ def health(): return { "status": "ok", "system": "cognitive-loop-os", - "version": str(config.get("app.version", "0.4.0")), + "version": str(config.get("app.version", "0.4.1")), "endpoints": _http_route_counts(), "stats": { "documents": _c("kb_documents"), @@ -649,7 +649,7 @@ def architecture(): Auth-->Router; Router-->Pipeline; Pipeline-->Search; Pipeline-->Garden Search-->SQLite; Search-->VecDB; Garden-->GraphDB; Review-->SQLite; SQLite-->Backup ```""", - "version": str(config.get("app.version", "0.4.0")), + "version": str(config.get("app.version", "0.4.1")), "measurement_note": "Module and test counts are intentionally not embedded.", } diff --git a/app/release-manifest.json b/app/release-manifest.json index 6f82c03..c3b3b1d 100644 --- a/app/release-manifest.json +++ b/app/release-manifest.json @@ -6,7 +6,7 @@ "english_name": "ArcheAxis OS", "workspace_name": "元枢·观心", "workspace_english_name": "ArcheAxis Cognitive Workspace", - "version": "0.4.0", + "version": "0.4.1", "requires_python": ">=3.11" }, "release": { @@ -23,7 +23,7 @@ "dependency_lock": { "path": "uv.lock", "algorithm": "sha256", - "digest": "59051acb00c9824e4c76d5ea1a69eb701d029565687b68dd683d50d2788a589c", + "digest": "10304d747de08eaefd2a6972ad0191bdc24c1ead3d42c3ca644194daeb4421c3", "format_version": 1, "revision": 3 }, diff --git a/config/defaults.yaml b/config/defaults.yaml index bbf6dd1..e1f7fac 100644 --- a/config/defaults.yaml +++ b/config/defaults.yaml @@ -1,7 +1,7 @@ # Public, non-secret baseline for every runtime profile. app: name: "Cognitive-Loop-OS" - version: "0.4.0" + version: "0.4.1" environment: "development" port: 8000 host: "127.0.0.1" diff --git a/config/settings.yaml b/config/settings.yaml index c87d9bd..480a4a1 100644 --- a/config/settings.yaml +++ b/config/settings.yaml @@ -4,7 +4,7 @@ app: name: "Cognitive-Loop-OS" - version: "0.4.0" + version: "0.4.1" release_version: "" environment: "development" port: 8000 diff --git a/desktop/package-lock.json b/desktop/package-lock.json index 2e6ada1..ee832ab 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -1,12 +1,12 @@ { "name": "archeaxis-desktop-shell", - "version": "0.4.0", + "version": "0.4.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "archeaxis-desktop-shell", - "version": "0.4.0", + "version": "0.4.1", "devDependencies": { "@tauri-apps/cli": "2.11.4" } diff --git a/desktop/package.json b/desktop/package.json index 0f5a4e7..877b9f2 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -1,7 +1,7 @@ { "name": "archeaxis-desktop-shell", "private": true, - "version": "0.4.0", + "version": "0.4.1", "scripts": { "tauri": "tauri" }, diff --git a/desktop/scripts/verify_nsis_install.ps1 b/desktop/scripts/verify_nsis_install.ps1 index da8c9ee..a373a6c 100644 --- a/desktop/scripts/verify_nsis_install.ps1 +++ b/desktop/scripts/verify_nsis_install.ps1 @@ -93,14 +93,14 @@ try { $base = "http://127.0.0.1:$($normal.Listener.LocalPort)" $workspaceStatus = (Invoke-WebRequest "$base/workspace" -UseBasicParsing).StatusCode $status = Invoke-RestMethod "$base/workspace/api/status" - if ($workspaceStatus -ne 200 -or $status.release.version -ne '0.4.0') { + if ($workspaceStatus -ne 200 -or $status.release.version -ne '0.4.1') { throw 'installed Workspace returned an invalid product response' } if ($RequireReleaseIdentity) { $version = Invoke-RestMethod "$base/version" if ( $version.release.status -ne 'released' -or - $version.release.tag -ne 'v0.4.0' -or + $version.release.tag -ne 'v0.4.1' -or $version.capabilities.public_installer -ne 'available' ) { throw 'installed runtime did not expose the verified public release identity' diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index b125db4..bd85bc5 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -49,7 +49,7 @@ checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "archeaxis-desktop-shell" -version = "0.4.0" +version = "0.4.1" dependencies = [ "getrandom 0.3.4", "serde", diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index 82fb7cf..d9b51a2 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "archeaxis-desktop-shell" -version = "0.4.0" +version = "0.4.1" edition = "2024" rust-version = "1.88" diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index 22f5e8d..8d38f79 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "ArcheAxis OS", - "version": "0.4.0", + "version": "0.4.1", "identifier": "com.archeaxis.cognitive-workspace", "build": { "frontendDist": "../bootstrap" diff --git a/docs/PROJECT_STATUS.md b/docs/PROJECT_STATUS.md index f21f66f..a7ce110 100644 --- a/docs/PROJECT_STATUS.md +++ b/docs/PROJECT_STATUS.md @@ -1,6 +1,12 @@ # 项目当前状态 -> 更新:2026-07-27。本页是能力状态入口;旧审计文件是历史快照。机器无关的恢复检查见 [`HANDOFF_2026-07-21.md`](HANDOFF_2026-07-21.md),实时分支、SHA、dirty 状态与 CI 必须从 Git/GitHub 读取。 +> 更新:2026-08-01。本页是能力状态入口;旧审计文件是历史快照。机器无关的恢复检查见 [`HANDOFF_2026-07-21.md`](HANDOFF_2026-07-21.md),实时分支、SHA、dirty 状态与 CI 必须从 Git/GitHub 读取。 + +## 发布真相 + +- 当前源码版本为 `0.4.1`,但 packaged source manifest 仍为 `unreleased / public=false`;源码字段不会预先宣告公开发布。 +- `v0.4.0` 是保留的 historical release,但 readback 已证明 incomplete checksum payload coverage:公开 installer 名称与 manifest 名称不一致,且有一个额外公开 payload 未被 manifest 覆盖。历史 tag、Release 和资产不原地替换。 +- 新版本只有在 tag 精确绑定受保护 `main`、exact-SHA CI 全绿、公开资产集合与 checksum payload allowlist 双向一致、provider digest 可核验、下载后 SHA-256 复算通过、release identity 读回一致、installer lifecycle 通过后,才可报告发布完成。当前没有签名发布声明。 ## 当前阶段 diff --git a/knowledge_base/api.py b/knowledge_base/api.py index eedb7d7..6c18a68 100644 --- a/knowledge_base/api.py +++ b/knowledge_base/api.py @@ -1,4 +1,4 @@ -"""Knowledge-Base API — SQLite-backed, v0.4.0. +"""Knowledge-Base API — SQLite-backed, v0.4.1. Route counts are reported live by the mounted Core ``/health`` endpoint. Full OpenAPI documentation at /docs. @@ -34,7 +34,7 @@ app = FastAPI( title="Cognitive-Loop-OS Knowledge-Base", - version="0.4.0", + version="0.4.1", description="Knowledge management runtime. Absorbs capabilities from Obsidian, Tana, Notion, Logseq, Roam, Heptabase, Capacities, Anytype, GraphRAG, and Zettelkasten.", docs_url="/docs", redoc_url="/redoc", diff --git a/pyproject.toml b/pyproject.toml index bcc8bb8..f3b3250 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "cognitive-loop-os" -version = "0.4.0" +version = "0.4.1" description = "Local-first cognitive and knowledge runtime with evidence-constrained workflows" readme = "README.md" requires-python = ">=3.11" diff --git a/shared/config.py b/shared/config.py index 23cd743..11574d8 100644 --- a/shared/config.py +++ b/shared/config.py @@ -25,7 +25,7 @@ _DEFAULTS: dict[str, Any] = { "app": { "name": "Cognitive-Loop-OS", - "version": "0.4.0", + "version": "0.4.1", "environment": "development", "port": 8000, "host": "0.0.0.0", diff --git a/tests/test_ci_a0_gates.py b/tests/test_ci_a0_gates.py index 1ca8aaa..c7244d2 100644 --- a/tests/test_ci_a0_gates.py +++ b/tests/test_ci_a0_gates.py @@ -94,6 +94,57 @@ def test_desktop_shell_uses_the_product_version_everywhere() -> None: } == {product_version} +def test_v0_4_1_release_candidate_uses_one_version_everywhere() -> None: + expected_version = "0.4.1" + project = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8")) + manifest = json.loads( + (ROOT / "app/release-manifest.json").read_text(encoding="utf-8") + ) + package = json.loads((ROOT / "desktop/package.json").read_text(encoding="utf-8")) + package_lock = json.loads( + (ROOT / "desktop/package-lock.json").read_text(encoding="utf-8") + ) + tauri = json.loads( + (ROOT / "desktop/src-tauri/tauri.conf.json").read_text(encoding="utf-8") + ) + cargo = tomllib.loads( + (ROOT / "desktop/src-tauri/Cargo.toml").read_text(encoding="utf-8") + ) + release_workflow = ( + ROOT / ".github/workflows/release.yml" + ).read_text(encoding="utf-8") + + assert { + project["project"]["version"], + manifest["product"]["version"], + package["version"], + package_lock["version"], + package_lock["packages"][""]["version"], + tauri["version"], + cargo["package"]["version"], + } == {expected_version} + assert f"--version {expected_version}" in release_workflow + assert ( + 'name = "cognitive-loop-os"\nversion = "0.4.1"\nsource = { editable = "." }' + in (ROOT / "uv.lock").read_text(encoding="utf-8") + ) + for path in ( + "config/defaults.yaml", + "config/settings.yaml", + "shared/config.py", + "app/main.py", + "knowledge_base/api.py", + "desktop/scripts/verify_nsis_install.ps1", + ): + text = (ROOT / path).read_text(encoding="utf-8") + assert expected_version in text + assert "0.4.0" not in text + lifecycle = (ROOT / "desktop/scripts/verify_nsis_install.ps1").read_text( + encoding="utf-8" + ) + assert "v0.4.1" in lifecycle + + def test_wheel_gate_requires_release_and_workspace_assets() -> None: workflow = WORKFLOW.read_text(encoding="utf-8") wheel_job = _job_section(workflow, "wheel-smoke", "browser-smoke") diff --git a/tests/test_diagnostics_api.py b/tests/test_diagnostics_api.py index 10ae72c..c82a4f1 100644 --- a/tests/test_diagnostics_api.py +++ b/tests/test_diagnostics_api.py @@ -18,7 +18,7 @@ def test_diagnostics_returns_safe_versioned_status() -> None: ) assert payload["release"] == { "status": "unreleased", - "version": "0.4.0", + "version": "0.4.1", "channel": "development", "source_commit": "unavailable", } @@ -37,7 +37,7 @@ def test_diagnostics_does_not_promote_unverified_release_override(monkeypatch) - assert response.status_code == 200 assert response.json()["release"] == { "status": "unreleased", - "version": "0.4.0", + "version": "0.4.1", "channel": "development", "source_commit": "unavailable", } @@ -54,7 +54,7 @@ def test_diagnostics_rejects_unsafe_release_version(monkeypatch) -> None: assert response.status_code == 200 assert response.json()["release"] == { "status": "unreleased", - "version": "0.4.0", + "version": "0.4.1", "channel": "development", "source_commit": "unavailable", } diff --git a/tests/test_release_manifest.py b/tests/test_release_manifest.py index 2f917bf..4e1e6dd 100644 --- a/tests/test_release_manifest.py +++ b/tests/test_release_manifest.py @@ -30,7 +30,7 @@ def test_release_manifest_is_packaged_truth_and_matches_dependency_lock() -> Non "channel": "development", "public": False, } - assert manifest["product"]["version"] == "0.4.0" + assert manifest["product"]["version"] == "0.4.1" assert manifest["source"]["commit"] == "unavailable" assert manifest["verification"]["embedded_test_counts"] is False lock_digest = hashlib.sha256((root / "uv.lock").read_bytes()).hexdigest() @@ -63,7 +63,7 @@ def test_release_manifest_is_packaged_truth_and_matches_dependency_lock() -> Non assert manifest["product"]["version"] == config.get("app.version") assert safe_release_summary() == { "status": "unreleased", - "version": "0.4.0", + "version": "0.4.1", "channel": "development", "source_commit": "unavailable", } @@ -145,11 +145,11 @@ def test_bundled_release_identity_exposes_a_verified_public_release_summary( { "schema_version": "1.0.0", "release": { - "tag": "v0.4.0", - "version": "0.4.0", + "tag": "v0.4.1", + "version": "0.4.1", "channel": "stable", "public": True, - "url": "https://github.com/DTALEX66/Cognitive-Loop-OS/releases/tag/v0.4.0", + "url": "https://github.com/DTALEX66/Cognitive-Loop-OS/releases/tag/v0.4.1", }, "source": { "commit": "34ca0fbd5ae636314a3403c473bde9247ef95907", @@ -166,12 +166,12 @@ def test_bundled_release_identity_exposes_a_verified_public_release_summary( assert release.safe_release_summary() == { "status": "released", - "version": "0.4.0", + "version": "0.4.1", "channel": "stable", "source_commit": "34ca0fbd5ae636314a3403c473bde9247ef95907", - "tag": "v0.4.0", + "tag": "v0.4.1", "ci_run": 30548553629, - "url": "https://github.com/DTALEX66/Cognitive-Loop-OS/releases/tag/v0.4.0", + "url": "https://github.com/DTALEX66/Cognitive-Loop-OS/releases/tag/v0.4.1", } assert release.effective_capabilities()["public_installer"] == "available" @@ -294,6 +294,85 @@ def test_release_workflow_publishes_only_checksum_bound_allowlist() -> None: assert 'gh release create $env:GITHUB_REF_NAME $releaseAssets' in workflow +def test_release_truth_documents_preserve_historical_and_source_manifest_truth() -> None: + root = Path(__file__).resolve().parents[1] + required = ["LICENSE", "THIRD_PARTY_NOTICES.md", "SECURITY.md", "CHANGELOG.md"] + for relative in required: + assert (root / relative).is_file(), f"missing release truth document: {relative}" + + readme = (root / "README.md").read_text(encoding="utf-8") + status = (root / "docs" / "PROJECT_STATUS.md").read_text(encoding="utf-8") + changelog = (root / "CHANGELOG.md").read_text(encoding="utf-8") + combined = "\n".join((readme, status, changelog)) + assert "v0.4.0" in combined + assert "historical" in combined.lower() + assert "incomplete checksum payload coverage" in combined + assert "unreleased / public=false" in combined + assert "artifacts are signed" not in changelog.lower() + + +def test_release_workflow_downloads_and_rehashes_exact_public_asset_set() -> None: + root = Path(__file__).resolve().parents[1] + workflow = (root / ".github" / "workflows" / "release.yml").read_text( + encoding="utf-8" + ) + + assert "Read back and verify draft release assets" in workflow + assert "gh release view $env:GITHUB_REF_NAME --json tagName,targetCommitish,isDraft,assets,url" in workflow + assert "gh release download $env:GITHUB_REF_NAME" in workflow + assert "Get-FileHash" in workflow + assert "provider digest" in workflow.lower() + assert "public asset set differs from expected release asset set" in workflow + + +def test_release_workflow_requires_exact_sha_ci_before_building_release_assets() -> None: + """A tag bound to main cannot bypass the successful CI run for that exact SHA.""" + root = Path(__file__).resolve().parents[1] + workflow = (root / ".github" / "workflows" / "release.yml").read_text( + encoding="utf-8" + ) + + assert "Require successful exact-SHA CI" in workflow + assert 'gh run list --commit $env:GITHUB_SHA --workflow CI' in workflow + assert "exact-SHA CI did not complete successfully" in workflow + assert workflow.index("Require successful exact-SHA CI") < workflow.index( + "Prepare bundled runtime and inject exact release identity" + ) + + +def test_release_workflow_keeps_assets_draft_until_readback_closes() -> None: + """No asset becomes public before its GitHub inventory and downloaded bytes verify.""" + root = Path(__file__).resolve().parents[1] + workflow = (root / ".github" / "workflows" / "release.yml").read_text( + encoding="utf-8" + ) + + draft_create = 'gh release create $env:GITHUB_REF_NAME $releaseAssets --draft' + download = "gh release download $env:GITHUB_REF_NAME" + publish = 'gh release edit $env:GITHUB_REF_NAME --draft=false' + assert draft_create in workflow + assert "Read back and verify draft release assets" in workflow + assert "$release.isDraft -ne $true" in workflow + assert "release draft state mismatch" in workflow + assert publish in workflow + assert workflow.index(draft_create) < workflow.index(download) < workflow.index(publish) + + +def test_release_workflow_readback_binds_target_and_identity_tree() -> None: + """The downloaded identity must bind all Git provenance, not only its commit.""" + root = Path(__file__).resolve().parents[1] + workflow = (root / ".github" / "workflows" / "release.yml").read_text( + encoding="utf-8" + ) + + assert '$acceptedReleaseTargets = @("main", $env:GITHUB_SHA)' in workflow + assert '$release.targetCommitish -notin $acceptedReleaseTargets' in workflow + assert "release target commit is neither main nor exact workflow SHA" in workflow + assert '$identity.source.tree -ne $tree' in workflow + assert "downloaded release identity tree mismatch" in workflow + assert "downloaded release identity CI URL mismatch" in workflow + + def test_release_identity_injection_manifests_exact_commit_and_tree(tmp_path) -> None: """Verify scripts/release_inject_identity.py writes valid identity manifest.""" import json diff --git a/uv.lock b/uv.lock index 5f0598f..9c4b4d5 100644 --- a/uv.lock +++ b/uv.lock @@ -618,7 +618,7 @@ wheels = [ [[package]] name = "cognitive-loop-os" -version = "0.4.0" +version = "0.4.1" source = { editable = "." } dependencies = [ { name = "apscheduler" },