Skip to content

fix(control-plane): read delegation registry through the refusing loader - #7386

Merged
huangruiteng merged 1 commit into
mainfrom
codex/fix-delegation-context-refusing-registry-loader
Oct 26, 2026
Merged

huangruiteng merged 1 commit into
mainfrom
codex/fix-delegation-context-refusing-registry-loader

Conversation

@hhyykk

@hhyykk hhyykk commented Oct 26, 2026

Copy link
Copy Markdown
Collaborator

Goal And Delivered Outcome

  • Outcome basis / optional anchor: Restores the architecture boundary that PR refactor(collaboration): converge Goal agent-context delegation projection in TypeScript #7325 broke. The architecture guard test test_direct_project_registry_loaders_have_source_session_denial has failed on main since that PR merged.

  • Goal/source and gap: PR refactor(collaboration): converge Goal agent-context delegation projection in TypeScript #7325 added a direct call to load_project_registry in loopx/control_plane/collaboration/delegation_context.py. That loader accepts source-session (v2) registries. Generic reads refuse them through require_runtime_compatible_project_registry, which load_registry uses. So the delegation reader could read a source-session registry that other reads refuse.

  • Observable before → after, with the validation row that proves it: Before, a registry whose profile is source_session_v1 was read, and the delegation packet could be ready. After, the reader reports config_error "unavailable", and the delegation packet is blocked with reason delegation_context_unavailable. The architecture guard passes. See the validation rows for the guard test and for test_source_session_registry_blocks_delegation_context.

  • Issue/task and intended base: No issue number is linked. Related to PR refactor(collaboration): converge Goal agent-context delegation projection in TypeScript #7325, which introduced the regression. Base branch: main.

Author Declaration

  • Written by: hhyykk, with AI assistance from Factory Droid

Implemented against

  • Specification and revision: No separate specification. The basis is the architecture guard test_direct_project_registry_loaders_have_source_session_denial.
  • Criteria:
Criterion (spec clause) Disposition Symbol / path Test or command
Direct project-registry loaders must refuse source-session registries (architecture guard) implemented loopx/control_plane/collaboration/delegation_context.py tests/architecture/test_source_session_registry_denial.py::test_direct_project_registry_loaders_have_source_session_denial
  • Self-check before submission: I ran the checks listed in the Validation section and recorded their results there. I did not run the full Python suite or an end-to-end CLI run with a source-session registry. Nothing else is claimed as verified.

Scope And Continuation

  • Completed scope and remaining work: The delegation reader now calls load_registry instead of load_project_registry. load_registry refuses the lifecycle-only source_session_v1 profile. It calls stat() first, so errors that Path.exists() would hide (ENOTDIR, ELOOP, EBADF) still report config_error "unavailable", as on main.

    Behavior change: a registry whose profile_id is source_session_v1 now gives config_error "unavailable". The delegation packet is blocked with reason delegation_context_unavailable. This applies to any wire format. A missing registry is unchanged (no goals, no error). Other registries decode as before.

    Not changed: the registry codec. It is the same as on main.

    This change is under loopx/control_plane/**, so it needs explicit maintainer merge authorization under the repository rules. The author has not merged it.

  • Slice boundary / successor: Two follow-up candidates are not part of this PR. First, registry.global.json still skips the profile refusal in the codec, which predates this change. Second, the architecture guard checks only direct name calls. Neither is tracked in this PR.

Validation

  • Tested revision: this PR head, rebased onto the current origin/main. Base comparison numbers were measured on the previous origin/main, before the rebase.
  • Run state: finished
  • Input classes: synthetic
Check kind Result Public-safe evidence / limitation
static passed ruff check passes on the two changed Python files. The new code is formatted with ruff format. Both files already had format differences on main, and the repository does not gate on ruff format.
unit passed tests/architecture/test_source_session_registry_denial.py: 9 passed on the PR head. On main: 1 failed and 8 passed, because main has an extra caller in delegation_context.py.
unit passed tests/control_plane/test_delegation_context.py with tests/test_turn_machine_credential.py: 18 passed on the PR head, including the two new tests.
unit passed test_source_session_registry_blocks_delegation_context: fails on main (the packet is ready instead of blocked) and passes on the PR head.
unit passed test_registry_path_under_a_file_is_unavailable_not_empty: passes on main and on the PR head. Negative control: the same change without the stat() line fails (None instead of unavailable).
regression_parity failed Full tests/architecture: main has 2 failed and 1605 passed. The PR head has 1 failed and 1606 passed. The remaining failure is tests/architecture/test_lark_identity_shape_owner.py::test_no_lark_module_hides_an_unfoldable_identifier_construction. It also fails on main, and this change does not touch it. The Lark subsystem maintainers own it.
unit passed node --test tests/control_plane_ts/agent_context.test.ts: 27 pass, 0 fail. This is a regression check only. The change is Python only.
integration not_run The full Python suite was not run. Main already has pre-existing failures that are tracked separately.
real_entrypoint not_run No end-to-end CLI run with a source-session registry. The behavior is covered at unit level.
manual not_applicable No browser or packaged smoke check. This change has no UI impact.
  • Coverage and gaps: The guard test and the two new tests cover the changed reader and the refusal path. Not run: the full Python suite and an end-to-end CLI run with a source-session registry. The one remaining architecture failure is the pre-existing Lark test described above.

See validation disclosure guidance.

Frontend / Visual Evidence

  • UI impact: none
  • Before: N/A
  • After: N/A
  • First viewport and preview: N/A
  • States and viewports shown: N/A
  • Source data: none
  • Attention review: N/A, no UI change.
  • Craft review: N/A, no UI change.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Refactoring (no functional changes)
  • Documentation update
  • Test update

LoopX Area

  • Control plane (goals, todos, quota, scheduler, registry, runtime)
  • Benchmark boundary (adapters, runners, verifiers, scoring, evidence)
  • Capability or extension (providers, adapters, skills)
  • Public docs or presentation surface (README, protocols, dashboard)
  • Build, packaging, installer, or CI
  • Host or runtime integration

Technical Direction

  • Direction / acceptance reference, when applicable: None referenced.

Shared-authority RFC fixture impact

N/A. This PR does not claim progress against the TypeScript control-plane migration or the shared Goal Authority RFC.

  • Production-scale fixture schema: N/A
  • Semantic dimensions changed, or reviewed no-impact rationale: No shared fixture changes. The new test writes its own temporary registry.
  • Provider conformance arms run: N/A
  • Read-only legacy/file/PostgreSQL three-arm rehearsal (required for promotion, runtime-routing, or compatibility-projection changes): N/A. This change is not a promotion, runtime-routing, or compatibility-projection change.

Boundary Checklist

  • Neither the diff nor this PR body/comments/attachments disclose private state, credentials, raw traces or verifier output, internal links, or local machine paths (including .loopx/, .codex/goals/, and live ACTIVE_GOAL_STATE.md).
  • I did not duplicate maintainer-owned benchmark work unless a maintainer split out a public issue for it.
  • I kept the change scoped to the linked issue/task.
  • Existing English/Chinese RFC pairs (including ledger entries) both have a diff, and the local premerge check python3 scripts/check_rfc_bilingual.py --base origin/main --head HEAD passes, or no paired RFC changed.
  • I completed the visual evidence section for UI changes, or marked UI impact none.
  • Every commit includes a DCO Signed-off-by trailer (git commit -s).

The delegation context reader called load_project_registry directly. That loader accepts source-session (v2) registries, which generic reads refuse, and the architecture guard allows only allowlisted modules to call it. Read through load_registry instead, which refuses the lifecycle-only profile.

Keep the unavailable mapping for registry paths that Path.exists() would hide (ENOTDIR, ELOOP). The stat() call before load_registry makes those raise as before.

Registries that carry profile_id source_session_v1 are now reported as unavailable, in any format. Other registries decode as before.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Signed-off-by: Jen <jen@JendeMacBook-Pro.local>

@loopx-agent loopx-agent left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewer: model_agent · gpt-6.1-sol · OpenAI · runtime_reported · reasoning_effort=max

详细中文评审

动机

读取已配置委派路线的 Agent 和集成方会遇到这个问题。同一份仅供生命周期操作的项目注册表,通用 CLI 已拒绝,委派规划却仍显示 ready;修改后委派规划也返回 blocked,并保留不可用原因。独立同案例比较确认三种项目注册表表示都被拒绝,普通对象和 v1 注册表仍能读取,读取过程不改写输入。本次只恢复委派事实读取的边界,不激活来源会话,不启动子 Agent,也不宣称整个生命周期或安装路径已通过验收。

改动思路

这里需要恢复既有边界的一致性,继续保留直接解码会让规划目录与通用运行入口互相矛盾。更小的正确方案就是调用已有拒绝式 loader;在本模块另写 profile 判断会复制规则,单独替换 loader 又会丢掉部分文件系统错误。配置分类仍属于既有 TypeScript owner;Python 只通过统一 loader 读取项目注册表,并保留 stat 的真实错误边界。没有第二套 profile 判断或新状态。恢复现有委派事实读取的一致拒绝与普通路径兼容;不推广 source-session,不修全局文件名豁免。

没有新增配置、状态镜像或用户确认。当前目录仍由原 TypeScript owner 判断 requester、路线与可用性,Python 只提供原始事实。规划提示不是执行准入;我没有把修复这个读取点解释为激活 source-session 或完成全部生命周期安全资格。

具体改动

本次共两文件,生产代码 +4/-2,测试 +42。规格依据是 docs/architecture/rfcs/goal-instance-identity-and-orphan-recovery-v0.md,不可变修订 2de490ed6c632e0d641453fd2f10ed3dde69fc48;独立阅读后使用其来源安全、非激活兼容与激活 hold,未要求此有界修复完成整个 M2/M3。

  • S7-read-refusal:来源不兼容必须拒绝;三种项目编码在实际事实读取与 typed 消费边界均由 ready 变为 blocked,满足此处义务。
  • M3-nonactivated-parity:非激活行为保持;九类既有行为、十二个关闭能力案例和真实对象/v1 CLI 读取保持一致。
  • M2-activation-hold:组件测试不授激活权;三种来源会话的通用 CLI 在基线和当前提交都提前拒绝,生命周期激活仍未被本 PR 证明。

关键代码讲解

  1. read_delegation_facts 先读取原配置,再 stat() 注册表,随后复用 load_registry。真正缺失仍进入原 FileNotFoundError 分支;ENOTDIR、ELOOP 和不兼容 profile 保留为 unavailable,不借空列表掩盖不可读状态。
  2. load_registry 本身没有修改;既有 decode_registry_snapshot 和 require_runtime_compatible_project_registry 继续拥有格式校验与生命周期 profile 的拒绝。没有在委派模块复制第二套判断。
  3. project_goal_agent_context 的开启条件、关闭时不读保留配置的条件及 typed 投影都未变。独立实际执行的关闭能力案例确认,没有因为这个 import 调整而新增目录读取、提示或执行权。

对主干的风险

最值得反驳的承诺是“换成 loader 就足够”:如果遗漏 stat(),底层 Path.exists() 可能吞掉 ENOTDIR/ELOOP,令不可用误成缺失。我在真实文件父路径、自循环符号链接、目录、坏 JSON、坏摘要、缺失文件及普通注册表上做了同定义比较;这些既有行为保持,输入文件逐字节未变。source_session_v1 放在 v2、普通对象和 v1 项目表示中,基线均错误显示 ready,当前均为 blocked/delegation_context_unavailable。

独立执行结果:111 项 Python 通过,涵盖事实读取、凭据/执行器绑定、Turn、agent-context、委派 CLI 和架构守卫;原生 TypeScript 上下文测试两侧各 27 项通过。Ruff、完整 diff hygiene、严格质量收据 cqr_dabb596af71aa8429f21 的 pass/valid 以及当前 Goal-bound standard premerge 均通过,无失败或 manual hold。测试数不是效果证明;决定性证据是上述实际输入到消费边界的拒绝与兼容比较。

全局文件名 registry.global.json 的既有 profile 豁免在两侧仍存在,作者已将它列为范围外问题;本次没有扩大它,也没有宣称修复。通用 CLI 对来源会话的提前拒绝仍可能以原异常文本退出,不能把它写成新提供的 JSON 错误体验。未独立跑全量 Python/全部架构套件,未把作者关于旧 Lark 失败的声明当成本次归因。按当前契约未读取、轮询或等待 CI。

额外的五项真实范围比较也已完成:受保护的项目 profile 与后来加入的 Goal 保持拒绝;改显示名或状态不绕过;同一物理项目里的普通注册表不被误拦;纠正调用来源后,真实 before_delegate CLI 返回原绑定路线,原来源会话文件不降级、不改写,也未启动子 Agent。

我的整体评价

批准当前精确 head 423ebb27bd64c13d991eaf8195d39d7be9f90445。long_horizon 的局部结果是减少一个可复现的错误规划事实;user_experience 保持原读取入口和零新增确认。这里没有完整成本或长期因果净收益数据,不能由质量收据、测试数量或本评审推出。复用和体量均合适:stat() 是已验证的错误边界,不需要另加 wrapper 或启动更大重构。现有词汇和 TypeScript 决策 owner 继续使用,相关有界重构判定为不需要。

此 diff 只修改内部事实读取与测试,没有渲染展示变化,visual_review 为 not_applicable。安装后的 App/Host、原生 Windows、全局文件名后续修复及 M2/M3 激活均不在本次资格内。APPROVE 与合并权限、精确 head 的后续 readiness 是分开的判断;本评审不执行合并。

英文简短结论

English verdict: APPROVE — 423ebb2. Reuses the canonical refusing registry loader while preserving stat-based IO errors. Independent real host/typed comparisons close three project-profile bypasses, preserve nine active and twelve disabled cases, and retain ordinary CLI parity. Focused Python/TS, exact quality and Goal-bound premerge pass. Global-name exemption, installed/native-platform and lifecycle activation remain outside this qualification; no merge authority.

@huangruiteng
huangruiteng merged commit 1163ada into main Oct 26, 2026
27 of 40 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants