fix(goal-start): fence guided bootstrap over orphaned Goal state - #4808
hahahahahayesyeseys wants to merge 4 commits into
Conversation
A project reset that deletes a Goal's registry entry can leave its project-local active-state file behind. inspect_bootstrap_connection classified that as the ordinary registry_without_goal absence, so guided packets kept offering connect_if_needed, fresh-agent registration, Todo writeback, host-loop activation and quota spend for the same human-readable Goal id, and a later diagnose reported a healthy Goal over state an earlier lane left behind. Separate the two absences: when no registry entry matches but a state file for that id still exists under a project goal-state route, the connection is now orphaned_goal_state and the packet carries no mutation continuation. Only inspection and the backup-state preview remain, and the gate records that archiving or adopting project-local orphan state has no shipped command yet. The whole fence lives in the new goals-domain module so the already oversized bootstrap_command_pack keeps only its wiring and stays inside its reviewed module ceiling. Refs loopx-project#4801 Signed-off-by: hahahahahayesyeseys <95999512+hahahahahayesyeseys@users.noreply.github.com>
Cover the detected fact over the current and legacy project state routes, the blocking guided transaction, the negative twins (plain absence keeps connect_if_needed, and a state file for another Goal id does not block), and that the shipped onboarding entry qualifier reports route=stop with zero actionable commands. Add the same invariant to the owning smoke through the real CLI so a future packet change cannot silently restore a continuation. Refs loopx-project#4801 Signed-off-by: hahahahahayesyeseys <95999512+hahahahahayesyeseys@users.noreply.github.com>
|
Full
Attribution of the 91, all pre-existing on
Combined with the earlier domain-scoped run ( |
huangruiteng
left a comment
There was a problem hiding this comment.
评审 exact head:144072cdbee74cb37b7b91ff8d5b8778f7c86366
动机
这个 PR 解决的是 #4801 中一个真实且高风险的 authority discontinuity:项目注册表不再声明某个 Goal,但同 id 的 ACTIVE_GOAL_STATE.md 仍留在项目目录时,start-goal --guided 不能把它当成全新 Goal,继续提供 bootstrap、注册新 agent、写 Todo、启用 host loop 和消费 quota 的路径。否则同一个可读 Goal id 会同时承载旧状态和新 lane 的写权限。
把这项工作拆成“先阻断危险续跑”的独立 slice 是合理的。archive/adopt、diagnose 告警和 opaque Goal generation identity 可以后续完成,因为单独落地 fence 已经能减少实际危害;但 fence 必须覆盖 issue 定义的全部“没有匹配 registry Goal、却存在 state candidate”情形。
改动思路
实现新增 loopx/control_plane/goals/orphaned_goal_state.py,把四个历史/当前项目状态根目录的候选探测、orphaned_goal_state connection projection、operator gate、command-pack 清理和 guided Markdown 渲染集中到一个 owner。inspect_bootstrap_connection 在“registry 存在且没有匹配 Goal entry”时调用该 classifier;fence_command_pack 将 bootstrap、registration、planning、refresh、host activation、quota 等 continuation 清空;guided_fence 把 transaction 收敛为 inspect_connection 和 resolve_orphaned_goal_state 两步。
这个 ownership 方向是对的:orphan 是 registry/state 的派生事实,不是新的持久化真相;现有 state-backup 根目录得到复用,也没有偷偷 copy/merge 状态。inspect 与 backup 都保持 preview-only,未把 archive/adopt 伪装成已经可用的命令。
具体改动
orphaned_goal_state_routes在.loopx/goals、.codex/goals、.claude/goals、.local/goals中按精确 Goal id 查找候选,只投影项目相对路径。registry_missing_goal_connection将 truthy registry 中的“无匹配 Goal”分成普通registry_without_goal和orphaned_goal_state。fence_command_pack清空 7 个 mutation continuation,并把 safety contract、recommended next step 和 resolution gate 对齐。guided_fence替换原 transaction 的 ordered steps,同时移除 identity selection gate;onboarding qualifier 因而返回route=stop。- 新增 9 个 focused tests,覆盖四种根目录、普通 absence、connected Goal、command-pack/guided parity 与 qualifier stop;bootstrap smoke 也新增了真实 CLI 形状。
阻塞问题在 loopx/bootstrap_command_pack.py:592:if not registry 仍然在新 classifier 之前直接返回 not_connected。我在 exact head 上用 shipped CLI 构造“没有 .loopx/registry.json,但 .codex/goals/reset-goal/ACTIVE_GOAL_STATE.md 存在”的项目,返回结果同时满足:
state_file_exists=trueconnection_state=not_connected- transaction 仍包含
connect_if_needed、select_agent_identity、Todo planning/writeback、refresh、activation、quota guard - command pack 仍给出 bootstrap、registration、plan 和 refresh continuation
这正是 #4801 要阻断的第二 authority 风险。当前测试 helper 总会写一个 truthy registry(含另一个 live Goal),所以 9 个新测试全部通过也不会触达这个 bypass。请在 ordinary missing/empty-registry return 前复用同一 orphan projection:只有完全没有 candidate 时才返回 not_connected;并补 missing registry、empty registry 与对应 no-state negative twins,最好走真实 CLI。
对主干的风险
已覆盖的 truthy-registry 分支风险控制得不错:普通全新 Goal 与已连接 Goal 的 negative twins 保持旧行为;gate 本身不写状态,所有 projected resolution route 都不含 --execute;当前 head 与最新 origin/main 没有发现文本冲突。
但是未覆盖分支的 blast radius 很大:operator reset 若直接移除整个 registry,host 会拿到普通 onboarding packet,依次获得 bootstrap、fresh-agent、Todo、host-loop 和 quota 的可执行引导。这个漏洞不是 presentation 差异,而是 fence authority 的 false negative,因此不能在当前 head 批准。
本地验证:focused tests 9 passed in 0.94s;examples/bootstrap-command-pack-smoke.py 通过;changed paths Ruff 通过;git diff --check 通过。按 Goal 配置 wait_for_ci=false,没有抓取或等待远端 CI。真实 missing-registry CLI 反例失败,作为本次 REQUEST_CHANGES 的决定性证据。
我的整体评价
这是一个有价值、边界合理且总体架构方向正确的 safety slice;新模块没有制造第二份持久化 authority,未来相关的小型重构也已经体现在集中 classifier/fence owner 上,无需再引入更大的 framework。当前唯一 blocker 很具体:classifier 必须覆盖 registry file 缺失/empty 的同一 orphan invariant。修复后请在新 exact head 重新跑 focused + smoke + real CLI twins;archive/adopt、diagnose 和 generation identity 仍可按 PR 已声明的后续边界继续,不要求在本 PR 扩 scope。
English verdict: REQUEST_CHANGES - head 144072cdbee74cb37b7b91ff8d5b8778f7c86366 correctly fences orphaned state when a truthy registry lacks the Goal, and 9 focused tests, the bootstrap smoke, Ruff, and diff check pass; however, a real exact-head CLI reproduction with a missing registry and surviving state still returns not_connected and exposes bootstrap/registration/Todo/activation/quota continuations, so the same classifier must cover missing and empty registries before approval.
The first version classified orphaned state only inside the readable, non-empty registry branch, so a reset that removed .loopx/registry.json outright still reached the early not_connected return and a guided packet carrying bootstrap, fresh-agent registration, Todo planning and writeback, host-loop activation and quota guard for a Goal id whose state file survived. Move the shared classifier above both absence returns: the ordinary case keeps exactly the fields and connection_state it returned before (registry_exists and the reason text included), and only a surviving state candidate switches it to orphaned_goal_state. The command-pack size ceiling still holds (2233 against 2240), because the classifier now replaces two inline literals rather than adding a third branch. Refs loopx-project#4801 Signed-off-by: hahahahahayesyeseys <95999512+hahahahahayesyeseys@users.noreply.github.com>
Parametrized twins for a missing and an empty registry pin both halves: surviving state fences the packet with no actionable command, while a genuinely fresh project keeps its ordinary onboarding continuation and its previous connection_state. Add the missing-registry case to the owning smoke through the real CLI so the bypass cannot return silently. Refs loopx-project#4801 Signed-off-by: hahahahahayesyeseys <95999512+hahahahahayesyeseys@users.noreply.github.com>
hahahahahayesyeseys
left a comment
There was a problem hiding this comment.
Blocker fixed at this head (a0ca91d1). The classifier now runs above both absence returns instead of only inside the readable-registry branch, so a reset that removes the registry file itself can no longer reach a normal onboarding packet.
Your repro, re-run through the shipped CLI at this head (synthetic project with only .codex/goals/reset-goal/ACTIVE_GOAL_STATE.md present and no .loopx/registry.json at all):
connection_state: orphaned_goal_state,registry_exists: falseguided_transaction.blocked_by: orphaned_goal_state, ordered steps exactlyinspect_connection+resolve_orphaned_goal_statebootstrap_dry_run_preview,bootstrap_after_user_confirmation,goal_start_connect_if_needed,goal_start_plan_prompt,goal_start_refresh_state,goal_start_host_loop_activation,goal_start_quota_should_runallnull- projected orphan route reported relative to the project, nothing written
Re-runs you asked for at the new exact head
| check | result |
|---|---|
| focused module tests | 13 passed - four new parametrized cases (missing and empty registry, each fenced and each cleared) on top of the previous nine |
| owning smoke | passes, now including the missing-registry case driven through the real CLI entry point |
| module ceiling | the command-pack module is 2233 lines against its reviewed 2240, so the canary's unreviewed-finding list is still exactly the two pre-existing paths |
| static | ruff check and py_compile clean on every changed path |
The ordinary-absence half is pinned to be unchanged rather than merely un-broken: a genuinely fresh project still returns not_connected with the same fields it carried before (including registry_exists and the reason text, and no new keys), and its packet still exposes connect_if_needed with a non-null command.
One correction to my own framing, and one case left to you. A readable registry declaring no goals (goals: []) was already covered by the first head, because it falls through to the matching branch; the genuinely open path was the missing-file case your repro found. Both are pinned now, but for different reasons, and the test names say so.
The same false-negative shape still exists one branch earlier: when the registry file exists but cannot be parsed, inspect_bootstrap_connection returns registry_invalid before the classifier, and I measured that a packet over surviving state still carries connect_if_needed, select_agent_identity, Todo planning and writeback, refresh, activation, quota guard and the scheduler ack step, with goal_start_connect_if_needed non-null. I did not fold it in here because overriding connection_state there would bury the parse error that is the operator's actual next action, and that trade-off is yours rather than mine to make. Two shapes I would accept from you: keep registry_invalid as the state and just strip the continuations when a candidate exists, or classify it as its own blocked state that names both facts. Say which and I will land it on this branch.
archive/adopt, diagnose surfacing and opaque Goal instance identity stay as the declared follow-ups.
huangruiteng
left a comment
There was a problem hiding this comment.
评审 exact head:a0ca91d1d61341df260aaaf49742314ef9bca4dc
动机
这个 PR 处理 #4801 中一个真实且高风险的 authority discontinuity:项目注册表不再声明某个 Goal,但同 id 的 ACTIVE_GOAL_STATE.md 仍留在项目目录时,start-goal --guided 不能把它当成全新 Goal,继续提供 bootstrap、注册新 agent、写 Todo、启用 host loop 和消费 quota 的路径。否则同一个可读 Goal id 会同时承载旧状态和新 lane 的写权限。
本轮复审确认上一轮指出的 missing/empty-registry 漏洞已经修复:当前 head 会在这些分支调用统一的 orphan classifier,并保留普通无状态新项目的旧行为。这是实质性进展。但我继续沿着同一 authority invariant 检查真实输入边界,发现 invalid/unparseable registry 仍从更早的分支绕过 classifier,因此当前 head 仍不能批准。
改动思路
实现把候选状态探测、orphaned_goal_state connection projection、operator gate、command-pack 清理和 guided Markdown 渲染集中在 loopx/control_plane/goals/orphaned_goal_state.py。inspect_bootstrap_connection 现在不仅覆盖“registry 存在但无匹配 Goal entry”,也通过 unregistered_goal_connection 覆盖 missing/empty registry;fence_command_pack 将 bootstrap、registration、planning、refresh、host activation、quota 等 continuation 清空;guided_fence 把 transaction 收敛为只读 inspect 与 resolution preview。
这个 ownership 方向仍然正确:orphan 是 registry/state 的派生事实,不是新的持久化真相;四个 current/legacy state roots 得到复用,也没有偷偷 copy、adopt 或合并状态。此次修复也没有复制第二套 fence。剩余问题应继续复用同一 classifier,而不是新增 registry-error 专用安全模型。
具体改动
orphaned_goal_state_routes在.loopx/goals、.codex/goals、.claude/goals、.local/goals中按精确 Goal id 查找候选,只投影项目相对路径。goal_connection_without_matching_entry现在同时服务 registry-without-goal 与 unregistered project,两类路径都能区分普通 absence 和 orphaned state。fence_command_pack清空 mutation continuations,并把 safety contract、recommended next step 和 resolution gate 对齐;guided_fence同步移除 identity selection 与后续 mutation steps。- focused tests 增至 13 个,新增 missing registry、empty registry 及 no-state negative twins;bootstrap smoke 也覆盖了真实 CLI 形状。
剩余 blocker 位于 loopx/bootstrap_command_pack.py:580。if registry_error 在 unregistered_goal_connection 和 orphan projection 之前直接返回 registry_invalid。我在当前 exact head 上构造了一个 disposable project:.loopx/registry.json 内容不可解析,同时 .codex/goals/reset-goal/ACTIVE_GOAL_STATE.md 存在。shipped source CLI 的结果同时满足:
connection_state=registry_invalidstate_file_exists=true- safety contract 仍报告
orphaned_goal_state_blocks_continuation=false - transaction 仍包含
connect_if_needed、select_agent_identity、Todo planning/writeback、refresh、activation 和 quota guard - command pack 仍给出 bootstrap、registration、plan 与 refresh continuation
这和上一轮 missing-registry 反例的安全后果相同:registry 已经不再提供可信 Goal authority,但 surviving state 存在时 host 仍拿到了建立第二 authority 的可执行引导。registry_invalid 诊断当然要保留;需要修的是它不应同时放行 mutation continuation。最小修复是在 registry-error 分支探测同一 orphan candidate,保留 parse error/registry_invalid 事实,但投影现有 no-continuation fence。请补 invalid-registry + surviving-state 的真实 CLI regression,以及 invalid-registry + no-state 的 negative twin,确保不是把所有 registry 解析错误都误报成 orphan。
对主干的风险
已覆盖的分支风险控制得不错:普通全新 Goal、missing/empty registry 无 candidate、已连接 Goal都保留既有行为;gate 本身不写状态,resolution routes 也保持 preview-only。当前 head 的 missing/empty 修复没有引入新的持久化状态或权限。
但 invalid registry 并非罕见的理论输入:中断写入、人工编辑或部分损坏都可能触发 parse error。此时 packet 一方面告诉 operator registry 不可信,另一方面仍提供 bootstrap、fresh-agent、Todo、host-loop 和 quota continuation;这是一个相互矛盾的 authority projection。影响范围跨过 CLI presentation,进入后续写路径,因此仍是 P1 blocker。
本地验证:tests/control_plane/test_start_goal_orphaned_goal_state.py 为 13 passed in 2.90s;examples/bootstrap-command-pack-smoke.py 通过;changed paths Ruff 通过;git diff --check 通过。按 Goal 配置 wait_for_ci=false,没有抓取或等待远端 CI。真实 invalid-registry CLI 反例未满足安全 invariant,是本次 REQUEST_CHANGES 的决定性证据。
我的整体评价
当前 head 正确修复了上一轮反馈,而且集中 classifier/fence owner 的方向合理;bounded future-facing pass 不需要再引入新的 framework。剩余修改也很局部:让 parse-error 分支保留诊断但复用既有 orphan fence,并用 state/no-state twins 固定语义。修复后请在新 exact head 重新跑 focused tests、bootstrap smoke、Ruff、diff check 与真实 CLI twins;archive/adopt、diagnose 和 Goal-generation identity 仍可保持本 PR 已声明的后续边界,不要求扩大范围。
English verdict: REQUEST_CHANGES - head a0ca91d1d61341df260aaaf49742314ef9bca4dc fixes the prior missing/empty-registry bypass, and 13 focused tests, the bootstrap smoke, Ruff, and diff check pass; however, an exact-head real-CLI reproduction with an invalid registry and surviving Goal state still reports registry_invalid while exposing bootstrap, registration, Todo, activation, and quota continuations, so the existing orphan fence must also cover that branch while preserving the parse diagnostic.
|
Recording the CI attribution for this head so nobody has to re-derive it: both red lanes here reproduce outside this PR. Python Tests - 11 failing tests at this head, and the same workflow on node-minimum-compatibility - 2 failures, Local exact-head numbers for this branch remain: 13 focused tests passed, the owning smoke passed, |
|
Full Against the same sweep at the reviewed head |
|
Independent review of exact head Verdict: REQUEST_CHANGES — concurring with the second maintainer round. The fence correctly covers missing/empty registries (13 focused tests, real-CLI smoke, roots aligned verbatim with CI note: the pytest/test-shard reds at this head come from Suggested fix shape is the maintainer's: probe the same orphan candidate in the registry-error branch, keep the |
Goal And Delivered Outcome
inspect_bootstrap_connectionclassified that as the ordinaryregistry_without_goalabsence, so a guided packet kept offeringconnect_if_needed, fresh-agent registration, Todo writeback, host-loop activation and quota spend for the same human-readable Goal id, and a laterdiagnosereported a healthy Goal over state an earlier lane left behind. The gap is a missing distinction between "nothing was here" and "state is here but its authority is gone".connection_state: registry_without_goaltogether withstate_file_exists: trueand a bootstrap-oriented continuation (captured by the failing-before run in theregression_parityrow). After, the same project returnsorphaned_goal_state,ordered_stepscollapses toinspect_connection+resolve_orphaned_goal_state, and all five mutation continuation commands arenull(real_entrypointrow).main.Scope And Continuation
diagnosesurfacing of an unresolved orphan; (2) a previewable archive/adopt route for project-local orphan state - measured in this slice,uninstall-projectselects goals from the registry andarchive-runtimeresolves only a goal directory under the shared runtime root, so neither reaches this state, and the gate says so instead of projecting a command that fails; (3) an opaque Goal instance/generation id so host bindings cannot reattach across a deletion. Each changes a different command's contract and needs an owner decision on shape.Validation
real_entrypointstart-goal --guidedexecuted through the shipped CLI against a synthetic project reproducing the issue's shape: two ordered steps,writes_now=false,spends_quota_now=false,goal_start_connect_if_needed/goal_start_refresh_state/goal_start_host_loop_activation/goal_start_quota_should_run/goal_start_plan_promptall null,recommended_next_step.kind=resolve_orphaned_goal_statereal_entrypointorphaned_goal_state, two ordered steps,registry_exists=false, and all seven continuation command keys nullunitroute=stopwith zero actionable commandsregression_parityconnection_state=registry_without_goalwithstate_file_exists=true, i.e. it reproduces the reported defect; with the change it passes. Negative twins pin that plain absence still keepsconnect_if_neededand that a state file for a different Goal id does not blockintegrationtests/control_plane,tests/cli_commands,tests/canary: 4570 passed, 8 skipped, 1 failed - the failure is pre-existing, see thestaticrow and the final rowstaticruff checkclean on all four changed paths;py_compileclean; module ceiling held: the oversized command-pack module is 2236 lines against its reviewed 2240 ceiling, so the canary's unreviewed-finding list is byte-identical to base (two findings, same paths) and this PR adds no size debtmanualbackup-state --project . --current-project-onlyreturneddry_run=truewithwrote_local_registry=false, and checksums of the registry and the orphaned state file were identical before and afterintegrationnot_connectedreturn bypassed the fence; a registry file that exists but cannot be parsed still returnsregistry_invalidbefore the classifier and keeps its continuations, which is disclosed here rather than changed because overriding that state would hide the parse error an operator must fix. The changed paths are the goal-start connection classification, the guided/command-pack projection of a continuation, and their tests, which the rows above execute through the real CLI entry point as well as in-process. Not covered here and not changed: thediagnoseprojection, host-binding instance identity, and any persisted state or receipt contract - no AuthorityStore, cursor, outbox or provider arm is touched, so no conformance arm applies. The fulltests/sweep is being re-run at this head and its result is recorded in a follow-up comment on this PR.Frontend / Visual Evidence
guided_transactionorconnect_if_neededexists under the dashboard or desktop sources, so the affected surface is the CLI packet a host session executes.Type of Change
LoopX Area
Technical Direction
Shared-authority RFC fixture impact
Boundary Checklist
.loopx/,.codex/goals/, and liveACTIVE_GOAL_STATE.md).none.Signed-off-bytrailer (git commit -s).