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
22 changes: 22 additions & 0 deletions docs/architecture/rfcs/agent-loop-effect-interpreter-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,28 @@ shape merely because of its size. Optional memory participation now has one
compact, signed envelope projection; the Codex CLI adapter preserves default-off
isolation. This does not qualify installed heartbeat/App adoption or model value.

Current source-path audit for the first convergence slice:

| Execution fact | Existing owner / projection | Remaining boundary |
|---|---|---|
| Goal/Agent/Todo identity | Quota selection and receipt; envelope actor, selected Todo and signed settlement identity | Capture identity and mutation-time validation remain necessary; display identity is not an execution grant |
| Full requirements | Interaction required reads; compact commands retained verbatim | Shared full Goal/Todo reads are proposed in #5794; neither a summary nor a source hash proves that a host read them |
| Capability refusal | Existing quota `capability_gate_v0`; compact boundary now retains exact `required`/`missing` arrays and historical source names | This repairs omitted facts, not readiness policy or capability activation |
| Selection / claim / lease | Selected Todo, action portfolio and current owning transactions | Compact selected ownership is not a fresh lease; retain the captured source and revalidate at the owning mutation |
| Replan / Goal closure | Replan action packet, contract capsule and vision audit | Full evidence remains on authorized detail paths; Todo completion is not Goal completion |
| Settlement / scheduler | Intact typed settlement plan and explicit host-owned scheduler projection | Scheduler detail, actual host readback and exactly-once recovery still need host adoption qualification |
| Optional memory | Verified boundary participation plus fresh host binding | Off/recall/ingest/stale/provider-failure isolation remains mandatory; transport parity is not model value |

The capability-fact correction is a read-model change in the established
TypeScript owner. Real captured File/SQLite decisions cover refusal and admission
with memory off, recall-only, ingest-only and invalidated configuration. New
projections sign those retained facts without rewriting saved signatures; no
new coverage version, admission rule or Python policy is introduced. Host
provider-failure and settlement cases remain covered separately, and do not
qualify installed App convergence or model costs. Keep all three Todos below
open until their own acceptance is met; this inventory is not blanket permission
to delete the remaining Python IO adapters.

Remaining implementation Todos, in dependency order:

| Todo | Observable outcome and decisive acceptance |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,24 @@ R5 短包投影也完整保留已有 CLI 结算计划,包括 effect identity
envelope 投影,Codex CLI 保持关闭隔离;这不代表安装态 heartbeat/App 或模型收益
已验收。

第一批收敛的当前源码路径核对:

| 执行事实 | 既有 owner/投影 | 保留的边界 |
|---|---|---|
| Goal/Agent/Todo 身份 | quota 选择及回执;envelope actor、selected Todo 和签名结算身份 | 捕获身份与写入时校验仍必要;展示身份不授予执行权 |
| 完整要求 | interaction 必读及原样保留的短包命令 | 完整 Goal/Todo 共用读取在 #5794 提案中;摘要或源 hash 不证明宿主已读 |
| 能力拒绝 | 既有 capability_gate_v0;短包保留原样 required/missing 与源中历史字段 | 修复事实遗漏,不改变就绪策略或能力开启 |
| 选择/claim/lease | selected Todo、action portfolio 及当前所属事务 | 短包归属不是新鲜 lease;保留捕获源并在所属写入入口复核 |
| replan/Goal 收尾 | replan action packet、contract capsule 和 vision audit | 完整证据仍走有权限的详情;Todo 完成不证明 Goal 完成 |
| 结算/scheduler | 完整 typed 结算计划及显式宿主调度投影 | 调度详情、实际宿主读回与一次恢复仍待宿主采用验收 |
| 可选 memory | 核验后的 boundary 参与事实及新鲜宿主绑定 | 关闭/recall/ingest/失效/provider 失败隔离持续保留;传输等价不是模型收益 |

能力事实修复归既有 TS read-model owner。真实捕获的 File/SQLite 决策覆盖拒绝及
准入,包含关闭、仅 recall、仅 ingest 与配置失效。新投影签名覆盖这些保留事实,
不重写历史签名、不增覆盖版本、准入规则或 Python 策略。宿主 provider 失败和
结算另有覆盖,仍不证明安装态 App 收敛或模型成本。以下三项继续按各自验收保持
未完成;这份核对不授权批量删除剩余 Python IO adapter。

后续实施 Todo 按依赖顺序推进:

| Todo | 可观察结果与决定性验收 |
Expand Down
21 changes: 21 additions & 0 deletions docs/reference/protocols/turn-envelope-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,27 @@ final packet, including diagnostics. The historical `source_json_bytes` and
`envelope_json_bytes` fields still count Unicode code points for v0 compatibility;
do not use them as wire-byte measurements.

### Capability refusal facts

`boundary.capability_gate` carries the existing quota gate's `required` and
`missing` arrays intact, including an empty `missing` array after admission.
Earlier compact projection used only `required_capabilities` and
`missing_capabilities`, which could report matching projection signatures while
omitting the current refusal facts. Historical names are still carried when
present in the supplied source; they are not synthesized or rewritten.

These facts participate in the existing boundary signature. Newly built
projections over current sources therefore have different hashes; saved
signatures are not rewritten and this is not a cross-version hash-equivalence
promise. Full quota output, admission, optional capability activation, claim,
lease and mutation checks are unchanged. No capability is granted by reading
these arrays. The default full heartbeat packet remains the source decision;
compact/Turn hosts retain its reason for refusal without recomputing readiness.

中文:短包原样保留现有能力门禁的 required/missing(包括合法空数组),历史字段
只在源中存在时保留。新投影签名覆盖这些事实,不改写历史签名;完整包、准入、
可选能力开关和 claim/lease 权限不变。读取缺失项不授予能力。

### Optional memory participation

The compact boundary retains the verified Goal/Agent Reward Memory automation
Expand Down
4 changes: 3 additions & 1 deletion loopx/control_plane/quota/turn_envelope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,9 @@ function boundary(payload: JsonObject): JsonObject {
const capabilityGate = object(payload.capability_gate);
if (Object.keys(capabilityGate).length > 0) {
result.capability_gate = Object.fromEntries(
["action", "reason", "required_capabilities", "missing_capabilities", "owner_action"]
// Current gate facts use required/missing; keep historical field names
// only when supplied by a stored source. Never rebuild the gate here.
["action", "reason", "required", "missing", "required_capabilities", "missing_capabilities", "owner_action"]
.filter((field) => capabilityGate[field] !== null && capabilityGate[field] !== undefined)
.map((field) => [field, capabilityGate[field]]),
);
Expand Down
72 changes: 72 additions & 0 deletions tests/control_plane/test_turn_envelope_capability_facts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""One captured native decision must preserve its capability refusal facts."""
from __future__ import annotations

import json
from pathlib import Path

import pytest

from loopx.control_plane.coordination.runtime_shadow import build_todo_runtime_shadow_projection
from tests.control_plane.canonical_authority_fixture import (
initialize_canonical_authority, isolate_sqlite_runtime,
)
from tests.control_plane.reward_memory_host_fixture import enable_live_memory
from tests.control_plane.test_quota_settlement_cli import (
AGENT_ID, GOAL_ID, TODO_ID, _run_cli, _write_fixture,
)


@pytest.mark.parametrize("provider", ["file", "sqlite"])
@pytest.mark.parametrize("mode", ["off", "recall_only", "ingest_only", "stale"])
@pytest.mark.parametrize("network_available", [False, True])
def test_real_captured_guard_preserves_required_and_missing_capabilities(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, provider: str,
mode: str, network_available: bool,
) -> None:
isolate_sqlite_runtime(tmp_path, monkeypatch)
project, runtime, registry = _write_fixture(tmp_path, required_capability="network")
state = project / f".codex/goals/{GOAL_ID}/ACTIVE_GOAL_STATE.md"
state.write_text(state.read_text().replace("action_kind=validate ",
"action_kind=validate continuation_policy=same_agent_non_delivery "))
code, listed = _run_cli(registry, runtime, "todo", "list", "--goal-id", GOAL_ID)
assert code == 0, listed
projection = build_todo_runtime_shadow_projection(
goal_id=GOAL_ID, handoff_mode="soft_claim", todos=listed["todos"],
)
initialize_canonical_authority(runtime, GOAL_ID, projection, state_path=state, provider=provider)
if mode != "off":
config = enable_live_memory(registry, recall=mode == "recall_only", ingest=mode == "ingest_only")
if mode == "stale":
config.write_text(config.read_text() + "\n")
capabilities = ["--available-capability", "filesystem_read"]
if network_available:
capabilities.extend(["--available-capability", "network"])
capture = tmp_path / "capture"
code, envelope = _run_cli(registry, runtime, "quota", "should-run",
"--goal-id", GOAL_ID, "--agent-id", AGENT_ID, "--todo-id", TODO_ID,
"--turn-instance-id", "turn-capability-facts", "--codex-app", *capabilities,
"--turn-envelope", "--decision-output-dir", str(capture), cwd=project)
assert code == (0 if network_available else 1), envelope
full = json.loads((capture / "decision.json").read_text())
gate = full["capability_gate"]
assert "network" in gate["required"]
assert ("network" in gate["missing"]) is (not network_available)
assert full["interaction_contract"]["agent_channel"]["delivery_allowed"] is network_available
compact = envelope["boundary"]["capability_gate"]
assert compact["required"] == gate["required"]
assert compact["missing"] == gate["missing"]
assert compact["action"] == gate["action"]
assert envelope["action"]["delivery_allowed"] is network_available
assert envelope["action_signature"]["matches"] is True
# The source remains one full observation, not another admission evaluation.
from loopx.control_plane.quota.turn_envelope import (
build_turn_envelope, turn_envelope_action_signature_document,
)
before = {p: p.read_bytes() for p in runtime.rglob("*") if p.is_file()}
assert build_turn_envelope(full)["boundary"]["capability_gate"] == compact
assert {p: p.read_bytes() for p in runtime.rglob("*") if p.is_file()} == before
changed = json.loads(json.dumps(envelope))
changed["boundary"]["capability_gate"]["missing"] = ["different-capability"]
assert turn_envelope_action_signature_document(changed) != turn_envelope_action_signature_document(envelope)
if mode in {"off", "stale"}:
assert "reward_memory" not in envelope["boundary"].get("capabilities", {})
23 changes: 23 additions & 0 deletions tests/control_plane_ts/turn_envelope.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,29 @@ const protocolActionFields = {
agent_action: "advance one bounded segment",
};

test("capability facts retain exact current and historical source fields", () => {
for (const fields of [
{required: ["network", "filesystem_write"], missing: ["network"]},
{required: ["network"], missing: []},
{required_capabilities: ["network"], missing_capabilities: ["network"]},
]) {
const source = payload();
source.capability_gate = {action: "repair_bridge", reason: "Unavailable capability",
...fields, runnable_candidates: [{private_detail: "not in compact context"}], available: ["shell"]};
const before = structuredClone(source);
const envelope = buildTurnEnvelope({payload: source, protocol_action_fields: protocolActionFields,
scheduler_execution_args: ""});
assert.deepEqual((envelope.boundary as JsonObject).capability_gate,
{action: "repair_bridge", reason: "Unavailable capability", ...fields});
assert.deepEqual(source, before);
assert.deepEqual(quotaActionSignatureDocument(source, protocolActionFields),
turnEnvelopeActionSignatureDocument(envelope));
const changed = structuredClone(envelope);
((changed.boundary as JsonObject).capability_gate as JsonObject)[Object.keys(fields)[1]] = ["different"];
assert.notDeepEqual(turnEnvelopeActionSignatureDocument(changed), turnEnvelopeActionSignatureDocument(envelope));
}
});

test("optional memory participation is compact, verified and signed", () => {
const plain = payload();
const build = (source: JsonObject) => buildTurnEnvelope({payload: source,
Expand Down
Loading