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
37 changes: 37 additions & 0 deletions docs/en/rfcs/2086-core-inference-gates-and-optional-gateway.md
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,42 @@ Gateways/hosts retain exact prepared content and metadata across tool rounds; ch
Embedding. Invalid, expired or unverifiable dependencies stop old reuse. The host explicitly refreshes into a new
context boundary rather than rewriting a historical injection.

### Explaining a rejected reuse in process

Revalidation keeps one decision and one public code per failure. A changed, inactive or unverifiable dependency still
rejects the old binding; nothing repairs it, re-prepares, extends its expiry or follows a new head. In-process callers of
`ScopedContextApplication.validate_reuse` may additionally read a content-free explanation from
`ContextReuseError.invalidation`. The check operation keeps returning `context-refresh-required` and HTTP keeps the
generic invalid-request error, so remote hosts receive no new reason.

| Field | Meaning |
| --- | --- |
| `attribution` | `complete`: every difference between the bound and current evidence is listed. `partial`: listed changes are evidenced, but validation stopped before comparing everything or changes were truncated. `unavailable`: nothing could be established safely. |
| `changes` | Ordered `subject` and `change`; only a root also carries its exact bound `ArtifactRef`. |
| `truncated` | At least one evidenced change was dropped to respect the bounds. |

Only `context-reuse-source-changed` raised by revalidation carries an explanation; every other code, binding error,
access denial and preparation failure carries none. The explanation comes from the same authorized read that decides:

- A bound root that is no longer its active head stops validation at that root. The change is `revised` for an active
root at a newer revision, including one written by a restore, so it does not imply edited content; otherwise it is
the lifecycle state `forgotten`, `merged` or `retired`, which outranks a revision written by the same operation.
Later roots and the root's ancestry are not examined, so the explanation is `partial` and never mentions an
unexamined root.
- When every root is current but the bounded snapshot differs, nodes are compared by identity as `changed`, `added` or
`removed`. A node that is itself a bound root is named; an ancestor or Source is reported only by subject, because
the binding records no root-to-dependency map. Cited code is compared afterwards, so this explanation is `complete`
only when no code was cited.
- A cited file whose digest differs is `code` `changed`, or `removed` when the repository would no longer index it.

Disclosure is limited to identities the caller already received as citations. Explanations contain no ancestor or
Source identity, current revision, merge target, content, digest or path. Missing, unreadable, ineligible or
unauthorized evidence keeps its existing code without an explanation, so an explanation cannot distinguish absent from
denied. An explanation lists at most 16 changes, and its canonical JSON form (sorted keys, UTF-8) is at most 4096 bytes.
Changes are ordered roots, ancestors, Sources, each in bound-snapshot order followed by additions; the bounds drop
trailing changes. Building an explanation reads nothing beyond the existing snapshot and file digests. If it fails, the
explanation is `unavailable` and the original rejection is raised unchanged.

### One check operation with two explicit phases

| Proposed POST path | operationId | Phase |
Expand Down Expand Up @@ -878,6 +914,7 @@ Private Gateway state IPC is not another public context operation.
- Verify one optimization/send owner when SDK and external adapters compose. A caller claiming prior optimization cannot skip checks or trigger a second prepare.
- Verify matching remote/in-process decisions under the same policy; forged plan/hash claims, expired constraints and opaque targets cannot upgrade coverage.
- Verify strict default four-field versus opt-in envelope branches, capability negotiation and explicit rejection of unsupported opt-in.
- Verify in-process revalidation explains only evidenced changes: one changed root of several is named without blaming the others, lifecycle states stay distinct, missing or denied evidence explains nothing, bounds truncate deterministically and an explanation failure keeps the original rejection.
- Reject wrong phases, missing/expired/cross-request request bindings and missing/mislocated historical blocks; verify multiple bindings, no HTTP-200 bypass and no repeated recall in checks.

- Verify Rust UDS and Python in-process/HTTPS decisions for identical DTOs, obsolete generations after Core restart, and no coverage upgrade through opaque third-party mutations.
Expand Down
32 changes: 32 additions & 0 deletions docs/zh/rfcs/2086-core-inference-gates-and-optional-gateway.md
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,37 @@ Core 在实际 select/render/fit 完成后生成完整、有界依赖:
网关/宿主在工具轮次保留准确准备值及其元信息;检查不重新 prepare、搜索或 Embedding。
依赖失效、过期或不能确认时停止旧复用,由宿主显式刷新并建立新上下文边界,不能原地改写旧注入。

### 进程内解释被拒绝的复用

重新验证对每种失败仍只作一个决定、给一个公开代码。依赖变化、失活或无法确认时仍拒绝旧 binding;
不修复、不重新 prepare、不延长期限,也不跟随新 head。`ScopedContextApplication.validate_reuse` 的进程内调用方
可另从 `ContextReuseError.invalidation` 读取不含内容的解释。检查操作仍返回 `context-refresh-required`,
HTTP 仍返回通用 invalid-request 错误,远端宿主不会收到新原因。

| 字段 | 含义 |
| --- | --- |
| `attribution` | `complete`:列出绑定证据与当前证据之间的全部差异。`partial`:所列变化都有证据,但验证在比较完之前停止,或变化被截断。`unavailable`:无法安全确立任何变化。 |
| `changes` | 有序的 `subject` 与 `change`;只有 root 另带其准确的绑定 `ArtifactRef`。 |
| `truncated` | 为满足上限,至少丢弃了一条有证据的变化。 |

只有重新验证抛出的 `context-reuse-source-changed` 带解释;其他代码、binding 错误、访问拒绝和准备阶段失败都不带。
解释来自作出决定的同一次授权读取:

- 绑定的 root 不再是活跃 head 时,验证停在该 root。活跃 root 已有新 revision 时记为 `revised`,
包括 restore 写出的 revision,因此不表示内容被编辑;
否则记为生命周期状态 `forgotten`、`merged` 或 `retired`,状态优先于同一操作写出的 revision。
其后的 root 和该 root 的祖先未检查,因此解释为 `partial`,也不会提到未检查的 root。
- 所有 root 都是当前值但有界快照不同时,按节点身份比较,记为 `changed`、`added` 或 `removed`。
本身是绑定 root 的节点会被点名;祖先或 Source 只报告 subject,因为 binding 不记录 root 到依赖的映射。
引用代码在其后才比较,所以只有未引用代码时该解释才是 `complete`。
- 引用文件摘要不同时记为 `code` `changed`;仓库不再索引该文件时记为 `removed`。

披露仅限调用方已作为引用收到的身份。解释不含祖先或 Source 身份、当前 revision、合并目标、内容、摘要或路径。
缺失、不可读、不合格或未授权的证据保持原有代码且不带解释,因此解释无法区分不存在与被拒绝。
一份解释最多 16 条变化,其规范 JSON 形式(键排序、UTF-8)最多 4096 字节。
变化按 root、祖先、Source 排序,各自按绑定快照顺序,其后为新增项;超出上限时丢弃尾部变化。
生成解释只使用已有快照和文件摘要,不做额外读取;生成失败时解释为 `unavailable`,原拒绝原样抛出。

### 一个检查操作,两个明确阶段

| 提议的 POST 路径 | operationId | 阶段 |
Expand Down Expand Up @@ -805,6 +836,7 @@ UDS 与 HTTPS 使用同一组当前授权、过期/篡改、多历史块与 bloc
- 覆盖 SDK 与外部适配器组合时唯一优化/发送所有者;客户端声称“已优化”不能跳过检查,也不能触发二次 prepare。
- 验证远端检查与进程内相同策略的决策一致,并拒绝伪造计划/hash、失效约束或不透明目标带来的覆盖升级。
- 验证默认四字段与 opt-in envelope 的严格分支、能力协商和不支持选项的明确拒绝。
- 验证进程内重新验证只解释有证据的变化:多个 root 中只点名变化的那个、生命周期状态彼此区分、缺失或被拒绝的证据不作解释、上限按确定顺序截断、解释失败时保持原拒绝。
- 验证错误 phase、缺失/过期/跨请求 request_binding、历史块缺失/错误位置与多块绑定,不能以 HTTP 200 绕过决策;检查不重复召回。

- 验证 Rust UDS 与 Python 进程内/HTTPS 相同 DTO 的决策一致,Core 重启后旧 generation 不授予权限;不透明第三方转换不能提升覆盖声明。
Expand Down
27 changes: 23 additions & 4 deletions src/powercontext/builtin/runtime/atomic_memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from dataclasses import dataclass, field, replace
from hashlib import sha256
from time import perf_counter
from typing import Any, cast
from typing import Any, Literal, cast
from uuid import uuid4

import rfc8785
Expand Down Expand Up @@ -119,13 +119,30 @@
MAX_ATOMIC_MEMORY_DEPENDENCIES = 64
MAX_ATOMIC_MEMORY_DEPENDENCY_DEPTH = 8
AtomicMemoryDependencySnapshot = list[dict[str, Any]]
AtomicMemoryRootChange = Literal["revised", "forgotten", "merged", "retired"]

# An active root fails only when its head moved; a lifecycle change outranks the revision it may also have written.
_ROOT_CHANGES: dict[AtomicMemoryStateValue, AtomicMemoryRootChange] = {
AtomicMemoryStateValue.ACTIVE: "revised",
AtomicMemoryStateValue.FORGOTTEN: "forgotten",
AtomicMemoryStateValue.MERGED: "merged",
AtomicMemoryStateValue.RETIRED: "retired",
}


class AtomicMemoryDependencyError(InvalidRuntimeRequestError):
"""Exact current roots or their bounded historical evidence cannot be validated."""
"""Exact current roots or their bounded historical evidence cannot be validated.

``root`` and ``root_change`` are set only when the authorized read found that a requested root is no longer
the active head it was bound at; every other failure leaves them unset.
"""

def __init__(self, reason: str) -> None:
def __init__(
self, reason: str, *, root: ArtifactRef | None = None, root_change: AtomicMemoryRootChange | None = None
) -> None:
self.reason = reason
self.root = root
self.root_change = root_change
super().__init__(f"atomic-memory-{reason}")


Expand Down Expand Up @@ -498,7 +515,9 @@ async def visit(ref: ArtifactRef, depth: int) -> None: # noqa: C901 - distingui
connection, self.scope_id, ref.artifact_id, context
)
if current.ref != ref or current.state is not AtomicMemoryStateValue.ACTIVE:
raise AtomicMemoryDependencyError("source-changed")
raise AtomicMemoryDependencyError(
"source-changed", root=ref, root_change=_ROOT_CHANGES.get(current.state)
)
expected = selected_by_ref.get((ref.artifact_id, ref.revision))
if selected is not None and (
expected is None
Expand Down
Loading
Loading