Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
3a60deb
Update Dockerfile
thefloshibisci Aug 5, 2026
73a318a
Fix memory bucket detail compatibility
thefloshibisci Aug 24, 2026
e756499
Refresh update manifest for bucket compatibility
thefloshibisci Aug 24, 2026
3d21a14
fix: accept MCP static token on read-only bucket detail endpoint
thefloshibisci Aug 24, 2026
c41b014
Refresh update manifest for MCP token auth fix
thefloshibisci Aug 24, 2026
b02c8c2
fix: lower min length for MCP tokens to accept short tokens
thefloshibisci Aug 24, 2026
33dd704
merge: sync P0luz main into fork
thefloshibisci Aug 31, 2026
d4b38ab
Merge upstream 3.6.14 and enforce AI-side bucket read permissions
thefloshibisci Nov 1, 2026
e215f72
Restrict upstream Docker Hub publishing to upstream repository
thefloshibisci Nov 1, 2026
262dbf5
Fix audited MCP, JWT and certificate dependencies with bounded lock r…
thefloshibisci Nov 1, 2026
30cc344
Require explicit dependency migration and verify the new security lock
thefloshibisci Nov 1, 2026
1427939
Refresh persisted dependency baseline when only the image lock changes
thefloshibisci Nov 1, 2026
4ad9525
Deploy tested main images to Tencent with scoped SSH and isolated pre…
thefloshibisci Nov 2, 2026
a0e39fd
Download verified Actions artifacts with bounded parallel HTTPS ranges
thefloshibisci Nov 2, 2026
c5a3ee9
fix(deploy): rehearse Xinchao with isolated OB dependency
thefloshibisci Nov 2, 2026
7858618
fix(deploy): allow bounded cross-region artifact transfers to finish
thefloshibisci Nov 2, 2026
feb1a1f
fix(deploy): refresh expiring artifact URLs during downloads
thefloshibisci Nov 2, 2026
d6e095d
fix(deploy): close safely while awaiting URL renewal
thefloshibisci Nov 2, 2026
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
11 changes: 3 additions & 8 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,6 @@
*.sh text eol=lf
entrypoint.sh text eol=lf

# Dashboard 热更新使用 GitHub 自动生成的源码归档。Git clone 仍保留可编辑的
# 约束文件,但归档只携带带 hash 的发布锁,避免旧更新器把源约束文字调整误判成
# 已安装依赖发生变化。
# 此兼容仅在 requirements.lock.txt 规范化 SHA-256 保持为下列值时安全:
# fdb24053349d8e18a55c3a5afbab8b92cc31d94b69e52359b350ab96b79001c9。
# 修改发布锁前必须移除此规则并提供显式依赖迁移;
# tests/test_update_source_gate.py 会强制校验该不变式。
/requirements.txt export-ignore
# Security dependency upgrades require an explicit image rebuild or locked pip
# install. Include both dependency manifests in archives so legacy updaters do
# not mistake this release for a code-only update. See docs/DEPENDENCY_SECURITY_20261102.md.
18 changes: 18 additions & 0 deletions .github/NANZHI_DEPLOY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Tencent production deployment

Only `main` push or a manual workflow run on `main` can deploy. Pull requests and other branches never receive the deployment job. The deployment job depends on the existing complete test jobs and downloads the exact image built in that run through a short-lived signed GitHub artifact URL; it does not rebuild on the server.

The client skips a queued commit if `main` has advanced. SSH checks the committed host public key and sends only the deployment instruction. The server downloads bounded parallel HTTPS ranges from GitHub artifact storage and verifies the final image SHA256; the GitHub token stays in Actions. Each repository has a separate `NANZHI_DEPLOY_KEY` secret, forced to the matching service; it cannot run a shell, forward ports, or deploy another service. Application tokens and data remain on the server.

The root-owned receiver on the server verifies archive digest, size, platform, tag, and revision label. It serializes updates, makes isolated data copies with SQLite online backups, and verifies health, invalid-token rejection, authenticated MCP initialization and tool discovery with external network access disabled. Failure at this stage leaves the running production service unchanged.

After rehearsal succeeds, it briefly enables a maintenance gate, stops only the selected service, archives its persistent data and saves the previous Compose configuration, then starts the tested image and repeats authentication checks. Success removes the maintenance gate. If failure happens after production is stopped or its data may have been migrated, the maintenance gate stays in place and the GitHub job fails. A maintainer must inspect the receipt and backup: automatic database downgrade could lose or corrupt new data, so it is deliberately not attempted.

Server paths (root only):

- `/usr/local/sbin/nanzhi-deploy`: deployment receiver.
- `/opt/nanzhi-deploy/runs/`: per-attempt receipts, previous configuration and pre-update data archive.
- `/opt/nanzhi-production/compose.json`: production services and private environment.
- `/var/lib/nanzhi-deploy-gates/`: persistent maintenance flags; remove only after verifying recovery.

Other services are manual. Old Zeabur Git triggers are disabled. Do not restart old Zeabur applications; legacy domains temporarily relay to the new server. Never print or commit production configurations, deployment private keys or data backups.
1 change: 1 addition & 0 deletions .github/nanzhi-known-hosts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
49.234.39.128 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKC70qOHpCT9KFi4Syv2MGEtLflUgaI+8JtAz0hWuNhj
372 changes: 372 additions & 0 deletions .github/ops/nanzhi_receiver.py

Large diffs are not rendered by default.

102 changes: 102 additions & 0 deletions .github/scripts/nanzhi_deploy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
"""Send the exact tested image to the service-scoped SSH deployment receiver."""
import hashlib
import json
import os
from pathlib import Path
import re
import subprocess
import sys
import tempfile
import threading
import urllib.request
import urllib.error
import urllib.parse


def main():
service, commit, filename = sys.argv[1:]
if service not in ('ombre-brain', 'xinchao-nian') or not re.fullmatch('[0-9a-f]{40}', commit):
raise ValueError('Invalid deployment identity')
# A queued older workflow must never replace a newer main commit.
latest = subprocess.check_output(
['gh', 'api', 'repos/' + os.environ['GITHUB_REPOSITORY'] + '/git/ref/heads/main', '--jq', '.object.sha'],
text=True,
).strip()
if latest != commit:
print('Skipped: main advanced after this build.')
return
image = Path(filename)
with image.open('rb') as source:
digest = hashlib.file_digest(source, 'sha256').hexdigest()
artifacts = json.loads(subprocess.check_output([
'gh', 'api', 'repos/' + os.environ['GITHUB_REPOSITORY'] + '/actions/runs/' + os.environ['GITHUB_RUN_ID'] + '/artifacts',
], text=True))['artifacts']
artifact = next(a for a in artifacts if a['name'] == 'nanzhi-image-' + commit and not a['expired'])
api_url = urllib.parse.urlsplit(artifact['archive_download_url'])
if api_url.scheme != 'https' or api_url.netloc != 'api.github.com' or not api_url.path.startswith('/repos/' + os.environ['GITHUB_REPOSITORY'] + '/actions/artifacts/'):
raise RuntimeError('Unexpected artifact API URL')
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self, *args, **kwargs):
return None
request = urllib.request.Request(artifact['archive_download_url'], headers={
'Authorization': 'Bearer ' + os.environ['GH_TOKEN'], 'Accept': 'application/vnd.github+json',
})
def signed_location():
try:
urllib.request.build_opener(NoRedirect()).open(request, timeout=30)
raise RuntimeError('Expected signed artifact download location')
except urllib.error.HTTPError as exc:
if exc.code != 302:
raise RuntimeError('Could not obtain artifact download location') from None
location = exc.headers['Location']
parsed = urllib.parse.urlsplit(location)
if parsed.scheme != 'https' or not (parsed.hostname or '').endswith('.blob.core.windows.net'):
raise RuntimeError('Unexpected artifact storage host')
return location
header = {'action': 'deploy_url', 'commit': commit, 'bytes': image.stat().st_size, 'sha256': digest,
'url': signed_location(), 'artifact_bytes': artifact['size_in_bytes']}
known_hosts = Path(__file__).resolve().parent.parent / 'nanzhi-known-hosts'
with tempfile.TemporaryDirectory(prefix='nanzhi-deploy-') as folder:
key = Path(folder) / 'key'
key.write_text(os.environ.pop('NANZHI_DEPLOY_KEY').strip() + '\n')
key.chmod(0o600)
cmd = [
'ssh', '-T', '-i', str(key), '-o', 'BatchMode=yes',
'-o', 'IdentitiesOnly=yes', '-o', 'StrictHostKeyChecking=yes',
'-o', 'UserKnownHostsFile=' + str(known_hosts),
'-o', 'ServerAliveInterval=30', '-o', 'ServerAliveCountMax=6',
'-o', 'ConnectTimeout=20', 'nanzhi-deploy@49.234.39.128',
]
with subprocess.Popen(cmd, stdin=subprocess.PIPE) as proc:
stop_refresh = threading.Event()
def refresh_url():
# Artifact URLs expire in ten minutes. Refresh the same artifact
# without transmitting the GitHub credential to the server.
while not stop_refresh.wait(180):
try:
renewed = dict(header, url=signed_location())
proc.stdin.write((json.dumps(renewed) + '\n').encode())
proc.stdin.flush()
except (BrokenPipeError, ValueError):
return
except Exception:
# The existing URL remains usable; retry well before expiry.
if stop_refresh.wait(15):
return
try:
proc.stdin.write((json.dumps(header) + '\n').encode())
proc.stdin.flush()
threading.Thread(target=refresh_url, daemon=True).start()
status = proc.wait(timeout=1920)
except BaseException:
proc.kill()
proc.wait()
raise
finally:
stop_refresh.set()
if status:
raise SystemExit(status)


if __name__ == '__main__':
main()
2 changes: 2 additions & 0 deletions .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ on:

jobs:
build-and-push:
# This workflow publishes the upstream owner's Docker Hub image.
if: github.repository == 'P0luz/Ombre-Brain'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
Expand Down
52 changes: 48 additions & 4 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
name: Tests

on:
workflow_dispatch:
push:
branches: [main, testing]
branches: [main, testing, 'fix/security-*']
paths-ignore:
- '*.md'
- 'backup_*/**'
Expand Down Expand Up @@ -39,8 +40,10 @@ jobs:
UV_EXCLUDE_NEWER: '2026-08-01T00:00:00Z'
run: |
rm -f requirements.lock.txt requirements-dev.lock.txt
uv pip compile requirements.txt --universal --python-version 3.12 --generate-hashes --output-file requirements.lock.txt
uv pip compile requirements-dev.in --universal --python-version 3.12 --generate-hashes --output-file requirements-dev.lock.txt
# Advance only these security-sensitive packages, preserving the
# original snapshot for all unrelated production and test packages.
uv pip compile requirements.txt --universal --python-version 3.12 --generate-hashes --exclude-newer-package mcp=2026-11-01T00:00:00Z --exclude-newer-package certifi=2026-11-01T00:00:00Z --exclude-newer-package pyjwt=2026-11-01T00:00:00Z --output-file requirements.lock.txt
uv pip compile requirements-dev.in --universal --python-version 3.12 --generate-hashes --exclude-newer-package mcp=2026-11-01T00:00:00Z --exclude-newer-package certifi=2026-11-01T00:00:00Z --exclude-newer-package pyjwt=2026-11-01T00:00:00Z --output-file requirements-dev.lock.txt
git diff --exit-code -- requirements.lock.txt requirements-dev.lock.txt

- name: Install locked dependencies
Expand Down Expand Up @@ -103,7 +106,7 @@ jobs:
docker run -d --name ob-ci-llm --network ob-ci-net \
-v "$PWD:/workspace:ro" -w /workspace python:3.12-slim \
python tests/mcp_llm_stub.py
docker build -t ombre-brain-ci:local .
docker build --label org.opencontainers.image.revision="$GITHUB_SHA" -t ombre-brain-ci:local .
docker run -d --name ob-ci-app --network ob-ci-net \
-p 127.0.0.1:18084:8000 \
-v ob-ci-data:/app/buckets \
Expand Down Expand Up @@ -142,9 +145,50 @@ jobs:
python -m pytest tests/test_web_api_docker_integration.py -q
--timeout=120


- name: Export tested deployment image
if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
run: |
docker tag ombre-brain-ci:local nanzhi/ombre-brain:github-${{ github.sha }}
docker save nanzhi/ombre-brain:github-${{ github.sha }} | gzip -1 > image.tar.gz

- name: Preserve tested image for deployment
if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
uses: actions/upload-artifact@v4
with:
name: nanzhi-image-${{ github.sha }}
path: image.tar.gz
compression-level: 0
retention-days: 3

- name: Clean Docker MCP stack
if: always()
run: |
docker rm -f ob-ci-app ob-ci-llm || true
docker volume rm ob-ci-data || true
docker network rm ob-ci-net || true

deploy-tencent:
name: Deploy tested image to Tencent
needs: test
if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
runs-on: ubuntu-latest
timeout-minutes: 35
permissions:
contents: read
actions: read
concurrency:
group: nanzhi-production-ombre-brain
cancel-in-progress: false
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/download-artifact@v4
with:
name: nanzhi-image-${{ github.sha }}
- name: Deploy with service-scoped key and pinned host identity
env:
GH_TOKEN: ${{ github.token }}
NANZHI_DEPLOY_KEY: ${{ secrets.NANZHI_DEPLOY_KEY }}
run: python .github/scripts/nanzhi_deploy.py ombre-brain "${{ github.sha }}" image.tar.gz
22 changes: 22 additions & 0 deletions docs/DEPENDENCY_SECURITY_20261102.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# 2026-11 dependency security update

This fork updates MCP 1.29.0 to 1.30.0, PyJWT 2.13.0 to 2.15.1 and certifi
2026.7.22 to 2026.10.23. Other resolved package versions remain unchanged.
The production and development locks include package hashes. Their resolution
keeps the original August index snapshot, advancing only these three packages
to the fixed November 1 UTC snapshot recorded in CI.

This requires a dependency installation, not only a hot code update. For Docker
or Zeabur, rebuild the image from the updated repository and recreate the service
with its existing persistent volumes and environment. For an existing virtual
environment, stop the service, run `python -m pip install --require-hashes -r
requirements.lock.txt`, verify `python -m pip check`, then restart it. Preserve a
current data backup and the previous deployment configuration before switching.

The source archive now includes both requirements.txt and requirements.lock.txt.
Older updaters must detect the dependency change instead of treating this archive
as code-only. Automatic pip installation remains disabled by default; do not
enable it merely to bypass the rebuild requirement. Verify installed versions
inside the running service, then check authentication, MCP initialization and
dependent clients. A source-code version string alone does not prove this fix is
installed. Security auditing remains enabled without ignore rules.
5 changes: 5 additions & 0 deletions docs/ENVIRONMENT_VARIABLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,8 @@
- `PASSWORD` → `OMBRE_DASHBOARD_PASSWORD`

正式名称和旧名同时存在时,以正式名称为准。删除兼容名必须经过一次明确的主版本迁移,并在发布说明中写出截止版本,禁止在普通重构中删除。

## Fork sidecar read compatibility

- `OMBRE_MCP_SERVICE_TOKEN`: optional dedicated read token (at least 32 characters). Accepted only on the fork bucket-detail read route, with AI-side letter visibility. Unset disables this credential.
- The existing `OMBRE_MCP_TOKEN` is accepted there only in token/hybrid mode and at least 32 characters. Dashboard mutation permissions are never granted.
25 changes: 14 additions & 11 deletions requirements-dev.lock.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# This file was autogenerated by uv via the following command:
# uv pip compile requirements-dev.in --universal --python-version 3.12 --generate-hashes --output-file requirements-dev.lock.txt
# uv pip compile requirements-dev.in --universal --python-version 3.12 --generate-hashes --exclude-newer-package mcp=2026-11-01T00:00:00Z --exclude-newer-package certifi=2026-11-01T00:00:00Z --exclude-newer-package pyjwt=2026-11-01T00:00:00Z --output-file requirements-dev.lock.txt
annotated-types==0.8.0 \
--hash=sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7 \
--hash=sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0
Expand Down Expand Up @@ -35,10 +35,11 @@ cachecontrol==0.14.4 \
--hash=sha256:b7ac014ff72ee199b5f8af1de29d60239954f223e948196fa3d84adaffc71d2b \
--hash=sha256:e6220afafa4c22a47dd0badb319f84475d79108100d04e26e8542ef7d3ab05a1
# via pip-audit
certifi==2026.7.22 \
--hash=sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775 \
--hash=sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55
certifi==2026.10.23 \
--hash=sha256:a9895e6e2758b371e0545e321a3a8fd1fcfd9829ada8824a936dfb16aaaf36cd \
--hash=sha256:e9b54de76e530bb6cf0ccd036787103acb58fd42ba930ea8bf948ddbaa2f61d1
# via
# -r requirements.txt
# httpcore
# httpx
# requests
Expand Down Expand Up @@ -735,9 +736,9 @@ markdown-it-py==4.2.0 \
--hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \
--hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a
# via rich
mcp==1.29.0 \
--hash=sha256:52d01f334de1868cc3bb2d6604931126a67631f99a6c5d3b82ba47290315ec36 \
--hash=sha256:f5a075bb611f23d6f4d080c6a1699fa62772eebc562ba9e66b306ddde1c755f7
mcp==1.30.0 \
--hash=sha256:445414625fce5c295faa505bb11bacece661ab6f4028d57c935db57820b7a3e4 \
--hash=sha256:666edb5009503e1047c9d60346a756f94b261f05cc2625f23d41c728ffc484d0
# via -r requirements.txt
mdurl==0.1.2 \
--hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \
Expand Down Expand Up @@ -1058,10 +1059,12 @@ pygments==2.20.0 \
# via
# pytest
# rich
pyjwt==2.13.0 \
--hash=sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423 \
--hash=sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728
# via mcp
pyjwt==2.15.1 \
--hash=sha256:42d59d631f7768a1028a64c7ff581a9bf7519804daf91fc5b6c56e30eec5e193 \
--hash=sha256:4f259e80cdfb6b3fc18a7de51fd1ef9ec79652f25019bae68975ca2468a34df8
# via
# -r requirements.txt
# mcp
pyparsing==3.3.2 \
--hash=sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d \
--hash=sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc
Expand Down
Loading