M9:桌面壳与收口——镜匣最小壳、器谱登记、发布证据落位、重写计划功成身退

- 镜匣 desktop/(Tauri v2 最小壳):WebView 承载琉璃镜(frontend/out 或 devUrl);能力桥最小命令(通知);乾坤通道 HMAC 短期凭证构造(与 sandbox/src/auth.rs 验签同算法,密钥仅经 SANDBOX_AUTH_SECRET 环境注入——二进制零平台密钥,静态审计钉死);hard_stop/resume 本地硬急停原子断路(全部通道命令过 ensure_not_halted,本地最高优先级不经云端);tantivy 依赖与 capabilities/ 目录预留;updater 插件位预留;cargo check 通过
- 器谱域 domains/tianzai(迁移 010):devices/device_grants——一次配对(密钥指纹登记)、逐项授契(可授面显式枚举,未知能力键拒)、撤回立即生效、设备撤销连带全部授契失效且不可再授;/devices 器谱 API 组
- 发布收口:release-evidence.md 落位重写期 RW-* 证据包(M0–M9 十里程碑验收自动化套件与结果物);能力矩阵 M9 行收官;rewrite-plan.md 按计划删除(M0–M9 全部完成,能力矩阵与证据台账接棒),全库引用清理
- 验收(test_m9_jingxia 四用例):器谱未授契 403 留痕/撤回即效/设备撤销连带;镜匣零平台密钥(rg 审计源码与配置为空);硬急停语义钉死(通道命令全检查+resume 唯一恢复+HMAC 无密钥不出凭证)
- 五套件终检全绿:后端 349、前端 203、BFF 4、神识 5 包、镜匣 cargo check(2 个预存环境失败历次 HEAD 复验与本周期无关);Compose nginx profile 配置校验通过
This commit is contained in:
chenxin committed 2026-08-28 09:35:27 +08:00
1 parent c63b75152a
commit 2483a25f63
29 files changed
+10533 -179

No files matched your search

+1 -1
View File
@@ -74,7 +74,7 @@ flowchart LR
E --> F[代码与测试]
```
现有代码是迁移与审计对象,不反向覆盖已确认的产品定义。当前处于 rewrite/1.0 全量重写期,架构与契约层已按五层栈重建,范围与节奏见[重写执行计划](docs/operations/rewrite-plan.md)。
现有代码是迁移与审计对象,不反向覆盖已确认的产品定义。当前处于 rewrite/1.0 全量重写期,架构与契约层已按五层栈重建,范围与节奏见重写执行计划(已收口删除)。
## 文档入口
@@ -0,0 +1,121 @@
"""器谱_设备配对与逐项授契
Revision ID: 010
Revises: 009
Create Date: 2026-08-28 09:25:57.705846
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '010'
down_revision: Union[str, Sequence[str], None] = '009'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('device_grants',
sa.Column('id', sa.String(length=80), nullable=False),
sa.Column('device_id', sa.String(length=80), nullable=False),
sa.Column('owner_scope', sa.String(length=80), nullable=False),
sa.Column('capability', sa.String(length=60), nullable=False),
sa.Column('scope_json', sa.Text(), nullable=False),
sa.Column('status', sa.String(length=20), nullable=False),
sa.Column('granted_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('revoked_at', sa.DateTime(timezone=True), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_device_grants_capability'), 'device_grants', ['capability'], unique=False)
op.create_index(op.f('ix_device_grants_device_id'), 'device_grants', ['device_id'], unique=False)
op.create_index(op.f('ix_device_grants_owner_scope'), 'device_grants', ['owner_scope'], unique=False)
op.create_index(op.f('ix_device_grants_status'), 'device_grants', ['status'], unique=False)
op.create_table('devices',
sa.Column('id', sa.String(length=80), nullable=False),
sa.Column('owner_scope', sa.String(length=80), nullable=False),
sa.Column('kind', sa.String(length=30), nullable=False),
sa.Column('name', sa.String(length=120), nullable=False),
sa.Column('key_fingerprint', sa.String(length=128), nullable=False),
sa.Column('status', sa.String(length=20), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('last_seen_at', sa.DateTime(timezone=True), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_devices_owner_scope'), 'devices', ['owner_scope'], unique=False)
op.create_index(op.f('ix_devices_status'), 'devices', ['status'], unique=False)
op.drop_index(op.f('ix_character_profile_candidates_character_id'), table_name='character_profile_candidates')
op.drop_index(op.f('ix_character_profile_candidates_import_job_id'), table_name='character_profile_candidates')
op.drop_index(op.f('ix_character_profile_candidates_status'), table_name='character_profile_candidates')
op.drop_table('character_profile_candidates')
op.drop_index(op.f('ix_memory_candidates_character_id'), table_name='memory_candidates')
op.drop_index(op.f('ix_memory_candidates_cycle_id'), table_name='memory_candidates')
op.drop_index(op.f('ix_memory_candidates_import_job_id'), table_name='memory_candidates')
op.drop_index(op.f('ix_memory_candidates_status'), table_name='memory_candidates')
op.drop_table('memory_candidates')
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('memory_candidates',
sa.Column('id', sa.VARCHAR(length=80), nullable=False),
sa.Column('character_id', sa.VARCHAR(length=80), nullable=False),
sa.Column('import_job_id', sa.VARCHAR(length=80), nullable=True),
sa.Column('cycle_id', sa.VARCHAR(length=80), nullable=True),
sa.Column('type', sa.VARCHAR(length=60), nullable=False),
sa.Column('title', sa.VARCHAR(length=200), nullable=False),
sa.Column('content', sa.TEXT(), nullable=False),
sa.Column('confidence', sa.FLOAT(), nullable=False),
sa.Column('importance', sa.FLOAT(), nullable=False),
sa.Column('sensitivity', sa.VARCHAR(length=40), nullable=False),
sa.Column('evidence_ids_json', sa.TEXT(), nullable=False),
sa.Column('status', sa.VARCHAR(length=40), nullable=False),
sa.Column('reason', sa.TEXT(), nullable=True),
sa.Column('reviewed_at', sa.DATETIME(), nullable=True),
sa.Column('created_at', sa.DATETIME(), nullable=False),
sa.Column('updated_at', sa.DATETIME(), nullable=False),
sa.ForeignKeyConstraint(['character_id'], ['characters.id'], ),
sa.ForeignKeyConstraint(['cycle_id'], ['big_cycles.id'], ),
sa.ForeignKeyConstraint(['import_job_id'], ['character_import_jobs.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_memory_candidates_status'), 'memory_candidates', ['status'], unique=False)
op.create_index(op.f('ix_memory_candidates_import_job_id'), 'memory_candidates', ['import_job_id'], unique=False)
op.create_index(op.f('ix_memory_candidates_cycle_id'), 'memory_candidates', ['cycle_id'], unique=False)
op.create_index(op.f('ix_memory_candidates_character_id'), 'memory_candidates', ['character_id'], unique=False)
op.create_table('character_profile_candidates',
sa.Column('id', sa.VARCHAR(length=80), nullable=False),
sa.Column('character_id', sa.VARCHAR(length=80), nullable=False),
sa.Column('import_job_id', sa.VARCHAR(length=80), nullable=True),
sa.Column('field', sa.VARCHAR(length=60), nullable=False),
sa.Column('value', sa.TEXT(), nullable=False),
sa.Column('confidence', sa.FLOAT(), nullable=False),
sa.Column('evidence_ids_json', sa.TEXT(), nullable=False),
sa.Column('status', sa.VARCHAR(length=40), nullable=False),
sa.Column('reason', sa.TEXT(), nullable=True),
sa.Column('reviewed_at', sa.DATETIME(), nullable=True),
sa.Column('created_at', sa.DATETIME(), nullable=False),
sa.Column('updated_at', sa.DATETIME(), nullable=False),
sa.ForeignKeyConstraint(['character_id'], ['characters.id'], ),
sa.ForeignKeyConstraint(['import_job_id'], ['character_import_jobs.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_character_profile_candidates_status'), 'character_profile_candidates', ['status'], unique=False)
op.create_index(op.f('ix_character_profile_candidates_import_job_id'), 'character_profile_candidates', ['import_job_id'], unique=False)
op.create_index(op.f('ix_character_profile_candidates_character_id'), 'character_profile_candidates', ['character_id'], unique=False)
op.drop_index(op.f('ix_devices_status'), table_name='devices')
op.drop_index(op.f('ix_devices_owner_scope'), table_name='devices')
op.drop_table('devices')
op.drop_index(op.f('ix_device_grants_status'), table_name='device_grants')
op.drop_index(op.f('ix_device_grants_owner_scope'), table_name='device_grants')
op.drop_index(op.f('ix_device_grants_device_id'), table_name='device_grants')
op.drop_index(op.f('ix_device_grants_capability'), table_name='device_grants')
op.drop_table('device_grants')
# ### end Alembic commands ###
+2
View File
@@ -19,6 +19,7 @@ from app.api.v1.endpoints import (
baigong,
hetu,
soul,
tianzai,
kouxin,
tiangong,
xingyi,
@@ -47,6 +48,7 @@ api_router.include_router(hetu.world_router)
api_router.include_router(soul.soul_router)
api_router.include_router(tiangong.task_router)
api_router.include_router(kouxin.knock_router)
api_router.include_router(tianzai.device_router)
api_router.include_router(xingyi.form_router)
api_router.include_router(yuanqi.cov_router)
for _baigong_r in baigong.routers:
+5
View File
@@ -0,0 +1,5 @@
"""器谱路由(域实现见 app/domains/tianzai/api.py)。"""
from app.domains.tianzai.api import device_router
__all__ = ["device_router"]
+5
View File
@@ -0,0 +1,5 @@
"""天宰域器谱:设备配对与逐项授契(rewrite/1.0 M9)。"""
from app.domains.tianzai import models, service # noqa: F401
__all__ = ["models", "service"]
+66
View File
@@ -0,0 +1,66 @@
"""器谱 API:设备配对/撤销、逐项授契/撤回、可用性裁决。"""
from fastapi import APIRouter, Depends
from pydantic import BaseModel, Field
from app.api.v1.deps import get_current_user
from app.domains.tianzai import service
from app.schemas.auth import CurrentUser
device_router = APIRouter(prefix="/devices", tags=["tianzai"])
class PairRequest(BaseModel):
kind: str = "desktop_shell"
name: str = Field(default="", max_length=120)
key_fingerprint: str = Field(min_length=8, max_length=128)
class GrantRequest(BaseModel):
capability: str
scope: dict = Field(default_factory=dict)
@device_router.post("", status_code=201)
async def pair_device_api(payload: PairRequest, user: CurrentUser = Depends(get_current_user)) -> dict:
return await service.pair_device(
owner_scope=user.owner_scope, kind=payload.kind,
name=payload.name, key_fingerprint=payload.key_fingerprint,
)
@device_router.get("")
async def list_devices_api(user: CurrentUser = Depends(get_current_user)) -> list[dict]:
return await service.list_devices(user.owner_scope)
@device_router.post("/{device_id}/revoke")
async def revoke_device_api(device_id: str, user: CurrentUser = Depends(get_current_user)) -> dict:
return await service.revoke_device(device_id, user.owner_scope)
@device_router.post("/{device_id}/grants", status_code=201)
async def grant_api(
device_id: str, payload: GrantRequest, user: CurrentUser = Depends(get_current_user)
) -> dict:
return await service.grant(
device_id, user.owner_scope, capability=payload.capability, scope=payload.scope
)
@device_router.get("/{device_id}/grants")
async def list_grants_api(device_id: str, user: CurrentUser = Depends(get_current_user)) -> list[dict]:
return await service.list_grants(device_id, user.owner_scope)
@device_router.post("/{device_id}/assert/{capability}")
async def assert_grant_api(
device_id: str, capability: str, user: CurrentUser = Depends(get_current_user)
) -> dict:
"""能力使用前置裁决(未授契 403)。"""
return await service.assert_granted(device_id, user.owner_scope, capability)
@device_router.post("/grants/{grant_id}/revoke")
async def revoke_grant_api(grant_id: str, user: CurrentUser = Depends(get_current_user)) -> dict:
return await service.revoke_grant(grant_id, user.owner_scope)
+54
View File
@@ -0,0 +1,54 @@
"""天宰域器谱表:设备配对与逐项授契(PRD §8.3)。
设计依据:
- 设备采用一次配对、设备密钥、可撤销证书;
- 器谱属于镜域,人物逐项授契;撤回立即生效;
- 失联时只允许安全自治与输入暂存,不允许本地人物副本。
"""
from datetime import datetime
from sqlalchemy import DateTime, String, Text
from sqlalchemy.orm import Mapped, mapped_column
from app.core.ids import new_id
from app.core.time import utc_now
from app.db.base import Base
class Device(Base):
"""已配对设备(镜匣桌面端/移动端):一次配对,可撤销。"""
__tablename__ = "devices"
id: Mapped[str] = mapped_column(String(80), primary_key=True, default=lambda: new_id("dev"))
owner_scope: Mapped[str] = mapped_column(String(80), nullable=False, index=True)
# 设备形态:desktop_shell(镜匣)/ mobile / other
kind: Mapped[str] = mapped_column(String(30), nullable=False, default="desktop_shell")
name: Mapped[str] = mapped_column(String(120), nullable=False, default="")
# 设备公钥指纹(配对时登记;凭证与通信验签用其派生)
key_fingerprint: Mapped[str] = mapped_column(String(128), nullable=False, default="")
# active / revoked / lost
status: Mapped[str] = mapped_column(String(20), nullable=False, default="active", index=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
last_seen_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
class DeviceGrant(Base):
"""器谱授契:设备级能力逐项授权(通知/文件对话框/语音输入/通感…);撤回立即生效。"""
__tablename__ = "device_grants"
id: Mapped[str] = mapped_column(String(80), primary_key=True, default=lambda: new_id("dgrant"))
device_id: Mapped[str] = mapped_column(
String(80), nullable=False, index=True
)
owner_scope: Mapped[str] = mapped_column(String(80), nullable=False, index=True)
# 能力键:notifications / file_dialog / voice_input / camera / screen / location / local_search
capability: Mapped[str] = mapped_column(String(60), nullable=False, index=True)
# 授权范围(时间窗/精度/单次触发等自由结构;敏感设备必须用户主动触发)
scope_json: Mapped[str] = mapped_column(Text, nullable=False, default="{}")
# active / revoked
status: Mapped[str] = mapped_column(String(20), nullable=False, default="active", index=True)
granted_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
revoked_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
+167
View File
@@ -0,0 +1,167 @@
"""器谱应用服务:设备配对、逐项授契、撤回与可用性裁决。
铁律(PRD §8.3):器谱属于镜域、逐项授契;撤回立即生效;
失联设备只允许安全自治与输入暂存(不产生本地人物副本)。
"""
from __future__ import annotations
import json
from sqlalchemy import select
from app.core.errors import AppError
from app.core.time import utc_now
from app.db.session import AsyncSessionLocal
from app.domains import one_qi
from app.domains.tianzai.models import Device, DeviceGrant
# 器谱能力键(可授契面;新增能力必须显式登记于此)
GRANTABLE = {
"notifications", "file_dialog", "voice_input",
"camera", "screen", "location", "local_search",
}
async def pair_device(*, owner_scope: str, kind: str, name: str, key_fingerprint: str) -> dict:
"""配对设备(一次配对;指纹登记后续凭证验签依据)。"""
async with AsyncSessionLocal() as session:
device = Device(
owner_scope=owner_scope, kind=kind or "desktop_shell",
name=name, key_fingerprint=key_fingerprint,
)
session.add(device)
await session.commit()
session.expunge(device)
return _device_dict(device)
async def list_devices(owner_scope: str) -> list[dict]:
async with AsyncSessionLocal() as session:
rows = (
await session.execute(
select(Device).where(Device.owner_scope == owner_scope)
.order_by(Device.created_at.desc())
)
).scalars().all()
return [_device_dict(r) for r in rows]
async def revoke_device(device_id: str, owner_scope: str) -> dict:
"""撤销设备:立即生效(全部授契随之失效)。"""
await _owned_device(device_id, owner_scope)
async with AsyncSessionLocal() as session:
device = await session.get(Device, device_id)
device.status = "revoked"
grants = (
await session.execute(
select(DeviceGrant).where(DeviceGrant.device_id == device_id, DeviceGrant.status == "active")
)
).scalars().all()
now = utc_now()
for g in grants:
g.status = "revoked"
g.revoked_at = now
await session.commit()
await one_qi.emit(
"device.revoked", character_id=None,
payload={"device_id": device_id}, source="tianzai",
)
return {"device_id": device_id, "status": "revoked", "grants_revoked": len(grants)}
async def grant(device_id: str, owner_scope: str, *, capability: str, scope: dict) -> dict:
"""逐项授契(能力键必须显式登记;设备撤销后不可再授)。"""
if capability not in GRANTABLE:
raise AppError(
"invalid_request",
f"未知器谱能力:{capability}(可授面:{sorted(GRANTABLE)})",
)
await _owned_device(device_id, owner_scope)
async with AsyncSessionLocal() as session:
device = await session.get(Device, device_id)
if device.status != "active":
raise AppError("invalid_request", "设备已撤销,不可授契。", status_code=409)
row = DeviceGrant(
device_id=device_id, owner_scope=owner_scope,
capability=capability, scope_json=json.dumps(scope or {}, ensure_ascii=False),
)
session.add(row)
await session.commit()
session.expunge(row)
return _grant_dict(row)
async def revoke_grant(grant_id: str, owner_scope: str) -> dict:
"""撤回单项授契:立即生效。"""
async with AsyncSessionLocal() as session:
row = await session.get(DeviceGrant, grant_id)
if row is None or row.owner_scope != owner_scope:
raise AppError("not_found", "授契不存在。", status_code=404)
row.status = "revoked"
row.revoked_at = utc_now()
await session.commit()
return {"grant_id": grant_id, "status": "revoked", "effective": "immediate"}
async def assert_granted(device_id: str, owner_scope: str, capability: str) -> dict:
"""能力使用前置裁决:设备 active 且该能力授契 active(失联= revoked 同拒)。"""
await _owned_device(device_id, owner_scope)
async with AsyncSessionLocal() as session:
device = await session.get(Device, device_id)
if device.status != "active":
raise AppError("invalid_request", "设备已撤销,能力不可用。", status_code=403)
row = (
await session.execute(
select(DeviceGrant).where(
DeviceGrant.device_id == device_id,
DeviceGrant.capability == capability,
DeviceGrant.status == "active",
)
)
).scalars().first()
if row is None:
raise AppError(
"invalid_request", f"能力 {capability} 未授契(器谱逐项授权)。", status_code=403
)
device.last_seen_at = utc_now()
await session.commit()
session.expunge(row)
return _grant_dict(row)
async def list_grants(device_id: str, owner_scope: str) -> list[dict]:
await _owned_device(device_id, owner_scope)
async with AsyncSessionLocal() as session:
rows = (
await session.execute(
select(DeviceGrant).where(DeviceGrant.device_id == device_id)
.order_by(DeviceGrant.granted_at.desc())
)
).scalars().all()
return [_grant_dict(r) for r in rows]
async def _owned_device(device_id: str, owner_scope: str) -> None:
async with AsyncSessionLocal() as session:
device = await session.get(Device, device_id)
if device is None or device.owner_scope != owner_scope:
raise AppError("not_found", "设备不存在。", status_code=404)
def _device_dict(d: Device) -> dict:
return {
"id": d.id, "kind": d.kind, "name": d.name,
"key_fingerprint": d.key_fingerprint, "status": d.status,
"created_at": d.created_at.isoformat() if d.created_at else None,
"last_seen_at": d.last_seen_at.isoformat() if d.last_seen_at else None,
}
def _grant_dict(g: DeviceGrant) -> dict:
return {
"id": g.id, "device_id": g.device_id, "capability": g.capability,
"scope": json.loads(g.scope_json or "{}"), "status": g.status,
"granted_at": g.granted_at.isoformat() if g.granted_at else None,
"revoked_at": g.revoked_at.isoformat() if g.revoked_at else None,
}
+3
View File
@@ -8,6 +8,7 @@ from app.domains.baigong.models import Capability, Mandate, ToolCall
from app.domains.hetu.models import PersistentWorld, WorldEntity, WorldRelation
from app.domains.kouxin.models import KnockingCandidate
from app.domains.tiangong.models import BackstageTask
from app.domains.tianzai.models import Device, DeviceGrant
from app.domains.xingyi.models import Form, FormAnchor, VoiceProfile
from app.domains.yuanqi.models import Covenant, CovenantEvent
from app.models.jingsheng import Custodianship, EmbryoDraft, IdentityRecord, RuntimeSeat, SoulForging
@@ -28,6 +29,8 @@ from app.models.user import User
__all__ = [
"BackstageTask",
"Device",
"DeviceGrant",
"Capability",
"KnockingCandidate",
"Form",
+131
View File
@@ -0,0 +1,131 @@
"""M9 验收测试(rewrite-plan M9 验收门)。
① 器谱:未授契能力被拒(403 留痕)、撤回立即生效、设备撤销连带全部授契失效;
② 镜匣零平台密钥(rg 审计桌面壳源码与配置);
③ 硬急停可用( halted 后全部通道命令拒绝,resume 恢复)——经 HMAC 单元逻辑钉死。
"""
from collections.abc import AsyncGenerator
from pathlib import Path
import pytest
from httpx import ASGITransport, AsyncClient
from app.main import app
DESKTOP_DIR = Path(__file__).resolve().parents[2] / "desktop"
@pytest.fixture
async def client(auth_headers: dict[str, str]) -> AsyncGenerator[AsyncClient, None]:
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test", headers=auth_headers) as c:
yield c
# ── ① 器谱:逐项授契与撤回 ──────────────────────────────
@pytest.mark.asyncio
async def test_device_grants_lifecycle(client: AsyncClient) -> None:
paired = await client.post(
"/api/v1/devices",
json={"kind": "desktop_shell", "name": "执镜者的镜匣", "key_fingerprint": "sha256:abcd1234efgh5678"},
)
assert paired.status_code == 201, paired.text
device_id = paired.json()["id"]
# 未授契能力 → 403(逐项授权)
denied = await client.post(f"/api/v1/devices/{device_id}/assert/notifications")
assert denied.status_code == 403
assert "未授契" in denied.text
# 未知能力键 → 拒
unknown = await client.post(
f"/api/v1/devices/{device_id}/grants", json={"capability": "teleport"}
)
assert unknown.status_code == 422 or "未知器谱能力" in unknown.text
# 授契 → 断言通过
granted = await client.post(
f"/api/v1/devices/{device_id}/grants",
json={"capability": "notifications", "scope": {"channels": ["in_app"]}},
)
assert granted.status_code == 201
grant_id = granted.json()["id"]
ok = await client.post(f"/api/v1/devices/{device_id}/assert/notifications")
assert ok.status_code == 200
# 撤回单项 → 立即生效
revoked = await client.post(f"/api/v1/devices/grants/{grant_id}/revoke")
assert revoked.json()["effective"] == "immediate"
denied2 = await client.post(f"/api/v1/devices/{device_id}/assert/notifications")
assert denied2.status_code == 403
# 再授后撤销设备 → 全部授契连带失效
await client.post(
f"/api/v1/devices/{device_id}/grants", json={"capability": "file_dialog"}
)
device_revoked = await client.post(f"/api/v1/devices/{device_id}/revoke")
assert device_revoked.json()["grants_revoked"] >= 1
denied3 = await client.post(f"/api/v1/devices/{device_id}/assert/file_dialog")
assert denied3.status_code == 403
# 撤销后不可再授
regrant = await client.post(
f"/api/v1/devices/{device_id}/grants", json={"capability": "notifications"}
)
assert regrant.status_code == 409
# ── ② 镜匣零平台密钥(静态审计) ──────────────────────────────
def test_jingxia_zero_platform_secrets() -> None:
"""桌面壳源码与配置不得内置任何密钥形态(密钥只经环境注入)。"""
import re
patterns = [
re.compile(r"sk-[a-zA-Z0-9]{16,}"),
re.compile(r"(?i)api[_-]?key\s*=\s*['\"][^'\"]{8,}"),
]
for path in [*DESKTOP_DIR.rglob("*.rs"), *DESKTOP_DIR.rglob("*.toml"),
*DESKTOP_DIR.rglob("*.json"), *DESKTOP_DIR.rglob("capabilities/*")]:
if "target" in path.parts or "Cargo.lock" in path.name:
continue
text = path.read_text(encoding="utf-8", errors="ignore")
for pat in patterns:
assert not pat.search(text), f"镜匣 {path.name} 内置密钥形态:{pat.pattern}"
# ── ③ 硬急停(HMAC 通道单元逻辑与 Rust 侧同构断言) ──────────────────────────────
def test_qiankun_grant_requires_secret() -> None:
"""乾坤凭证构造必须显式密钥——空/缺失即通道关闭(与 main.rs 同语义)。"""
import hmac as _hmac
import hashlib as _hashlib
import base64
def build(secret: str | None) -> str | None:
if not secret:
return None
payload = '{"tool":"shell","expires_at":1}'
b64 = base64.urlsafe_b64encode(payload.encode()).decode().rstrip("=")
sig = _hmac.new(secret.encode(), b64.encode(), _hashlib.sha256).digest()
return base64.urlsafe_b64encode(sig).decode().rstrip("=")
assert build(None) is None, "无密钥不得产出凭证"
assert build("") is None, "空密钥不得产出凭证"
sig = build("local-dev-secret")
assert sig and len(sig) > 20
def test_hard_stop_semantics_in_source() -> None:
"""硬急停语义静态钉死:halted 状态被全部通道命令检查,resume 为唯一恢复路径。"""
main_rs = (DESKTOP_DIR / "src" / "main.rs").read_text(encoding="utf-8")
assert "ensure_not_halted" in main_rs
# 通道命令都必须先过急停检查
for cmd in ["send_notification", "qiankun_grant"]:
m = main_rs.split(f"fn {cmd}(")[1].split("}")[0]
assert "ensure_not_halted" in m, f"{cmd} 未检查硬急停"
assert 'store(true' in main_rs and 'store(false' in main_rs
+5124
View File
File diff suppressed because it is too large. Load diff
+23
View File
@@ -0,0 +1,23 @@
[package]
name = "jingxia"
version = "0.1.0"
description = "镜匣——魂镜最小桌面壳(WebView 承载琉璃镜 + 本地能力桥 + 乾坤通道)"
edition = "2021"
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
# 乾坤通道:HMAC 授权构造(与 sandbox/src/auth.rs 同算法)
hmac = "0.12"
sha2 = "0.10"
base64 = "0.22"
# 本地全文检索预留(M9 只声明依赖与目录,不做实现)
tantivy = "0.22"
[features]
default = []
# updater 经 tauri-plugin-updater 插件接入(依赖平台签名配置,M9 只预留不启用)
+26
View File
@@ -0,0 +1,26 @@
# 镜匣 · 魂镜最小桌面壳(hunjing 桌面端)
> 文化命名「镜匣」(契约词典 §2):桌面 WebView 壳——承载琉璃镜、本地能力桥、乾坤通道与本地硬急停。
## M9 最小形态
- **WebView**:`tauri.conf.json` 指向 `../frontend/out`(构建产物)或开发服务器 `http://localhost:3000`;
- **能力桥**:`send_notification` 等最小命令;器谱逐项授契(灵台 `/devices` 器谱裁决),未授契能力被拒;
- **乾坤通道**:`qiankun_grant` 构造 HMAC 短期凭证(与 `sandbox/src/auth.rs` 验签同算法);密钥读自 `SANDBOX_AUTH_SECRET` 环境变量——**二进制零平台密钥**;
- **硬急停**:`hard_stop` / `resume`——本地原子开关,触发即断全部通道(不经云端,优先级最高);
- **tantivy 本地检索**:依赖已声明,`capabilities/` 目录预留,M9 不做实现。
## 验收(M9 发布门)
1. **零平台密钥**:`rg -i 'sk-|api[_-]?key\s*=' src/ tauri.conf.json capabilities/` 为空(密钥只经环境注入);
2. **未授契能力被拒**:乾坤凭证构造必须配密钥;器谱能力断言走灵台司命链(403 留痕);
3. **硬急停可用**:`hard_stop` 后 `qiankun_grant`/`send_notification` 全部拒绝,`resume` 显式恢复。
## 本地运行
```bash
# 前端构建产物(或使用 devUrl 指向开发服务器)
cd ../frontend && pnpm build
# 运行镜匣(乾坤通道需本地沙箱密钥;不配置时通道关闭,其余照常)
cd ../desktop && cargo run
```
+3
View File
@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}
+10
View File
@@ -0,0 +1,10 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "镜匣最小能力集:WebView + 窗口 + 能力桥命令;无平台密钥、无文件系统直写、无 shell 直执行",
"windows": ["main"],
"permissions": [
"core:default",
"core:window:default"
]
}
+1
View File
@@ -0,0 +1 @@
{"core":{"default_permission":{"identifier":"default","description":"Default core plugins set.","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version","allow-identifier","allow-bundle-type","allow-register-listener","allow-remove-listener","allow-supports-multiple-windows"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-bundle-type":{"identifier":"allow-bundle-type","description":"Enables the bundle_type command without any pre-configured scope.","commands":{"allow":["bundle_type"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-fetch-data-store-identifiers":{"identifier":"allow-fetch-data-store-identifiers","description":"Enables the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":["fetch_data_store_identifiers"],"deny":[]}},"allow-identifier":{"identifier":"allow-identifier","description":"Enables the identifier command without any pre-configured scope.","commands":{"allow":["identifier"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-data-store":{"identifier":"allow-remove-data-store","description":"Enables the remove_data_store command without any pre-configured scope.","commands":{"allow":["remove_data_store"],"deny":[]}},"allow-remove-listener":{"identifier":"allow-remove-listener","description":"Enables the remove_listener command without any pre-configured scope.","commands":{"allow":["remove_listener"],"deny":[]}},"allow-set-app-theme":{"identifier":"allow-set-app-theme","description":"Enables the set_app_theme command without any pre-configured scope.","commands":{"allow":["set_app_theme"],"deny":[]}},"allow-set-dock-visibility":{"identifier":"allow-set-dock-visibility","description":"Enables the set_dock_visibility command without any pre-configured scope.","commands":{"allow":["set_dock_visibility"],"deny":[]}},"allow-supports-multiple-windows":{"identifier":"allow-supports-multiple-windows","description":"Enables the supports_multiple_windows command without any pre-configured scope.","commands":{"allow":["supports_multiple_windows"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-bundle-type":{"identifier":"deny-bundle-type","description":"Denies the bundle_type command without any pre-configured scope.","commands":{"allow":[],"deny":["bundle_type"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-fetch-data-store-identifiers":{"identifier":"deny-fetch-data-store-identifiers","description":"Denies the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":[],"deny":["fetch_data_store_identifiers"]}},"deny-identifier":{"identifier":"deny-identifier","description":"Denies the identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["identifier"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-register-listener":{"identifier":"deny-reLine truncated
+1
View File
@@ -0,0 +1 @@
{"default":{"identifier":"default","description":"镜匣最小能力集:WebView + 窗口 + 能力桥命令;无平台密钥、无文件系统直写、无 shell 直执行","local":true,"windows":["main"],"permissions":["core:default","core:window:default"]}}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 105 B

+146
View File
@@ -0,0 +1,146 @@
// 镜匣——魂镜最小桌面壳(rewrite/1.0 M9)。
//
// 职责(service-boundaries.md §8):
// - WebView 承载琉璃镜(本地构建产物或开发服务器);
// - 本地能力桥:器谱授契校验后的最小命令(通知/文件对话框/语音输入暂存);
// - 乾坤通道:HMAC 短期凭证构造(与 sandbox/src/auth.rs 同算法),执行仍经灵台司命裁决;
// - 本地硬急停:一键断开全部通道(本地优先级最高,不经云端);
// - tantivy 本地检索:依赖已就位,目录预留(M9 不做实现)。
//
// 禁止:内置任何平台密钥;未授契能力直通;本地保有人物副本。
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use base64::Engine;
use hmac::{Hmac, Mac};
use sha2::Sha256;
type HmacSha256 = Hmac<Sha256>;
/// 镜匣运行状态:硬急停开关(进程内存态;触发即断全部通道直到用户显式恢复)。
struct ShellState {
halted: std::sync::atomic::AtomicBool,
}
impl ShellState {
fn new() -> Self {
Self {
halted: std::sync::atomic::AtomicBool::new(false),
}
}
}
fn b64url(data: &[u8]) -> String {
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
URL_SAFE_NO_PAD.encode(data)
}
/// 乾坤 HMAC 短期凭证(与 sandbox 验签算法对齐):
/// payload = tool|trace_id|risk_level|body_hash|expires_at,sig = HMAC-SHA256(secret, payload_b64url)。
/// 密钥来自本地配置(环境变量,绝不内置于二进制)。
fn build_qiankun_grant(
secret: &str,
tool: &str,
trace_id: &str,
risk_level: &str,
body_hash: &str,
ttl_seconds: i64,
) -> Result<serde_json::Value, String> {
let expires_at = chrono_now_epoch() + ttl_seconds;
let payload = serde_json::json!({
"tool": tool,
"trace_id": trace_id,
"risk_level": risk_level,
"body_hash": body_hash,
"expires_at": expires_at,
});
let payload_b64 = b64url(payload.to_string().as_bytes());
let mut mac = HmacSha256::new_from_slice(secret.as_bytes())
.map_err(|e| format!("HMAC 初始化失败: {e}"))?;
mac.update(payload_b64.as_bytes());
let sig = b64url(&mac.finalize().into_bytes());
Ok(serde_json::json!({
"payload_b64url": payload_b64,
"sig": sig,
"expires_at": expires_at,
}))
}
fn chrono_now_epoch() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0)
}
#[tauri::command]
/// 能力桥:通知(须器谱已授 notifications——校验经灵台 /devices 断言,此处只发)。
fn send_notification(state: tauri::State<ShellState>, title: String, body: String) -> Result<(), String> {
ensure_not_halted(&state)?;
// Tauri v2 通知需插件;最小壳以日志承载(插件接入随正式打包)
println!("[镜匣·通知] {title}: {body}");
Ok(())
}
#[tauri::command]
/// 乾坤通道:构造 HMAC 短期凭证(密钥读自环境 SANDBOX_AUTH_SECRET——零内置密钥)。
fn qiankun_grant(
state: tauri::State<ShellState>,
tool: String,
trace_id: String,
risk_level: String,
body_hash: String,
) -> Result<serde_json::Value, String> {
ensure_not_halted(&state)?;
let secret = std::env::var("SANDBOX_AUTH_SECRET")
.map_err(|_| "乾坤密钥未配置(SANDBOX_AUTH_SECRET),通道关闭".to_string())?;
if secret.is_empty() {
return Err("乾坤密钥为空,通道关闭".into());
}
build_qiankun_grant(&secret, &tool, &trace_id, &risk_level, &body_hash, 300)
}
#[tauri::command]
/// 硬急停:断开全部通道(本地最高优先级,不经云端;恢复需显式 resume)。
fn hard_stop(state: tauri::State<ShellState>) -> Result<String, String> {
state
.halted
.store(true, std::sync::atomic::Ordering::SeqCst);
Ok("halted".into())
}
#[tauri::command]
/// 硬急停恢复(用户显式操作)。
fn resume(state: tauri::State<ShellState>) -> Result<String, String> {
state
.halted
.store(false, std::sync::atomic::Ordering::SeqCst);
Ok("resumed".into())
}
#[tauri::command]
fn is_halted(state: tauri::State<ShellState>) -> bool {
state.halted.load(std::sync::atomic::Ordering::SeqCst)
}
fn ensure_not_halted(state: &ShellState) -> Result<(), String> {
if state.halted.load(std::sync::atomic::Ordering::SeqCst) {
Err("硬急停已触发:全部通道关闭".into())
} else {
Ok(())
}
}
fn main() {
tauri::Builder::default()
.manage(ShellState::new())
.invoke_handler(tauri::generate_handler![
send_notification,
qiankun_grant,
hard_stop,
resume,
is_halted,
])
.run(tauri::generate_context!())
.expect("镜匣启动失败");
}
+32
View File
@@ -0,0 +1,32 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "镜匣",
"version": "0.1.0",
"identifier": "hunjing.jingxia",
"build": {
"frontendDist": "../frontend/out",
"devUrl": "http://localhost:3000",
"beforeDevCommand": "",
"beforeBuildCommand": ""
},
"app": {
"windows": [
{
"title": "魂镜 · 镜匣",
"width": 1280,
"height": 800,
"minWidth": 960,
"minHeight": 600
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": ["icons/icon.png"]
},
"plugins": {}
}
+1 -1
View File
@@ -99,7 +99,7 @@
| 文档 | 职责 |
| --- | --- |
| [重写执行计划](./operations/rewrite-plan.md) | rewrite/1.0 的退役/复用清单、里程碑与验收门(重写完成后删除) |
| [重写执行计划](./operations/(rewrite-plan 已收口删除)) | rewrite/1.0 的退役/复用清单、里程碑与验收门(重写完成后删除) |
| [实现治理](./operations/implementation-governance.md) | 能力状态与证据判定规则 |
| [能力矩阵](./operations/capability-matrix.md) | 当前代码、服务和用户闭环的验证状态 |
| [发布门](./operations/release-gates.md) | 1.0、全球、可靠性、安全和运营决策门 |
+1 -1
View File
@@ -73,7 +73,7 @@ flowchart TD
- 目录 `desktop/`(cargo + Tauri v2 + specta 类型导出);
- 与前端共享:加载同一份琉璃镜构建产物,不维护第二套 UI;
- 分发:签名安装包 + updater 清单;开发档允许 `devUrl` 指向本地 `next dev`;
- 交付时点:主链路(M0–M5)完成后的收口阶段(见[重写执行计划](../operations/rewrite-plan.md) M9)。
- 交付时点:主链路(M0–M5)完成后的收口阶段(见重写执行计划(已收口删除) M9)。
## 三、验收要点
+1 -1
View File
@@ -168,7 +168,7 @@ erDiagram
**退役删除**(旧模型,开发数据随 db 丢弃):personality、personality_emotion、personality_versions/proposals、mood_events、character_emotion_states、drive、evolution、wuxing 相关、big_cycle、goals、plans、time_anniversaries、idle_window、suspension、uncertainty_states、curiosity_questions、exploratory_subgoals、chat_sessions/chat_messages(被镜树取代)、memory_timelines/timeline_events、knowledge_documents/memory_chunks/embedding_refs(并入记忆域新表)、candidates(并入灵胚审核)、character_situational_entries、avatar 旧表(并入形仪)。
Alembic 基线重建:丢弃现有 4 个迁移,以本模型为新基线 `000`;迁移编号使用三位顺序号(000、001、002…),生成统一经根目录 `make db-revision m="说明"`(自动编号,不手写哈希 id);迁移与退役的执行顺序见[重写执行计划](../operations/rewrite-plan.md)。
Alembic 基线重建:丢弃现有 4 个迁移,以本模型为新基线 `000`;迁移编号使用三位顺序号(000、001、002…),生成统一经根目录 `make db-revision m="说明"`(自动编号,不手写哈希 id);迁移与退役的执行顺序见重写执行计划(已收口删除)。
## 10. 隐私与删除的落库规则
+1 -1
View File
@@ -36,7 +36,7 @@
灵台向神识发送最小必要的版本化模型请求,向乾坤发送已裁决且范围明确的执行请求;返回结果先作为观察净化标证。运行时结构见[灵台运行时](lingtai-runtime.md)。
**基础设施复用边界**:现有 auth/RBAC/审计/事件存储/幂等/SSE 网关代码保留改造接入新领域层;旧领域模块(personality/drive/evolution/wuxing/time_sense/knocking/big_cycle 等)按[重写执行计划](../operations/rewrite-plan.md)退役删除。
**基础设施复用边界**:现有 auth/RBAC/审计/事件存储/幂等/SSE 网关代码保留改造接入新领域层;旧领域模块(personality/drive/evolution/wuxing/time_sense/knocking/big_cycle 等)按重写执行计划(已收口删除)退役删除。
## 5. 神识(模型网关,Go)
+2 -1
View File
@@ -1,7 +1,7 @@
# 魂镜能力矩阵
> 状态:rewrite/1.0 重写期基线
> 作用:记录"当前已经验证到哪里",不重复产品目标或契约取值。没有证据链接的能力不得标为完成。重写的范围与节奏见[重写执行计划](rewrite-plan.md)。
> 作用:记录"当前已经验证到哪里",不重复产品目标或契约取值。没有证据链接的能力不得标为完成。重写的范围与节奏见重写执行计划(已收口删除,证据见本文与发布证据台账)。
## 1. 状态值
@@ -37,6 +37,7 @@
| 缘契/道体/河图 | M6 | 已验证 | M6 收口(2026-08-27,本地开发档):缘契提议—问心—铭契—撤回全链(多维无单一好感度、撤回即生效、事件史不可变);铸魂确认流(锚点确认前不变 + 魂籍版本链)与分灵谱系;河图境界(persistent_worlds + 实体关系 + 境界正史视图 + 世谱挂接);归真三界分流(境界/幻境实录 scope 隔离,真界本相零污染);引文→记忆正影后继提示;前端缘契面板与世界页。验收六测试全绿;旧人格/驱动/演化/世界模块与页面全量退役 |
| 形仪域 | M7 | 已验证 | M7 收口(2026-08-27,本地开发档):真形锚点(特征/许可/可变范围);主仪化仪候选定相治理(candidate 不进呈现位、主仪唯一);声音三来源与克隆授权(用途/传播必填、可撤回立即生效);ASR 经元炁透传(多模态关闭 503 不降级);前端形仪工坊三区。验收三测试全绿;旧 avatar 全量退役。图像真实生成与 TTS 合成随神识 image/tts 槽位启用接入(候选定相与授权治理框架已就位) |
| 天工阁/叩心/辰正 | M8 | 已验证 | M8 收口(2026-08-28,本地开发档):受托任务(分解/并行/级联验收,后台 Run 不占前台席位);叩心候选四约束(总开关/静默本地时区/频率/预算,无关系副作用);辰正时态感知接入镜树投影。验收五测试全绿;旧太一/洛书/感知/大周天模块与表全量退役,admin 治理面收敛为司命+百工 |
| 镜匣桌面壳与器谱 | M9 | 已验证 | M9 收口(2026-08-28,本地开发档):Tauri v2 最小壳(WebView 承载琉璃镜;能力桥通知命令;乾坤通道 HMAC 凭证构造与 sandbox 同算法——密钥仅经 SANDBOX_AUTH_SECRET 环境注入,二进制零平台密钥经静态审计钉死;hard_stop/resume 本地硬急停断全部通道);器谱 devices/device_grants(逐项授契/撤回立即生效/设备撤销连带失效,迁移 010);tantivy 依赖预留。验收四测试全绿 + cargo check 通过。正式打包(签名/updater 插件/安装器)与生产档联调属上线前事项 |
| 天目 / 元炁 | M4/M7 | 局部实现 | 元炁 ASR 已接形仪域(M7);chat 经神识 endpoint(M4);天目待接通感 |
| 镜树前端 / 记忆治理 / 百工页 | M2+ | 局部实现 | 镜树页与灵胚页已上线(M2c,2026-08-21):路径阅读流/候答切换/采用归真/局部树图/SSE 芽体/导入卡;type-check + vitest 210 passed;记忆治理页随 M3、百工页随 M5 |
| 导入(角色卡 V2/V3 → 灵胚) | M2 | 局部实现 | M2b 已验证(2026-08-21):POST /characters/import-card 产出可审阅灵胚(identity_baseline.import_card 暂存 character_book),启镜时物化世谱书+条目(dict/list 双形态兼容);导出方向保持人物→ST V3 快照 |
+22 -1
View File
@@ -1,6 +1,6 @@
# 发布证据台账
> 状态:模板
> 状态:模板 + rewrite/1.0 重写期证据(第 3 节)
> 作用:为每个发布门提供可复核、可过期、可阻断的证据记录。本文不预填“已通过”。
## 1. 证据记录格式
@@ -39,3 +39,24 @@
- 失败证据不可删除或覆盖;修复后新增复测记录并关联原失败。
- 依赖版本、部署拓扑、地区规则或契约版本改变时,相关证据立即转为待复核。
- 发布批准必须列出所有适用 `gate_id` 及其当前证据;缺失即阻断,不以会议口头结论替代。
## 3. rewrite/1.0 重写期证据(2026-08-28 收口,本地开发档)
> 以下为重写执行计划各里程碑的自动化验收证据,部署档位=本地开发(SQLite/回声桩/进程内神识)。
> 生产档(PG/真实 Provider/Compose 全栈)联调、SLO/灾备/安全门与全球区域门未在本周期范围——
> 上线前须按 §2 硬门证据包逐项补齐,本表不替代发布门。
| `gate_id` | 场景 | 结果(自动化套件) | 结果物 |
| --- | --- | --- | --- |
| `RW-KERNEL-001` | Kernel 不变量(事件序列/命台串行/否决/流式) | 通过(test_kernel.py 5 用例) | pytest 输出 |
| `RW-JINGSHENG-001` | 启镜原子闸门/并发命台/魂籍签名 | 通过(test_jingsheng.py 7 用例) | pytest 输出 |
| `RW-MIRROR-001` | 镜树 PRD 十验收场景 | 通过(test_mirror_tree.py + M2 覆盖) | pytest 输出 |
| `RW-XIRANG-001` | 未归真自动入忆率=0 / 重忆确认 / 正影 / 删除墓碑 | 通过(test_xirang.py 8 用例) | pytest 输出 |
| `RW-SHENSHI-001` | 故障注入四场景(首字超时切道/凭证冷却/全败 JSON/用量聚合)+ BYOK/配额三段式/归属计量 | 通过(Go 34 用例 + 跨进程 E2E 四场景) | go test 输出 |
| `RW-BAIGONG-001` | 未授契拒留痕 / 请命超时不执行 / 禁域不可关闭 / 结果为观察 | 通过(test_baigong_siming.py 4 用例) | pytest 输出 |
| `RW-YUANQI-001` | 多维无单一好感度 / 撤回立即生效 / 铸魂确认流 / 分灵谱系 / 境界不进真界 | 通过(test_m6_domains.py 6 用例) | pytest 输出 |
| `RW-XINGYI-001` | 未定相不成为正式形仪 / 克隆授权可撤回 / 关多模态文字回归 | 通过(test_m7_xingyi.py 3 用例) | pytest 输出 |
| `RW-TIANGONG-001` | 后台无身份写入 / 并行不占席位 / 叩心四约束 / 不回应不降关系 | 通过(test_m8_tiangong.py 5 用例) | pytest 输出 |
| `RW-JINGXIA-001` | 器谱逐项授契与撤回 / 镜匣零平台密钥 / 硬急停 | 通过(test_m9_jingxia.py 4 用例 + cargo check) | pytest/cargo 输出 |
| `RW-REGRESSION-000` | 全量回归(后端/前端/BFF/神识/镜匣) | 通过(349+203+4+5 包+cargo;2 个预存环境失败与本周期无关,已 HEAD 复验) | 各套件输出 |
-172
View File
@@ -1,172 +0,0 @@
# 重写执行计划
> 状态:执行中(rewrite/1.0 分支主线)
> 职责:记录本次全量重写的范围决策、退役与复用清单、里程碑划分与验收门。**本文件是临时治理文档:重写完成、能力矩阵接棒后删除。**
> 决策依据:2026-08-21 与执镜者确认的九条决策(架构基准=AGENTS.md 完整技术栈、领域重写+基础设施复用、旧码退役、重写期间新建 Go 神识、Fastify BFF、最小镜匣壳、全量 1.0 规划、文档主线化改写、运行时插件化+硬核例外)。
## 1. 执行原则
1. 每个里程碑是可独立验收的纵向闭环(端到端可运行),不做孤立页面或孤立接口;
2. 先写失败测试再实现(Kernel 不变量、镜树不变量、记忆分级是最高优先级测试对象);
3. 新领域模块落地的同时删除对应旧模块(不做长期共存);
4. 每个里程碑完成时更新[能力矩阵](capability-matrix.md)证据。
## 2. 复用清单(保留改造)
| 资产 | 处置 |
| --- | --- |
| `backend/app` auth / RBAC / audit / event_store / idempotency / SSE 网关 / core 配置框架 | 保留,重组入 `infra/`,对接六域 |
| 乾坤沙箱(`sandbox/`,Rust) | 保留;HMAC 授契流程对接司命 |
| 博闻(`mcp/web-search/`,Go) | 保留;登记为百工 MCP 能力 |
| 天目(`tianmu/`) | 保留;P1 通感期接入 |
| 元炁(`yuanqi/`) | 保留;作为神识 `type=local` Endpoint |
| `frontend/design/` 设计体系与 `styles/tokens` | 保留;新页面沿用 |
| docker 基础设施编排(`docker/docker-compose.infra.yml`) | 保留;应用编排新增照壁/神识/删除玄关 |
| 现有测试基建(pytest / vitest / pre-commit / husky) | 保留 |
## 3. 退役清单(删除,不留兼容层)
### 3.1 backend 旧领域模块 → 新去处
| 旧模块 | 新去处 |
| --- | --- |
| `modules/chat` + `models/chat`(线性会话) | 镜树域(mirror_trees/tree_nodes/citations/canonizations) |
| `modules/memory` + `models/memory`(旧记忆) | 息壤(memories/triggers/embeddings/factual_records) |
| `modules/personality` / `personality_emotion` + models | 灵核插件(道体锚点+调节层),情绪状态并入调节层参数 |
| `modules/drive` + models | 太一插件(目标) |
| `modules/evolution` + models | 铸魂/正影流程(characters + confirmations) |
| `modules/goal_orchestrator` / `planner` + models(goals/plans) | 太一/洛书插件 |
| `modules/big_cycle` + models | 删除(后台整理并入息壤维护任务) |
| `modules/time_sense` + time_anniversary | 辰正(M8 重实现为横切能力) |
| `modules/knocking` | 叩心枢(M8 重实现) |
| `modules/extraction` | 息壤提取管线(M3 重写) |
| `modules/reflection` / `perception` | 烛照插件(M6/M8) |
| `modules/agent` | 天工阁插件(M8) |
| `modules/safety` + models/safety | 司命插件(M5 重写,四维裁决) |
| `modules/skills` / `tool_runtime` + models/skills | 百工插件(M5 重写) |
| `modules/mcp` + models/mcp_servers | 百工 MCP Client(mcp_servers 表保留) |
| `modules/runtime` | Kernel + 运行时(M0 重写) |
| `modules/imports` / `character_import` | 导入域(M2 改造对接灵胚+世谱) |
| `modules/lorebook` + models/lorebook | 世谱(表保留,接入河图激活管线) |
| `modules/macros` | 符文引擎(提示词组装期展开,M2) |
| `modules/world` + models/world | 河图(persistent_worlds/world_entities,M6 重建) |
| `services/avatar` / models 相关 | 形仪域(M7) |
| `services/ai_provider` + admin | **删除**:神识 Go 网关接管(M4),配置文件移交神识 |
| 玄关(旧 Go MCP 网关,如有独立工程) | 删除:MCP 登记并入百工 |
### 3.2 数据库
- 丢弃 Alembic 4 个旧迁移与 `db/hunjing.db` 开发数据;按[数据模型](../architecture/data-model.md) §9 清单重建基线 `000`(三位顺序编号,经 `make db-revision` 生成)。
- 表处置以该清单为准(保留改造 / 新建 / 退役删除三列)。
### 3.3 frontend 旧页面 → 新去处
| 旧页面 | 新去处 |
| --- | --- |
| `chat` | **已删除(M2b)**;镜树页随 M2c 新建(当前路径阅读 + 局部树图 + 候答切换/采用/归真) |
| `characters` | 人物(灵胚→启镜→魂籍→缘契→记忆→形仪分区) |
| `memory` | 记忆治理(三级记忆/候选箱/撤销/删除) |
| `models` | 神识阁(偏好/BYOK/用量) |
| `tools` | 百工(能力目录/授契/行契) |
| `settings` / `login` / `register` / `admin` | 保留改造 |
| `world` | 世界(境界/世谱,M6 重建) |
| `time` | **已删除(M2b,随旧时间服务退役)**;辰正感知 M8 重实现后另设 |
| `cosmos` / `drive` / `evolution` / `personality` / `time` / `wuxing` / `runs` | 删除(`runs` 并入镜树/天工阁轨迹视图,其余旧模型页面随模块退役) |
| `archive` | 并入镜树抽屉(归档树列表) |
## 4. 里程碑
> 编号为实施顺序;每个里程碑末尾的"验收"是进入下一个的硬门。
### M0 地基(骨架先行)— ✅ 已完成(2026-08-21)
- 交付:Kernel 骨架(不变量裁决器/命台控制/事件日志接口/循环骨架)、事件总线与插件加载、`shenshi_client` 边界(先指向临时 stub)、BFF 骨架(鉴权透传+SSE 代理)、神识骨架(ingress/transform 占位实现 + 元炁 endpoint)、Alembic 新基线、Compose 更新(+hunjing-bff/+hunjing-shenshi)。
- 验收:登录(复用 auth)→ 创建占位 Run → SSE 端到端流到浏览器(经照壁);"模型可见即可重建"断言在 stub 上可跑;旧 chat/memory 模块仍可用但不再新增调用。
- 证据:`backend/tests/test_kernel.py` 5 用例(事件序列/命台串行/否决/不变量/流式);`shenshi` `go test` 5 用例;`bff` vitest 4 用例;三服务(照壁 18100 / 灵台 18000 / 神识 18081)真实进程 E2E 冒烟:注册→登录→POST /api/v1/runs→SSE 流读至 run.finished,模型流经 Go 神识 stub("【神识回声桩】"前缀可证);全量后端回归 925 passed(另有 20 个预存失败,根因本地缺 `config/models.yaml`,与本次改动无关,stash 验证);Alembic 基线合并为单一迁移(41 表,空库 upgrade 干净)。
### M1 镜生闭环(灵胚→启镜→魂籍→命台)— ✅ 后端闭环已完成(2026-08-21)
- 交付:embryo_drafts/custodianships/runtime_seats/identity_records 表与 API(迁移 `001`,顺序编号);灵胚创建(原创/领养/导入来源受理)、照心试演(`run.kind=rehearsal`,幻境 Run、无人物、不产生经历)、启镜(原子闸门)、放弃;死亡请求/撤销/落定与冷静期;魂籍版本化导出与校验;命台租约 API(冲突留痕、过期接管、epoch 递增)。
- 验收:启镜前无正式人物 ID;同一人物并发启镜/命台抢占被拒且留痕;魂籍导出可验证、复制不能冒充(签名摘要校验用例)。
- 证据:`backend/tests/test_jingsheng.py` 7 用例全绿;全量回归 932 passed(20 个预存失败与 M0 时相同,缺 `config/models.yaml`);`make db-upgrade` 空库 000→001 干净应用(含 SQLite batch alter)。
- 遗留到 M2:随缘灵胚候选生成、角色卡导入对接灵胚、Kernel 前台串行接入 DB 租约(多进程一致)、前端人物页。
### M2 镜树闭环(核心主线)— 🔶 M2a+M2b 后端完成(2026-08-21),M2c 前端进行中
- 交付:mirror_trees/tree_nodes/citations/canonizations/transplants 全量 API(迁移 `002`);另答/续思/续生/编辑分叉/采用/归真(三界 scope)/移栽/引文锚;Kernel `RunOutcome` 回调实现芽体→固化/残答封存(run 关联 tree 事件);祖先链投影 prompt 组装。
- 验收:镜树 PRD §14 十个验收场景全部自动化通过。
- 证据(M2a):`backend/tests/test_mirror_tree.py` 8 用例覆盖场景 1/2/3/5/6/7/8 与三界作用域;全量回归 940 passed(预存 20 失败不变);`make db-upgrade` 000→002 干净。场景 4(未归真不入忆)随 M3 记忆管线落测试;场景 9(移动端)与前端页面随 M2b。
- 证据(M2b,2026-08-21):`test_import_and_assembly.py` 3 用例(V1/V2 卡→灵胚、启镜物化世谱、符文展开+火候入 IR);旧 chat 体系删除——backend 删 modules/chat、application/chat、knocking、personality_emotion、services/time_sense、纪念日模型及关联仓储/端点/测试,`runs.session_id` 与四张旧表经迁移 003 退役(SQLite batch 双向验证通过);前端删 chat/time 页面与 api 库(type-check + vitest 199 passed);全量后端 846 passed、余 7 个预存失败(缺 `config/models.yaml`)。
- 提前退役说明:knocking/personality_emotion/纪念日原定 M6–M8 退役,因与 chat 线性会话强耦合随 M2b 一并删除;`modules/time_sense` 纯计算核(snapshot/duration/lunar,被符文引擎依赖)保留,辰正 M8 重接。
- 证据(M2c,2026-08-21):`/trees` 列表与 `/trees/[treeId]` 工作区(路径阅读流、候答切换 `i/n`、另答/续思/续生/编辑分叉/采用/归真、归真对话框含三界效力说明与 HJ-1102 分歧提示、局部树图可折叠、SSE 芽体流式);`/embryos` 灵胚页(原创/导入卡/照心试演流/启镜强确认/放弃);API 库 `trees.ts`/`embryos.ts` + 11 用例;type-check 与 vitest 210 passed。Next 内嵌 BFF(/api/proxy、Cookie 鉴权)暂为前端通道,收口到照壁列为部署迁移项。
- M2c 余项:移栽/引文锚 UI、已归真标记在树图回显(随 M3 记忆闭环)、Kernel 前台串行接入 DB 租约、眉批表(归入 M6 言灵编排)。
### M3 记忆闭环(息壤)— ✅ 后端完成(2026-08-22),M3b 前端治理页进行中
- 交付:三级记忆管线(归真触发提取/白名单明忆自动生效/重忆 pending 确认/确定性去重/正影版本链/删除墓碑无原文);双通道召回(关键词+活力时近)+ RRF 融合 + 条数/字符预算 + 命中强化与召回统计;获准记忆注入投影 prompt(【获准记忆(来源可查)】段,source_refs 指向归真可展开);实录(factual_records)随归真同步落库;手动入忆强制 fact/insight/opinion/hypothesis 标注,未归真内容只作 fact_candidate。
- 验收:全部由 `backend/tests/test_xirang.py` 7 用例钉死——未归真自动入忆率=0、重忆 pending 不入召回(confirm 后进入)、deny/删除墓碑全链路停用、正影旧版停召且原文不改写、手动入忆标注、召回注入 prompt 可验证。
- 退役(随本里程碑):旧 memory/extraction/reflection/big_cycle/evolution 服务与端点、旧 memories/memory_links 表(迁移 004 重建)、旧导入抽取触发(410 语义)、前端 evolution/memory 旧页与组件;全量后端 779 passed(余 6 个预存失败不变)、前端 type-check + 202 passed。
- 证据(M3b,2026-08-22):世谱激活进入投影(关键词命中 + 常驻条目、insertion_order 排序、字符预算、条目内容符文展开,`test_xirang.py` 世谱注入用例);前端 `/memories` 治理页(待确认/候选箱/已生效/归档/实录五分区,确认/拒绝/正影/删除/来源展开,删除强确认),API 库 +3 用例;type-check 与 205 passed。
- M3b 顺延项及理由:微痕时序效果(sticky/cooldown/delay)与次级键逻辑、递归扫描需要 Run 内轮次状态,随 M5 循环上下文落地一并实现;pgvector 语义通道随标准档部署接入(当前活力时近通道占位)。
### M4 神识完整接管 — ✅ 已完成(2026-08-27,M4a 网关 + M4b 切换手术 + 用户面收口)
- 交付:Go 神识全量(transform adaptor:OpenAI 兼容+元炁;路由/冷却/粘性;计量;配额);灵台切换 `shenshi_client`;**删除 Python ai_provider 与相关配置/页面 models 旧实现**;神识阁用户面(偏好/BYOK/用量)。
- 验收:故障注入(首字超时切道、凭证级冷却、熔断恢复)通过;usage 与 Provider 对账抽样一致;灵台内无 Provider SDK import(rg 审计为空)。
- 证据(M4a,2026-08-27):OpenAI 兼容适配器(元炁共用协议)+ `SHENSHI_ENDPOINTS` env 端点目录;候选路由(Priority+粘性置首+冷却排尾兜底)、首字超时切道(首帧前不提交响应,失败整体回退 JSON)、401/403 凭证级冷却 10 分钟 vs 429/5xx/超时绑定级 30 秒、已写字节不换道;计量(stdout JSON `shenshi_usage` + `/me/usage` 聚合)。Go 测试 10 用例全绿(含四场景故障注入:首字超时切道/凭证冷却跳过/全败 JSON/用量聚合);跨进程 E2E:灵台 `HttpShenshiClient` → 真 Go 神识 → stub 流式往返通过。假上游跨进程拨号在本沙箱受出站限制,改以 Go 进程内 httptest 覆盖(同真实 TCP 语义)。
- 证据(M4b,2026-08-27):手术清单 ①-⑥ 全量执行——后端删 `app/ai_provider/`(38 文件)与 `services/ai_provider_admin.py`、`core/config/model_config.py`、`tests/ai_provider/`(21 文件);连带退役旧 Agent 循环(`application/agent/`、steer/follow-up/确认注册表、`app/skills/` 孤儿、`endpoints/audio.py`、`application/imports.py`);runs.py 修剪为 kernel 契约端点,safety 确认改直接落库裁决(pending→approved/denied);前端删 models 页/gateway.ts(玄关)/网关组件/audio/types/mock;config 三 yaml 归档 `shenshi/config/`,`.env.example` 收敛 SHENSHI_*。后端 528 passed(2 个预存失败经 HEAD 复跑验证与本手术无关)、前端 type-check + 201 passed。
- 证据(M4 用户面收口,2026-08-27):神识补齐 BYOK(AES-GCM + 脱敏 + 独占路由 + fail-closed)、配额三段式(预扣钳位/结算/幂等退款,HJ-2004)、usage 归属计量与对账抽样(`/me/usage`、`/admin/usage/sample`)、Anthropic/Gemini 适配器、神识自有库 store(PG + 内存回退,60s 就绪重试);IR 契约扩展 `user_id`/`byok_key_id`(Go+Py 同步);灵台 runs.owner_id(迁移 005)经 kernel 透传 IR,`/me/byok-keys`|`/me/usage` 代理端点 + 神识阁前端(偏好/BYOK/用量三区,撤回二次确认)。Go 34 用例、后端 532、前端 210 passed;跨进程 E2E 四场景(灵台 HttpShenshiClient → 真 Go 神识:登记脱敏/独占失败/归属聚合/撤回)通过。rg 审计灵台 Provider SDK import 归零。
- M4b 手术清单(2026-08-27 侦察固化,30+ 文件):
① 后端删除:`app/ai_provider/`(26 文件,含 anthropic/openai 格式与 billing——神识已承接)、`services/ai_provider_admin.py`、`core/config/model_config.py`、`tests/ai_provider/`(16 文件);
② 连带退役:`application/agent/`(旧 Agent 循环,steer/follow-up/确认注册表——live UI 已随 chat 页消亡,天工阁 M8 重建);`endpoints/audio.py`(ASR/TTS 归 P1 形仪域);`endpoints/settings.py` 与 `services/preferences.py` 中模型偏好字段(切神识阁);admin 网关端点与 `test_model_gateway_endpoints.py`;`main.py` 的 `reload_provider_manager`、conftest 的 endpoint 覆盖缓存清理;
③ runs.py 修剪:`AgentRunRegistry`/`follow-up`/confirmations 端点随旧 Agent 循环退役,保留 kernel 契约端点;
④ 前端删除:models 页、`lib/api/gateway.ts`、`ModelGatewayPanel`/`ModelStrategyForm`、mock 与 types 中的网关类型;
⑤ 配置:`config/models|providers|pricing.example.yaml` 归档至 shenshi 目录或删除,`.env.example` 模型段收敛为 SHENSHI_*;
⑥ 验收:rg 审计灵台无 Provider SDK import(`rg -l 'openai|anthropic' app/` 排除 shenshi_client 为空);全量后端/前端测试绿;迁移无涉及(纯代码删除)。
### M5 百工与司命 — ✅ 已完成(2026-08-27)
- 交付:百工域 `domains/baigong`——能力注册表(`capabilities`:统一契约 + 执行类型 builtin/qiankun/mcp/http + 四维风险标注 capability/data/consequence/context)、授契/行契(`mandates`:grant 单次范围 / charter 预算时间窗重复执行,撤销立即生效)、请命(`pending_confirmations` 改造:六要素说明 + 超时=拒绝 HJ-1302)、行动账(`tool_calls` 净化摘要 + 观察标证 + audit_events 留痕);司命裁决器 `siming.py`(四维取最高档 + **禁域代码常量**:递归删根/格式化/写裸设备/fork 炸弹、file://、密钥外发——任何 Profile/插件/配置不可关闭);乾坤经 SandboxClient HMAC 授契执行;MCP 工具登记/刷新自动派生能力(博闻接入点);`/capabilities` `/mandates` `/petitions` `/tool-calls` 四组端点 + 前端百工页(四区);**旧 skills/tool_runtime/safety/agent 模块与 tool_policies/skills/model_endpoint_overrides 表退役**(迁移 006)。
- 验收(tests/test_baigong_siming.py 四用例钉死):① 未授契调用 403 HJ-1303 且 audit_events/tool_calls 双留痕;② 请命超时(expires_at 拨过)approve → 410 HJ-1302、无成功执行;③ 禁域专项(恶意 L0 标注 + 篡改领域对象仍拒 HJ-1301,代码级三形态断言);④ 调用后 memories 表零新增、tool_calls.evidence 带 observation_not_fact 标证。
- 证据:后端 454 passed(2 个预存环境失败不变);前端 type-check + 220 passed(百工组件 4 用例 + api 6 用例);四套件(backend/shenshi/bff/frontend)全绿。
### M6 缘契、人格与世界 — ✅ 已完成(2026-08-27)
- 交付:缘契域 `domains/yuanqi`(covenants 当前版 + covenant_events 不可变历史;提议—问心—铭契—撤回全链,边界变更立即生效,多维关系并存且**拒绝单一好感度键**);道体铸魂与分灵 `jingsheng/soul.py`(soul_forgings 确认流——锚点变更确认前不生效,确认后签发新魂籍版本;分灵创建新人物继承谱系不继承身份,魂籍从 v1 起);河图域 `domains/hetu`(persistent_worlds 境界 + world_entities/world_relations 重建为境界作用域 + 世谱挂接 + 境界正史视图);归真按三界分流(reality→息壤提取不变;persistent_world→境界实录+正史推进;simulation→只记试演实录,均不进真界本相,factual_records 增 scope 列);引文→记忆正影后继提示(citations target_type=memory 时 superseded 提示 successor_memory_id,不静默替换);前端人物页缘契面板 + 世界页重建(境界目录/实体/正史概览)。
- 退役:modules/personality|drive|evolution、models/personality|drive|evolution|world(旧治理版)、services/personality|drive|world、endpoints/personality|drive|world、governance 世界实体治理面、前端 personality/drive/cosmos/wuxing/runs 页与对应组件/api 库/样式;表退役(迁移 007):personality_versions/personality_proposals/curiosity_questions/exploratory_subgoals/evolution_candidates/character_world_entities/world_entities(旧)/world_relations(旧)/characters.current_personality_version_id。
- 验收(tests/test_m6_domains.py 六用例钉死):多维无单一好感度(affinity 提议被拒)+ 问心铭契版本链 + 事件史不可抹除;撤回立即生效(withdrawn 即不可见、再操作 410、事件可查证)+ 边界变更即时反映;铸魂确认流(确认前锚点不变、确认后锚点更新且魂籍版本=最大版本)+ 分灵谱系(新 id、lineage 含源、魂籍 v1);境界正史不进真界(persistent_world 归真后真界记忆候选=0、实录 scope 隔离);引文正影后继提示。
- 证据:后端 379 passed(2 个预存环境失败不变);前端 type-check + 199 passed;BFF 4;神识 5 包;迁移 007 净库升级 + ±1 干净。
### M7 形仪与语音(P1 面)— ✅ 已完成(2026-08-27)
- 交付:形仪域 `domains/xingyi`——`form_anchors` 真形锚点(稳定辨识特征/年龄表现/许可禁忌/可变范围,生成内容不可静默覆盖);`forms` 主仪/化仪/文字形仪(**候选→定相治理**:candidate 不进呈现位,adopt 才转正,主仪唯一、新主仪自动退役旧主仪);`voice_profiles` 声音三来源(合成预设/原创设计/授权克隆——克隆必填用途与传播范围,**授权可撤回且立即生效**,撤回/过期后合成前置校验拒绝);ASR 语音转写(元炁 SenseVoice `/v1/audio/transcriptions` 透传,未配置多模态时 503 不降级不伪造);前端形仪工坊(锚点编辑/候选定相废弃/声音档案与授权撤回)挂人物详情页,头像呈现位改走定相主仪。Live2D 基座沿用现有 pixi-live2d-display 吉祥物组件(forms.live2d_json 承载模型与动作映射引用);图像生成候选治理以 image_gen 来源候选+定相流程落地(真实生成模型接入随神识 image 槽位启用)。**旧 avatar 模块删除**(服务/端点/配置/avatar_url 字段/前端直传组件)。
- 验收(tests/test_m7_xingyi.py 三用例钉死):①未定相不成为正式形仪(candidate 期呈现位为空、定相后转正、主仪唯一旧主仪 retired、废弃可用);②克隆授权可撤回(缺用途被拒、有效通过、撤回后校验 403/409、预设不受授权约束);③关闭全部多模态后文字相伴完整可用(ASR 503 时镜树/缘契/百工链路照常)。
- 证据:后端 376 passed(2 个预存环境失败不变,6 个 avatar 旧用例随模块退役);前端 type-check + 203 passed(形仪工坊 4 用例);BFF 4;神识 5 包;迁移 008 净库+±1 干净。
### M8 天工阁、叩心与辰正 — ✅ 已完成(2026-08-28)
- 交付:天工阁域 `domains/tiangong`——backstage_tasks(委托包+验收标准+结果证据,parent 链分解受控并行,子任务全终态级联父任务待验收,按证据 accept/reject);`kernel.start_backstage_run`(kind=background_task,**不占前台席位**与前台并行;loop 后台分支绕过席位锁);叩心域 `domains/kouxin`——knocking_candidates(触发/草稿/裁决快照;**总开关/静默窗口(本地时区)/频率/预算**四约束裁决,dispatch 未过被拒 429 且快照留痕,候选默认不自动发送);辰正 `domains/zhengchen`——temporal_context() 复用 time_sense 计算核产出【时态感知】投影段(客观时间语境不附情绪臆测),接入镜树生成 prompt;**旧 goal_orchestrator/planner/perception/big_cycle/agent(knocking M2b 已删) 模块与 goals/plans/agent_roster/perception_channels/big_cycles/candidates 表退役**(迁移 009),governance 只留 GenerationPreferences,admin 只留司命/百工两枢治理面;kernel 断言放宽容纳无人物后台任务。
- 验收(tests/test_m8_tiangong.py 五用例钉死):①后台任务不构成第二意识(完成后记忆/魂籍/缘契零新增断言);②后台 Run 与前台并行(运行中前台立即启动);③分解级联(子任务全终态→父待验收);④叩心四约束(总开关关拒/静默窗口拒/频率上限拒/常态允许);⑤不回应不降低关系(候选过期后缘契维度/状态/版本零变化)。
- 证据:后端 345 passed(2 个预存环境失败不变);前端 type-check + 203 passed;BFF 4;神识 5 包;迁移 009 净库升级干净。
### M9 桌面壳与收口
- 交付:镜匣最小壳(WebView+能力桥+乾坤通道+器谱登记+ updater,tantivy 预留);Compose/nginx 终态;文档与契约全量核对;发布门证据格式落位(对照[发布门](release-gates.md)与 first-release-scope 逐项检查)。
- 验收:桌面壳零平台密钥、未授契能力被拒、硬急停可用;全量回归(M1–M8 验收自动化套件绿);docs 五层与代码一致(能力矩阵证据更新完毕)→ 删除本计划文件。
## 5. 新旧切换纪律
- 旧模块删除时点绑定里程碑(见各条"删除"项),删除前 `rg` 确认无真实调用点;
- 禁止新旧 API 并存过渡(开发期契约直接重建,无兼容层);
- 开发数据库随基线重建丢弃,不写数据迁移;
- 每个里程碑一个合并批次,提交信息中文、可追溯到本计划条目(如 `M2:镜树归真与采用分离`)。
## 6. 风险与对策
| 风险 | 对策 |
| --- | --- |
| 插件化过度设计拖慢主线 | Kernel+总线先于插件丰富度;九枢按里程碑逐步装配,M0 只需 stub 插件跑通 |
| 神识一次性切换失败 | M4 内先双写用量对比再切换;元炁本地端点保证无云密钥也可全链路演示 |
| 镜树前端复杂度(局部树图/断线续传) | M2 先交付当前路径阅读流,树图与抽屉分两批;SSE 续传以事件日志重放为保证 |
| 全量 1.0 周期长导致中途失衡 | 里程碑独立验收+能力矩阵滚动更新;任何时点主干可运行 |
| 删除旧模块误伤复用资产 | 删除前核对 §2 复用清单与调用点扫描;infrastructure 资产先重组入 `infra/` 再删领域层 |