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
9 changes: 8 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,17 @@ __pycache__/
.venv/
venv/
.env
.env.*
!.env.example
.git/
.gitignore
todos.db
*.db-journal
*.db-wal
*.db-shm
data/
data/
storage/
experiments/chroma_db/
docker_data/
docker_storage/
docker_chroma_db/
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,9 +296,9 @@ Hybrid 检索(同一租户/分类、已索引当前版本,最多 Top14)

**校验只证明格式、引用出处和状态接线,不证明语义蕴含、逐句忠实或抗提示注入。** 不把有效引用或实验 gate 放行写成答案正确率,也不改写历史 NO_PROMOTION / FAIL。

## 3.3 集成验收状态(2026-10-17)
## 3.3 集成验收状态(更新至 2026-10-18)

**当前不接受为已验收发布版。** 已补齐仓库声明的评测测试依赖,在隔离 Windows / Python 3.12.4 环境运行完整 `tests/`:最新 656 项通过、零失败/跳过;不只原重点回归集。主 pytest 进程禁止外部 socket 与读取项目私有 `data/`,测试使用临时数据;CI 包含相同完整离线检查。离线成功不是模型质量验收。
**当前不接受为已验收发布版。** 已补齐仓库声明的评测测试依赖,在隔离 Windows / Python 3.12.4 环境运行完整 `tests/`:最新 659 项通过、零失败/跳过;不只原重点回归集。主 pytest 进程禁止外部 socket 与读取项目私有 `data/`,测试使用临时数据;CI 包含相同完整离线检查。离线成功不是模型质量验收。

真实问答计划复用同一脚本,默认仍然零调用:

Expand Down Expand Up @@ -327,7 +327,9 @@ python -m scripts.smoke_hybrid_runtime --ask --execute --env-file .env

本次仅 12 次新加坡分类,均 HTTP 200,无重试;usage 合计 27481 tokens,请求体 119304 bytes,没有重跑检索或生成。`python -m scripts.probe_version_audit` 默认只输出零调用计划;真实执行还要求事先授权、配置专用新加坡凭据并绑定计划哈希。单次/累计请求体上限为 24/192 KiB,最多 12 次调用;发送守卫逐次比对完整请求体,预期标签不进入模型请求。任何网络、提供方或解析异常即停,不自动续跑。候选仅部分有效,下一步应先判断审计改进的收益,不自动进入新一轮付费调参。

只读检查确认 Docker 引擎仍不可连接;真实运行服务 HTTP、容器重启/持久化与回退验收均未完成。本次使用临时 ASGI 应用、SQL 和 Chroma,不是部署验收;Ticket Agent 仍使用 Dense,不声明全链 Hybrid,默认开关保持关闭。下一步仍需处理版本审计一致性与运行环境验收,而不是因 8 类流程通过就晋级发布。
2026-10-18 启动 Docker 后完成[隔离本地容器验收](docs/reports/local_container_acceptance_2026-10-18.json):使用原 Dockerfile/Alembic/Uvicorn、假凭据、3 个独立卷和 `--network none`,通过容器内真实回环 TCP HTTP 检查迁移 head、JWT/角色/租户隔离、两篇虚构文档的元数据/原文校验和、独立 Chroma 预制向量及审计记录。在重启、删除重建保留卷、临时开启空范围 Hybrid、恢复关闭配置后均通过,最终审计 10 条,工单/Agent 执行表仍为零;没有付费模型调用。

本次不发布宿主机端口、不加载真实 `.env`;Chroma 预制向量不是文档索引/检索质量验证,空范围 `no_context` 不是非空问答验证。完整 Compose、宿主机回环访问、真实服务非空问答及 Dense 答案回退仍待验收,不因此宣称全面发布通过。固定检查入口为 `python -m scripts.smoke_local_persistence`,首次全新验收卷加 `--seed`;**只允许规定假凭据/数据库路径、Docker 无网络环境,拒绝 `-O`,不能直接针对日常服务运行**。临时容器和仅含虚构数据的卷已清理,专用镜像保留供复现。Ticket Agent 仍使用 Dense,默认 Hybrid 开关和 v5.3.1 候选接线均未改变。

---

Expand Down Expand Up @@ -670,6 +672,8 @@ docker compose up --build

Docker Compose 用于本地可复现运行与核心链路验证,不作为生产部署能力声明。

镜像构建排除 `.env`/其变体、开发及 Docker 的数据库/上传文件/Chroma 目录,保留公开 `.env.example`。Git 忽略不等于 Docker 构建忽略;不要把日常运行数据复制进镜像。

---

# 10. 测试 / CI
Expand Down
188 changes: 188 additions & 0 deletions docs/reports/local_container_acceptance_2026-10-18.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
{
"date": "2026-10-18",
"observed_at_utc": "2026-10-18 14:11:48 UTC",
"status": "LOCAL_CONTAINER_PERSISTENCE_PASS_NOT_FULL_DEPLOYMENT_ACCEPTANCE",
"base_commit": "6b56b3b10726f8cf922b407190cc69ae4bb7f801",
"scope": "Actual Dockerfile-built Linux containers, Alembic and Uvicorn, real TCP HTTP on container loopback. Dedicated disposable volumes, no port publication; not TestClient/ASGI in-process and not the full Compose deployment.",
"docker_engine_version": "29.6.2",
"container_python_version": "3.11.15",
"migration_revision": "20260705_0001 (head)",
"image": "copilot-acceptance-20261018:latest",
"final_image_id": "sha256:3a6991f9dae4ed2f127a97aa2afc818cb72d171220a9abddfd87c781d6069fbc",
"initial_phase_image_id": "sha256:8c4e38c8f99adb8671426f7260882bbbd0a51a7fe361b98ed981a54e2afb2d54",
"source_sha256_raw_at_execution": [
{
"Name": ".dockerignore",
"Sha256": "f38a591e61b17cbd9f84876a09cd94e14d9f146a9e89bd8b4bac8d68cdd6e480"
},
{
"Name": "Dockerfile",
"Sha256": "065848ff7de76fa0cac1c21346a1f235692050c6825c6d7b84b68a6b33c8c851"
},
{
"Name": "smoke_local_persistence.py",
"Sha256": "fd113cf6cf40d47b00e4d23c562a5155832001033294b3a3f6e9a4e833665852"
},
{
"Name": "test_docker_build_context.py",
"Sha256": "22dcfd8d3e163120025814dd61df6355728c3b077445e4474ab30475360ceb10"
}
],
"image_build": {
"dockerfile_changed": false,
"requirements_changed": false,
"declared_dependencies_installed": true,
"pip_check": "No broken requirements found.",
"runtime_data_and_env_paths_absent_in_image": true,
"public_env_template_retained": true
},
"isolation": {
"runtime_network": "none",
"published_ports": {},
"real_env_loaded": false,
"credentials": "fixed dummy key; provider base URLs point to container loopback port 9",
"data_mounts": [
{
"Name": "copilot_acceptance_20261018_6b56b3b_sql",
"Destination": "/app/data"
},
{
"Name": "copilot_acceptance_20261018_6b56b3b_storage",
"Destination": "/app/storage"
},
{
"Name": "copilot_acceptance_20261018_6b56b3b_chroma",
"Destination": "/app/experiments/chroma_db"
}
],
"no_primary_checkout_data_mounts": true,
"provider_calls": 0,
"candidate_v5_3_1_activated": false
},
"phases": [
{
"name": "fresh_seed_default_off",
"status": "PASS",
"network": "none",
"published_ports": {},
"documents": 2,
"vector_canaries": 1,
"provider_calls": 0,
"container_id": "57a0029efb63af2d2f7ae4651e111444b7757b8ab3c9a2b877f5695731d430b1",
"hybrid_search_enabled": false,
"hybrid_ask_enabled": false,
"search_http_status": 503,
"ask_http_status": 503,
"audit_records": 2
},
{
"name": "same_container_restart",
"status": "PASS",
"network": "none",
"published_ports": {},
"documents": 2,
"vector_canaries": 1,
"provider_calls": 0,
"container_id": "57a0029efb63af2d2f7ae4651e111444b7757b8ab3c9a2b877f5695731d430b1",
"hybrid_search_enabled": false,
"hybrid_ask_enabled": false,
"search_http_status": 503,
"ask_http_status": 503,
"audit_records": 4
},
{
"name": "remove_recreate_same_volumes_empty_scope_on",
"status": "PASS",
"network": "none",
"published_ports": {},
"documents": 2,
"vector_canaries": 1,
"provider_calls": 0,
"container_id": "5708fdffdbc2fa6fc3c5136ddb985857d289d05f62eb6605fe151ae22f87ef69",
"hybrid_search_enabled": true,
"hybrid_ask_enabled": true,
"search_http_status": 200,
"search_result": "empty results",
"ask_http_status": 200,
"ask_result": "no_context",
"audit_records": 6
},
{
"name": "recreate_configuration_rollback_off",
"status": "PASS",
"network": "none",
"published_ports": {},
"documents": 2,
"vector_canaries": 1,
"provider_calls": 0,
"container_id": "65fb0850ff81f45859f8b105e2982aa4c174861bec37c911a6c097751c7bbd96",
"hybrid_search_enabled": false,
"hybrid_ask_enabled": false,
"search_http_status": 503,
"ask_http_status": 503,
"audit_records": 8
},
{
"name": "final_guarded_image_default_off_recheck",
"status": "PASS",
"network": "none",
"published_ports": {},
"documents": 2,
"vector_canaries": 1,
"provider_calls": 0,
"container_id": "3aef54544ada721714bd45ccbe903acaf1b2a3821004150a7b73b08b2ead6ec7",
"image_id": "sha256:3a6991f9dae4ed2f127a97aa2afc818cb72d171220a9abddfd87c781d6069fbc",
"hybrid_search_enabled": false,
"hybrid_ask_enabled": false,
"search_http_status": 503,
"ask_http_status": 503,
"audit_records": 10
}
],
"checks_held_across_successful_phases": [
"live loopback HTTP",
"migration head/integrity",
"JWT/role/tenant scope",
"SQL/document file checksums",
"Chroma canary (not retrieval quality)",
"Hybrid empty-scope/disabled behavior",
"audit persistence",
"zero tickets/agent actions"
],
"persistence_checks": {
"sqlite_integrity": "ok",
"documents_metadata_and_checksums": "unchanged for both tenants",
"uploaded_file_content": "fixed fictional source retained",
"chroma": "one directly inserted fixed 2D vector and document retained; separate canary collection, not embedding/index/retrieval quality",
"initial_audit_ids": "retained across restart/recreation",
"ticket_agent_tables": "ticket, agent_runs, approval_requests, tool_calls all zero"
},
"local_orchestration_note": "One verify exec was issued before the asynchronous docker restart command completed and exited 1 without a result. The command was awaited before running the successful verification; no reseed, no failed exec counted as PASS.",
"offline_verification": {
"passed": 659,
"failed": 0,
"skipped": 0,
"duration_seconds": 72.99,
"environment": "Windows Python 3.12.4; temporary DB/dummy keys; external Python socket and private project data guards"
},
"cleanup": {
"owned_container_removed": true,
"owned_synthetic_volumes_removed": 3,
"label": "copilot.acceptance=20261018-6b56b3b",
"business_data_removed": false,
"synthetic_data_reproducible_from_seed_script": true,
"dedicated_image_retained_for_reproduction": true,
"global_prune_run": false
},
"pending": [
"Original Compose configuration and host-loopback port access; real .env intentionally not loaded",
"Non-empty document index/Hybrid retrieval/classification/generation through a running server (earlier real-provider checks were isolated ASGI)",
"Functional Dense answer fallback, rather than only restoring disabled Hybrid configuration",
"Independent semantic-quality acceptance; v5.3.1 version-audit candidate remains offline and partially effective"
],
"non_actions": [
"No paid model calls, business documents, TechQA/DEV, real credentials, public deployment or new ticket/agent action",
"No primary checkout edits, existing container/volume changes, persistent .env edits or candidate-policy promotion",
"Frozen prompts and historical real-provider records unchanged"
]
}
120 changes: 120 additions & 0 deletions scripts/smoke_local_persistence.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
"""Live container-loopback checks; disposable offline volumes ONLY, no model calls."""

import argparse
import hashlib
import json
import os
from pathlib import Path
import sqlite3
import time

import httpx

STATE = Path("/app/data/runtime_acceptance_state.json")
TEXT = "Fictional CedarDesk local persistence canary. No model or ticket action."
COLLECTION = "runtime_acceptance_canary"


def run(seed=False):
if not __debug__:
raise RuntimeError("Acceptance checks require assertions enabled; do not use -O")
# ponytail: use Docker's network-none isolation, not a new mock-provider stack.
assert Path("/.dockerenv").exists() and {p.name for p in Path("/sys/class/net").iterdir()} == {"lo"}
expected = {"DATABASE_URL": "sqlite:////app/data/acceptance.db", "SQL_ECHO": "false",
"DOCUMENT_STORAGE_ROOT": "/app/storage/documents", "PYTHON_DOTENV_DISABLED": "1",
"DASHSCOPE_API_KEY": "offline-acceptance-not-a-key", "DASHSCOPE_BASE_URL": "http://127.0.0.1:9/v1",
"DASHSCOPE_PHASE_B_API_KEY": "offline-acceptance-not-a-key",
"DASHSCOPE_PHASE_B_BASE_URL": "http://127.0.0.1:9/v1", "ANONYMIZED_TELEMETRY": "False"}
assert all(os.getenv(key) == value for key, value in expected.items()), "Disposable environment required"
with httpx.Client(base_url="http://127.0.0.1:8000", trust_env=False, timeout=5) as api:
for _ in range(40):
try:
if api.get("/health").status_code == 200:
break
except httpx.RequestError:
pass
time.sleep(0.5)
else:
raise RuntimeError("Live server did not become healthy")

def request(method, path, status=200, **kwargs):
response = api.request(method, path, **kwargs)
assert response.status_code == status, f"{method} {path}: HTTP {response.status_code}"
return response.json()

headers = {}
for user, password in (("support", "support"), ("support_other", "support"), ("employee", "employee")):
token = request("POST", "/auth/token", json={"username": user, "password": password})["access_token"]
headers[user] = {"Authorization": "Bearer " + token}
request("GET", "/documents", 401)
request("GET", "/documents", 403, headers=headers["employee"])

from services.document_service import get_document_chroma_collection
collection = get_document_chroma_collection(collection_name=COLLECTION)
if seed:
assert not STATE.exists() and collection.count() == 0, "Refusing to reseed existing state"
state = {"documents": []}
for user in ("support", "support_other"):
assert request("GET", "/documents", headers=headers[user])["total"] == 0
doc = request("POST", "/documents/upload", 201, headers=headers[user], data={"category": "it"},
files={"file": ("runtime_canary.md", TEXT.encode(), "text/markdown")})
state["documents"].append({"user": user, "document": doc})
collection.add(ids=["synthetic-persistence-canary"], embeddings=[[1.0, 0.0]], documents=[TEXT])
else:
state = json.loads(STATE.read_text(encoding="utf-8"))

for item in state["documents"]:
user, original = item["user"], item["document"]
doc = request("GET", "/documents/" + original["id"], headers=headers[user])
assert doc == original and doc["status"] == "uploaded" # Deliberately no embedding/index call.
path = Path(doc["source_path"]).resolve()
assert path.is_relative_to(Path("/app/storage/documents"))
assert path.read_text(encoding="utf-8") == TEXT
assert hashlib.sha256(path.read_bytes()).hexdigest() == doc["checksum"]
other = "support_other" if user == "support" else "support"
request("GET", "/documents/" + doc["id"], 404, headers=headers[other])
vector = collection.get(ids=["synthetic-persistence-canary"], include=["documents", "embeddings"])
assert vector["ids"] == ["synthetic-persistence-canary"] and vector["documents"] == [TEXT]
assert vector["embeddings"].tolist() == [[1.0, 0.0]]

enabled = os.getenv("RAG_HYBRID_SEARCH_ENABLED") == "true"
search = request("POST", "/rag/search", 200 if enabled else 503, headers=headers["support"],
json={"query": "Fictional empty scope", "category": "other", "retrieval_mode": "hybrid"})
if enabled:
assert search["results"] == [] and search["retrieval_mode"] == "hybrid"
ask_enabled = enabled and os.getenv("RAG_HYBRID_ASK_ENABLED") == "true"
answer = request("POST", "/rag/ask", 200 if ask_enabled else 503, headers=headers["support"],
json={"question": "Fictional empty scope", "category": "other", "retrieval_mode": "hybrid",
"refusal_policy": "v5_3_experimental"})
if ask_enabled:
assert answer["retrieval_status"] == "no_context" and answer["sources"] == []
logs = request("GET", "/agent-ops/retrieval-logs", headers=headers["support"])
ids = {row["id"] for row in logs}
assert len(ids) >= 2 and all(row["tenant_id"] == "tenant_demo" for row in logs)
if seed:
state["initial_audit_ids"] = sorted(ids)
STATE.write_text(json.dumps(state), encoding="utf-8")
else:
assert set(state["initial_audit_ids"]) <= ids

with sqlite3.connect("/app/data/acceptance.db") as db:
from alembic.config import Config
from alembic.script import ScriptDirectory
assert {row[0] for row in db.execute("select version_num from alembic_version")} == set(
ScriptDirectory.from_config(Config("alembic.ini")).get_heads())
assert db.execute("pragma integrity_check").fetchone() == ("ok",)
assert db.execute("select count(*) from documents").fetchone() == (2,)
for table in ("ticket", "agent_runs", "approval_requests", "tool_calls"):
assert db.execute(f"select count(*) from {table}").fetchone() == (0,)
return {"status": "PASS", "phase": "seed" if seed else "verify", "network": "none",
"hybrid_search_enabled": enabled, "hybrid_ask_enabled": ask_enabled,
"documents": 2, "vector_canaries": 1, "audit_records": len(ids), "provider_calls": 0,
"checks": ["live loopback HTTP", "migration head/integrity", "JWT/role/tenant scope",
"SQL/document file checksums", "Chroma canary (not retrieval quality)",
"Hybrid empty-scope/disabled behavior", "audit persistence", "zero tickets/agent actions"]}


if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--seed", action="store_true", help="Seed once into brand-new disposable volumes")
print(json.dumps(run(parser.parse_args().seed)))
28 changes: 28 additions & 0 deletions tests/test_docker_build_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""Static safety contract; not evidence that Docker build/runtime was executed."""

from pathlib import Path
import subprocess
import sys

import pytest


def test_build_context_excludes_secrets_and_runtime_mounts():
root = Path(__file__).resolve().parents[1]
rules = set((root / ".dockerignore").read_text(encoding="utf-8").splitlines())
assert {".env", ".env.*", "!.env.example", ".git/", "data/", "storage/", "experiments/chroma_db/",
"docker_data/", "docker_storage/", "docker_chroma_db/"} <= rules


def test_live_check_refuses_non_disposable_environment_before_http(monkeypatch):
from scripts import smoke_local_persistence as smoke
monkeypatch.setattr(Path, "exists", lambda _: False)
monkeypatch.setattr(smoke.httpx, "Client", lambda **_: pytest.fail("Unsafe environment reached HTTP"))
with pytest.raises(AssertionError):
smoke.run()


def test_live_check_refuses_optimized_python_before_http():
result = subprocess.run([sys.executable, "-O", "-m", "scripts.smoke_local_persistence"],
capture_output=True, text=True, timeout=30)
assert result.returncode != 0 and "require assertions enabled" in result.stderr
Loading