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
20 changes: 20 additions & 0 deletions doc/architecture/SPEC/kb_service.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
只有 auto_merge 仓库才有非停止类写动作,shadow 与 disabled 一律不发布;
`close`/`open_revert_pr` 总可执行)。
- `cli`:`infermatrix-copilot kb keygen|status|pause|resume|control`。
- `reconcile`:负责人已合并的初始化/管理知识历史的显式接收;固定目标与 active、GitHub PR/merger/当前检查、
完整 first-parent 提交清单、目标树的两个校验器及真实快照验证,签名计划经重新验证后才安装不可变回执。

## 不变量
- 模块不出现任何仓库名;仓库差异只来自 adapter 配置。
Expand Down Expand Up @@ -296,6 +298,24 @@ issue(同一报告只开一个),标签不存在时不带标签重试,绝
- `kb status` 新增 `unknown_commits`:未处置的未知提交及其接受请求的状态。
- 测试:`test_kb_accept_unknown.py`。

## 2026-10-06 负责人已合并历史的监督接收

- `kb reconcile-reviewed --plan FILE --target SHA --allow-merger LOGIN --require-check NAME --reason TEXT`
核验当前 main 的完整 SHA 与 active 的连续 first-parent 历史;所有知识提交都绑定唯一真实已合并 PR、
merge SHA、父提交、完整原始 diff 哈希、允许的 merger、认证操作员和实际 review 状态。
指定检查须在精确 PR head 上当前成功;同一 app/context 的较早失败重试不覆盖最新结果。
- 在目标树的独立临时导出中执行两个必须存在且成功的知识校验器;导出不能遗漏或改变 git 树的任何知识 blob,
被服务的 Markdown/YAML 必须为普通 UTF-8 文件。构建并加载临时快照,验证格式、所有文件哈希、树和路由。
- 计划按 `kb-reconciliation-plan` 签名但不授予信任。`--apply FILE` 持有激活锁并重新核验所有证据,
main/active 或证据变化即拒绝;仅两个 pin 均未改变时重复 apply 复用同一回执。
- 回执按不同用途 `kb-reviewed-reconciliation` 签名,包含操作员/理由/检查/校验器及快照哈希,
在 `reconciliations/<内容哈希>.json` 通过 fsync 和硬链接原子创建,禁止覆盖和符号链接。
审计和激活每次重新验证签名、仓库及精确提交清单;控制记录直接读取回执,不在 `trusted_merges` 缓存监督信任。
缺失回执使提交恢复未知,损坏回执拒绝激活/控制续签,后续新增知识提交仍保持未知。
- 保持 `accept-unknown` 和自动质量门不变;不伪造 `disposed:`,不更改暂停或发布模式。
有待异步接受、撤回或原始 intake 候选的提交拒绝进入此路径。`kb status` 显示已核验的 `supervised_commits`。
- 测试:`test_kb_reconcile_reviewed.py`。

## 2026-09-29 一致性冲突只算本次改动的
一致性评审看整个 owner 目录,但只有**涉及本次改动的规则**(新增、编辑、退役、被替代的规则 ID)的冲突才使变更失败;
两条都未被改动的规则之间的冲突已经在 main 上,记入该目录的 `preexisting`,不计入变更(精炼也无法修复它,否则该目录
Expand Down
6 changes: 5 additions & 1 deletion doc/architecture/SPEC/knowledge_service.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,11 @@ reported as reconciliation.

`signing` provides Ed25519 envelopes whose signature covers a purpose tag plus
the canonical JSON payload, so a signature made for one purpose (gate verdict,
outbox item, control record, publisher ack) never verifies for another. It
outbox item, control record, publisher ack, reconciliation plan, supervised
reconciliation receipt) never verifies for another. Plans use
`kb-reconciliation-plan`; only revalidated receipts use the distinct
`kb-reviewed-reconciliation` purpose, and neither can be replayed as an
automatic gate verdict or publisher action. It
needs only `cryptography`.

## 2026-09-28 verdict
Expand Down
24 changes: 24 additions & 0 deletions doc/features/knowledge-service.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,30 @@ infermatrix-copilot kb rollback --to <sha> # 回指到较早的快照(下一
合并精确的撤回 PR 并 `kb resume` 后恢复,被撤回的内容会作为新候选重新过门禁;也可以用 `kb accept-unknown <sha>` 让该提交本身完整通过门禁后保留它(通过才算处置,仍需 `kb resume`)。发版巡检在上游新 release 后运行
T1/T2/T3 与 purge,每个规则页一个变更集,全部经过质量门。

初始化、增加 owner 目录、修改顶层入口或校验器时,负责人合并的 PR 可能包含普通自动规则门禁不接受的路径。
`kb reconcile-reviewed` 是这种已合并历史的显式操作员接收流程:以服务密钥签名,先生成可检查的计划,再重新核验后写入不可变回执。
要求当前 main 的完整 SHA、当前 active 的连续 first-parent 历史、每个知识提交对应的唯一真实已合并 PR、
允许的负责人 merger 与认证操作员,以及精确 PR head 上指定的当前成功检查。保留实际 review 状态,不虚构 APPROVED。
在固定目标树上必须通过两个知识校验器与真实快照加载、格式、哈希和路由检查;缺失、失败或进行中的检查均拒绝。

```bash
infermatrix-copilot kb reconcile-reviewed --plan reconciliation.json \
--target <full-main-sha> --allow-merger <owner-login> --require-check suite \
--reason 'Owner-authorized recovery of merged initialization and administration'
# 检查计划中的提交、PR、操作员、检查和校验哈希后,再明确执行:
infermatrix-copilot kb reconcile-reviewed --apply reconciliation.json
infermatrix-copilot kb status
infermatrix-copilot kb resume --all
infermatrix-copilot kb activate
```

需提供 `KB_GITHUB_READ_TOKEN` 以核验认证操作员(沿用负责人的现有只读 API 凭据)。计划本身不产生信任;
apply 重新核验所有证据,main 或 active 改变、证据变化时要求新计划。两个 pin 不变时重复 apply 返回同一回执。
回执在 `$KB_STATE_DIR/reconciliations/`,应随恢复备份保留;审计、激活和控制记录每次直接校验其签名与完整提交范围,
不缓存此类信任。新知识提交仍会阻塞激活;删除或破坏回执不能续签旧信任。
接收不写 `disposed:`,不解除暂停,不更改 shadow/auto_merge,也不替代自动候选的完整质量门。
已有异步接受、撤回或待 intake 候选的提交必须先处理完,不能由此流程绕过。

评审服务读取知识:将 `KNOWLEDGE_ROOT` 指向 `$KB_STATE_DIR/active`。


Expand Down
16 changes: 12 additions & 4 deletions src/infermatrix_copilot/kb_service/audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ def disposals(rt) -> dict[str, str]:


def _records(rt) -> dict:
from .reconcile import trusted_commits

reviewed = trusted_commits(rt)
merge_shas, merged_heads, pending = set(), set(), set()
for lifecycle in rt.registry.values():
for changeset in rt.ledger.changesets(lifecycle.repo, _RECORDED):
Expand All @@ -98,7 +101,7 @@ def _records(rt) -> dict:
if changeset.get("head_sha"):
pending.add(changeset["head_sha"])
disposed = disposals(rt)
return {"merges": merge_shas, "heads": merged_heads, "pending": pending,
return {"merges": merge_shas | reviewed, "reviewed": reviewed, "heads": merged_heads, "pending": pending,
"disposed": disposed, "disposing": set(disposed.values())}


Expand Down Expand Up @@ -126,8 +129,10 @@ def _knowledge_paths(rt, sha: str, parents: list[str]) -> list[str]:
def open_unknown(rt) -> list[str]:
"""Unknown commits not disposed of yet."""
disposed = disposals(rt)
from .reconcile import trusted_commits
reviewed = trusted_commits(rt)
return sorted(name[len(UNKNOWN):] for name in rt.ledger.cursors_with_prefix("*", UNKNOWN)
if name[len(UNKNOWN):] not in disposed)
if name[len(UNKNOWN):] not in disposed and name[len(UNKNOWN):] not in reviewed)


def provenance_problems(rt, sha: str) -> list[str]:
Expand All @@ -151,7 +156,9 @@ def provenance_problems(rt, sha: str) -> list[str]:
def publish_trusted(rt) -> None:
"""What the publisher may treat as trusted on main (control record)."""
recs = _records(rt)
trusted = sorted(recs["merges"] | set(recs["disposed"]) | recs["disposing"])[-TRUSTED_KEEP:]
# Supervised trust is never cached: control publication verifies its
# durable receipts directly, so removal/corruption cannot renew stale trust.
trusted = sorted((recs["merges"] - recs["reviewed"]) | set(recs["disposed"]) | recs["disposing"])[-TRUSTED_KEEP:]
rt.ledger.set_cursor("*", TRUSTED, json.dumps(trusted))


Expand Down Expand Up @@ -264,12 +271,13 @@ def _issue_pending_reverts(rt, main: str) -> None:
if not getattr(rt, "lease_owner", None):
return
disposed = disposals(rt)
remaining = set(open_unknown(rt))
for name, raw in sorted(rt.ledger.cursors_with_prefix("*", UNKNOWN).items(),
key=lambda item: json.loads(item[1]).get("at", 0)):
record = json.loads(raw)
sha = name[len(UNKNOWN):]
lifecycle = rt.registry.get(record.get("repo") or "")
if record.get("state") != "revert_pending" or lifecycle is None or sha in disposed:
if record.get("state") != "revert_pending" or lifecycle is None or sha in disposed or sha not in remaining:
continue
record.update(_issue_revert(rt, lifecycle, sha, record, main))
rt.ledger.set_cursor("*", name, json.dumps(record))
Expand Down
47 changes: 44 additions & 3 deletions src/infermatrix_copilot/kb_service/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import json
import os
import re
import subprocess
import sys
import time
from pathlib import Path
Expand Down Expand Up @@ -90,16 +91,17 @@ def _outbox(state_dir: Path, ledger):
return Outbox(state_dir, _signing_key(), ledger, clock=time.time)


def _unknown_status(ledger) -> dict:
def _unknown_status(ledger, reviewed: set[str] | None = None) -> dict:
"""Unknown knowledge commits not disposed of, with any accept request."""
from .accept import ACCEPT
from .audit import DISPOSED, UNKNOWN

disposed = {n[len(DISPOSED):] for n in ledger.cursors_with_prefix("*", DISPOSED)}
reviewed = reviewed or set()
out = {}
for name, raw in ledger.cursors_with_prefix("*", UNKNOWN).items():
sha = name[len(UNKNOWN):]
if sha in disposed:
if sha in disposed or sha in reviewed:
continue
record = json.loads(raw)
request = ledger.get_cursor("*", ACCEPT + sha)
Expand Down Expand Up @@ -214,6 +216,14 @@ def main(argv: list[str] | None = None) -> int:
sub.add_parser("control")
accept = sub.add_parser("accept-unknown")
accept.add_argument("sha")
reconcile = sub.add_parser("reconcile-reviewed", help="verify and admit an exact owner-merged knowledge history")
action = reconcile.add_mutually_exclusive_group(required=True)
action.add_argument("--plan", type=Path, help="write a signed plan; does not admit any commit")
action.add_argument("--apply", type=Path, help="revalidate and apply a previously inspected signed plan")
reconcile.add_argument("--target", help="exact current main SHA, required when preparing a plan")
reconcile.add_argument("--allow-merger", action="append", default=[])
reconcile.add_argument("--require-check", action="append", default=[])
reconcile.add_argument("--reason", default="")
run = sub.add_parser("run")
run.add_argument("--playbook", required=True)
run.add_argument("--repo", required=True)
Expand Down Expand Up @@ -333,6 +343,30 @@ def main(argv: list[str] | None = None) -> int:
if args.command in ("init", "widen", "deepen"):
# before any ledger is opened: kb init never touches the service's kb.db
return _init_command(args, _state_dir(args.state_dir))
if args.command == "reconcile-reviewed":
from ..config import Settings
from ..knowledge_service.signing import canonical_json
from .runtime import KbRuntime
from .reconcile import make_plan, apply_plan

rt = KbRuntime.from_env(Settings(), state_dir=_state_dir(args.state_dir))
try:
key = _signing_key()
if args.plan:
envelope = make_plan(rt, key, target=args.target or "", allow_mergers=args.allow_merger,
required_checks=args.require_check, reason=args.reason)
with args.plan.open("xb") as handle:
handle.write(canonical_json(envelope))
print(f"verified reconciliation plan: {args.plan}; no commit admitted")
else:
receipt = apply_plan(rt, json.loads(args.apply.read_text()), key)
print(f"supervised reconciliation receipt: {receipt}; pause and publication modes unchanged")
return 0
except (ValueError, OSError, KeyError, RuntimeError, subprocess.SubprocessError) as exc:
print(str(exc), file=sys.stderr)
return 1
finally:
rt.ledger.close()

state_dir = _state_dir(args.state_dir)
ledger = _ledger(state_dir)
Expand All @@ -341,6 +375,12 @@ def main(argv: list[str] | None = None) -> int:
if _sync_repos(ledger, registry) and os.environ.get("KB_SIGNING_KEY"):
_refresh(state_dir, ledger, registry)
if args.command == "status":
from .reconcile import read_receipts
from .sources import KnowledgeRepo

public = _signing_key().public_key() if (state_dir / "reconciliations").exists() else None
reviewed = read_receipts(KnowledgeRepo(Path(os.environ.get("KB_KNOWLEDGE_CLONE") or
state_dir / "knowledge-repo")), state_dir, public)
report = {
"state_dir": str(state_dir),
"active_snapshot": ledger.active_snapshot(),
Expand All @@ -351,7 +391,8 @@ def main(argv: list[str] | None = None) -> int:
"human_queue": len(ledger.human_queue(row["repo"])) if row["repo"] != "*" else None}
for row in ledger.all_repo_states()
],
"unknown_commits": _unknown_status(ledger),
"unknown_commits": _unknown_status(ledger, reviewed),
"supervised_commits": sorted(reviewed),
}
json.dump(report, sys.stdout, indent=2, sort_keys=True)
print()
Expand Down
6 changes: 6 additions & 0 deletions src/infermatrix_copilot/kb_service/outbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,12 @@ def _control_payload(self) -> dict:
# what the publisher checks main against before every merge: every
# governed knowledge commit since the active snapshot must be trusted
trusted = json.loads(self._ledger.get_cursor("*", "trusted_merges") or "[]")
from .reconcile import read_receipts
from .sources import KnowledgeRepo

knowledge = KnowledgeRepo(Path(os.environ.get("KB_KNOWLEDGE_CLONE") or self.root / "knowledge-repo"))
reviewed = read_receipts(knowledge, self.root, self._key.public_key())
trusted = sorted(set(trusted) | reviewed)
return {"issued_at": self._clock(), "repos": repos,
"provenance": {"active": self._ledger.active_snapshot() or "", "trusted": trusted}}

Expand Down
Loading
Loading