From ae15893e6c41f0b51f8ad57df767bea517463139 Mon Sep 17 00:00:00 2001 From: generalio <1487144524@qq.com> Date: Tue, 18 Aug 2026 09:51:46 +0800 Subject: [PATCH 1/9] docs(workflow): add autonomous handoff protocol --- docs/agent-workflow-autonomous-handoff.md | 57 +++++++++++++++++++++++ tools/tests/test_agent_workflow.py | 18 +++++++ 2 files changed, 75 insertions(+) create mode 100644 docs/agent-workflow-autonomous-handoff.md diff --git a/docs/agent-workflow-autonomous-handoff.md b/docs/agent-workflow-autonomous-handoff.md new file mode 100644 index 0000000..019dece --- /dev/null +++ b/docs/agent-workflow-autonomous-handoff.md @@ -0,0 +1,57 @@ +# MultiWeb 自主工作流交接协议 + +本文档说明既有多 Agent 工作流的交接约定,不增加角色、状态、权限或审批能力。出现冲突时,文件优先级严格为 +`AGENTS.md` > `docs/agent-workflow.md` > 本文档。 + +## 角色与最小交接 + +Supervisor 负责创建 `.codex/workflow/runs//` 账本、按既有状态机推进状态、启动下一角色,并保留公开的 +相对证据路径和 Git SHA。进入下一状态前,先使用 `tools/agent_workflow.py transition` 记录状态变化和证据。 + +Planner 仅做只读分析,向 Supervisor 返回任务契约、基线 SHA、任务 DAG、独占文件或模块、依赖、最小验证命令、验收 +标准,以及公共 API、平台和安全默认值影响。`TRIVIAL` 契约必须写明理由、独占范围和快速路径验证命令。 + +Implementer 只能在独立 worktree 内修改任务契约明确授权且由其独占的范围。交接至少包含:任务和基线、实际修改文件 +与所有权映射、可应用 diff 或 worktree、执行命令及退出结果、未验证范围,以及公共 API、跨平台差异和安全默认值影响。 + +Integrator 只能应用完整交接并处理不改变语义的机械冲突。交接至少包含:已合并的交接项、范围不重叠检查、机械冲突 +处理说明、`git diff --check` 结果,以及固定候选提交所需的候选 SHA 证据。是否创建提交仍须遵守 `AGENTS.md` 和维护者 +的明确授权。 + +Verify-Reviewer 只在干净 worktree 对固定候选 SHA 做只读验证和审查。报告至少包含:候选 SHA、实际执行命令及退出 +结果、未验证范围、公共 API、跨平台一致性、JS 桥与安全默认值检查、按 P0/P1/P2 分类且带文件位置的问题,以及唯一的 +`PASS` 或 `REJECT` 裁决。审查报告只交给 Supervisor 记录。 + +## 账本、状态与证据 + +账本目录只保存公开状态、相对证据路径和 Git SHA;既有文件包括 `state.json`、`events.jsonl`、`task-contract.md`、 +`plan.json`、`handoffs/`、`candidate.sha`、`review-report.md` 与 `resume.md`。事件必须连续且时间严格递增,最后一条 +事件时间必须与 `state.json` 的 `lastEventAt` 相同。 + +状态仅按既有状态机迁移:`PLANNED` 可进入 `IMPLEMENTING` 或 `PAUSED`;`IMPLEMENTING` 可进入 +`READY_TO_INTEGRATE`、`PAUSED` 或 `BLOCKED`;`READY_TO_INTEGRATE` 可进入 `VALIDATING` 或 `BLOCKED`; +`VALIDATING` 可进入 `REVIEWED` 或 `BLOCKED`;`REVIEWED` 可进入 `COMMIT_READY`、`IMPLEMENTING` 或 `BLOCKED`; +`COMMIT_READY` 可进入 `PR_READY`;`PAUSED` 只能回到中断前状态或进入 `BLOCKED`。每个阶段最多自动尝试两次。 + +每次状态变化必须由 Supervisor 先执行 `transition` 并提供任务目录内的常规证据文件。进入 `VALIDATING` 必须提供 +`--candidate-sha ` 和 `candidate.sha` 证据;进入 `REVIEWED` 必须提供 `--verdict PASS|REJECT` 和 +`review-report.md` 证据。进入 `COMMIT_READY` 前,`candidate.sha` 与 `review-report.md` 必须绑定同一候选 SHA,裁决为 +`PASS`,且不存在未解决 P0/P1。 + +## 退回、阻断与恢复 + +接口签名、默认值、导航策略、JS 桥、安全设置或测试预期发生语义冲突时,Integrator 不得自行解决,必须退回原 +Implementer 或 Planner。交接不完整、需要维护者或凭据授权、无法启动所需角色、发现 P0/P1 时,Supervisor 必须暂停或 +阻断,记录原因和唯一下一步,不能静默以单 Agent 完成。 + +P0/P1 必须使 Verify-Reviewer 给出 `REJECT`,并阻断提交或 PR;P2 不阻断,但必须写入 PR 风险与兼容性说明。候选 SHA +一旦变化,之前的审查裁决立即失效;返工必须从 `REVIEWED` 回到 `IMPLEMENTING`,清除旧审查绑定后重新集成和审查。 + +恢复任务前,Supervisor 必须先执行: + +```shell +python3 tools/agent_workflow.py validate .codex/workflow/runs/ +python3 tools/agent_workflow.py resume .codex/workflow/runs/ +``` + +只有 `validate` 成功后才能读取 `resume` 给出的唯一下一步并继续推进。 diff --git a/tools/tests/test_agent_workflow.py b/tools/tests/test_agent_workflow.py index ca8e4d6..6f7555a 100644 --- a/tools/tests/test_agent_workflow.py +++ b/tools/tests/test_agent_workflow.py @@ -45,6 +45,24 @@ def run_cli(self, *arguments: str) -> subprocess.CompletedProcess[str]: check=False, ) + def test_all_workflow_role_configs_require_the_autonomous_handoff_protocol(self) -> None: + protocol_path = REPOSITORY_ROOT / "docs" / "agent-workflow-autonomous-handoff.md" + protocol_relative_path = protocol_path.relative_to(REPOSITORY_ROOT).as_posix() + role_configuration_paths = ( + REPOSITORY_ROOT / ".codex" / "agents" / "workflow-implementer.toml", + REPOSITORY_ROOT / ".codex" / "agents" / "workflow-integrator.toml", + REPOSITORY_ROOT / ".codex" / "agents" / "workflow-planner.toml", + REPOSITORY_ROOT / ".codex" / "agents" / "workflow-verify-reviewer.toml", + ) + + self.assertTrue(protocol_path.is_file(), f"缺少工作流协议文件:{protocol_relative_path}") + for role_configuration_path in role_configuration_paths: + self.assertIn( + protocol_relative_path, + role_configuration_path.read_text(encoding="utf-8"), + f"{role_configuration_path.relative_to(REPOSITORY_ROOT)} 必须声明工作流协议文件", + ) + def test_validate_accepts_a_planned_run(self) -> None: run_directory = self.copy_fixture("valid") From 65ccd3236e3d634d18f9a40f3ab8d7f2cec3d6e7 Mon Sep 17 00:00:00 2001 From: generalio <1487144524@qq.com> Date: Tue, 18 Aug 2026 09:52:49 +0800 Subject: [PATCH 2/9] fix(extension): harden script bridge origins --- .../AndroidScriptBridgeInstrumentationTest.kt | 57 +++++++++++++++++++ .../android/AndroidScriptBridgeInstaller.kt | 8 +-- .../AndroidScriptBridgeConfigurationTest.kt | 18 ++++++ .../desktop/DesktopScriptBridgeInstaller.kt | 3 + .../DesktopScriptBridgeConfigurationTest.kt | 6 +- .../extension/NativeWebViewBridgeExtension.kt | 7 +++ .../multiweb/extension/WebViewExtension.kt | 45 +++++++++++++-- .../NativeWebViewBridgeExtensionTest.kt | 27 +++++++++ .../extension/WebViewExtensionTest.kt | 53 +++++++++++++++++ .../multiweb/ios/IosScriptBridgeInstaller.kt | 3 + .../ios/IosScriptBridgeConfigurationTest.kt | 4 ++ 11 files changed, 222 insertions(+), 9 deletions(-) diff --git a/webview-android/src/androidTest/kotlin/io/github/multiweb/android/AndroidScriptBridgeInstrumentationTest.kt b/webview-android/src/androidTest/kotlin/io/github/multiweb/android/AndroidScriptBridgeInstrumentationTest.kt index 4029e06..f33fe28 100644 --- a/webview-android/src/androidTest/kotlin/io/github/multiweb/android/AndroidScriptBridgeInstrumentationTest.kt +++ b/webview-android/src/androidTest/kotlin/io/github/multiweb/android/AndroidScriptBridgeInstrumentationTest.kt @@ -80,6 +80,63 @@ class AndroidScriptBridgeInstrumentationTest { } } + @Test + fun 精确Https来源门面不会在同源srcdoc子框架创建() { + assumeBridgeFeaturesSupported() + val evaluationCompleted = CountDownLatch(1) + val result = AtomicReference() + val instrumentation = InstrumentationRegistry.getInstrumentation() + val activity = launchWebViewActivity() + lateinit var webView: WebView + instrumentation.runOnMainSync { + webView = WebView(activity).apply { + settings.javaScriptEnabled = true + AndroidScriptBridgeInstaller.install( + webView = this, + javaScriptEnabled = true, + bridges = listOf( + object : ScriptBridgeWithFacade { + override val name: String = "TrustedBridge" + override val transportName: String = "__multiweb_trusted_bridge" + override val allowedHosts: Set = setOf("trusted.example") + override val facade: ScriptBridgeFacade = ScriptBridgeFacade(setOf("ping")) + + override fun handle(call: ScriptBridgeCall): ScriptBridgeResponse { + return ScriptBridgeResponse(isSuccess = true) + } + }, + ), + ) + webChromeClient = object : WebChromeClient() { + override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean { + val prefix = "multiweb-trusted-child-facade:" + if (consoleMessage.message().startsWith(prefix)) { + result.set(consoleMessage.message().removePrefix(prefix)) + evaluationCompleted.countDown() + } + return super.onConsoleMessage(consoleMessage) + } + } + activity.setContentView(this) + loadDataWithBaseURL( + "https://trusted.example/", + "", + "text/html", + "utf-8", + null, + ) + } + } + + try { + assertTrue("未完成同源子框架门面检查", evaluationCompleted.await(10, TimeUnit.SECONDS)) + assertEquals("true", result.get()) + } finally { + destroyWebView(activity, webView) + } + } + @Test fun 不安全来源策略允许顶层Https页面调用受限桥() { assertUnsafeBridgeCanBeCalled("https://legacy.example/") diff --git a/webview-android/src/main/kotlin/io/github/multiweb/android/AndroidScriptBridgeInstaller.kt b/webview-android/src/main/kotlin/io/github/multiweb/android/AndroidScriptBridgeInstaller.kt index f466056..f94287b 100644 --- a/webview-android/src/main/kotlin/io/github/multiweb/android/AndroidScriptBridgeInstaller.kt +++ b/webview-android/src/main/kotlin/io/github/multiweb/android/AndroidScriptBridgeInstaller.kt @@ -21,8 +21,8 @@ import java.net.URI * * 不使用 `addJavascriptInterface`:后者会把整个对象暴露给所有已加载页面。精确 HTTPS 策略直接使用对应来源规则; * 不安全兼容策略必须使用 AndroidX WebKit 的全来源 `*` 规则安装内部消息通道,因而该内部对象会进入全部框架。 - * 此时网页门面仅在顶层 HTTP/HTTPS 页面创建,原生消息入口也会再次拒绝直接来自子框架、`file:`、`data:` 与自定义 - * Scheme 的请求。 + * 两种策略的网页门面均仅在顶层窗口创建,原生消息入口也会再次拒绝直接来自子框架、 + * `file:`、`data:` 与自定义 Scheme 的请求。 */ internal object AndroidScriptBridgeInstaller { fun install( @@ -79,11 +79,11 @@ internal data class AndroidScriptBridgeConfiguration( /** AndroidX WebKit 安装内部消息通道与门面脚本时使用的来源规则。 */ val allowedOriginRules: Set, ) { - /** 生成受限 Promise 门面;不安全模式下门面仅在顶层 HTTP/HTTPS 页面创建。 */ + /** 生成受限 Promise 门面;所有策略的门面均仅在顶层窗口创建。 */ fun facadeInjectionScript(): String? { val bridgeFacade = facade ?: return null val originCheck = when (originPolicy) { - is ScriptBridgeOriginPolicy.ExactHttpsHosts -> "true" + is ScriptBridgeOriginPolicy.ExactHttpsHosts -> "window.top === window" ScriptBridgeOriginPolicy.UnsafeAnyHttpOrHttps -> { "window.top === window && (window.location.protocol === 'http:' || " + "window.location.protocol === 'https:') && window.location.hostname.length > 0" diff --git a/webview-android/src/test/kotlin/io/github/multiweb/android/AndroidScriptBridgeConfigurationTest.kt b/webview-android/src/test/kotlin/io/github/multiweb/android/AndroidScriptBridgeConfigurationTest.kt index af26ec8..0d29a64 100644 --- a/webview-android/src/test/kotlin/io/github/multiweb/android/AndroidScriptBridgeConfigurationTest.kt +++ b/webview-android/src/test/kotlin/io/github/multiweb/android/AndroidScriptBridgeConfigurationTest.kt @@ -30,6 +30,24 @@ class AndroidScriptBridgeConfigurationTest { ) } + @Test + fun `精确 HTTPS 门面脚本只在顶层窗口创建`() { + val configuration = AndroidScriptBridgeConfiguration.create( + listOf( + object : ScriptBridgeWithFacade { + override val name = "TrustedBridge" + override val transportName = "__trusted_bridge_transport" + override val allowedHosts = setOf("trusted.example") + override val facade = ScriptBridgeFacade(setOf("ping")) + + override fun handle(call: ScriptBridgeCall): ScriptBridgeResponse? = null + }, + ), + ).single() + + assertContains(requireNotNull(configuration.facadeInjectionScript()), "window.top === window") + } + @Test fun `脚本执行仅接受可信 HTTPS 默认端口主文档`() { val allowedHosts = setOf("EXAMPLE.com") diff --git a/webview-desktop/src/main/kotlin/io/github/multiweb/desktop/DesktopScriptBridgeInstaller.kt b/webview-desktop/src/main/kotlin/io/github/multiweb/desktop/DesktopScriptBridgeInstaller.kt index 99eaf11..08b809f 100644 --- a/webview-desktop/src/main/kotlin/io/github/multiweb/desktop/DesktopScriptBridgeInstaller.kt +++ b/webview-desktop/src/main/kotlin/io/github/multiweb/desktop/DesktopScriptBridgeInstaller.kt @@ -225,6 +225,9 @@ internal data class DesktopScriptBridgeConfiguration( * 安全边界不一致。 */ internal fun isTrustedJavaScriptUrl(url: String?, allowedHosts: Set): Boolean { + if (allowedHosts.isEmpty()) { + return false + } return isTrustedJavaScriptUrl(url, ScriptBridgeOriginPolicy.ExactHttpsHosts(allowedHosts)) } diff --git a/webview-desktop/src/test/kotlin/io/github/multiweb/desktop/DesktopScriptBridgeConfigurationTest.kt b/webview-desktop/src/test/kotlin/io/github/multiweb/desktop/DesktopScriptBridgeConfigurationTest.kt index d41beeb..b579a2d 100644 --- a/webview-desktop/src/test/kotlin/io/github/multiweb/desktop/DesktopScriptBridgeConfigurationTest.kt +++ b/webview-desktop/src/test/kotlin/io/github/multiweb/desktop/DesktopScriptBridgeConfigurationTest.kt @@ -53,7 +53,11 @@ class DesktopScriptBridgeConfigurationTest { assertEquals(true, isTrustedJavaScriptUrl("https://example.com:443/page", allowedHosts)) assertEquals(false, isTrustedJavaScriptUrl("https://example.com:8443/page", allowedHosts)) assertEquals(false, isTrustedJavaScriptUrl("http://example.com/page", allowedHosts)) - assertEquals(false, isTrustedJavaScriptUrl("https://example.com/page", emptySet())) + } + + @Test + fun `空允许主机集合拒绝可信 HTTPS 页面`() { + assertFalse(isTrustedJavaScriptUrl("https://example.com/page", emptySet())) } @Test diff --git a/webview-extension-api/src/commonMain/kotlin/io/github/multiweb/extension/NativeWebViewBridgeExtension.kt b/webview-extension-api/src/commonMain/kotlin/io/github/multiweb/extension/NativeWebViewBridgeExtension.kt index 0f31d10..04f7a38 100644 --- a/webview-extension-api/src/commonMain/kotlin/io/github/multiweb/extension/NativeWebViewBridgeExtension.kt +++ b/webview-extension-api/src/commonMain/kotlin/io/github/multiweb/extension/NativeWebViewBridgeExtension.kt @@ -119,6 +119,10 @@ class NativeWebViewBridgeExtension private constructor( /** * 创建仅允许精确 HTTPS 主机使用的旧网页桥。 * + * [allowedHosts] 必须为非空的精确 ASCII 主机名集合。 + * [ScriptBridgeOriginPolicy.ExactHttpsHosts] 会在构造时拒绝端口、凭据、通配符、Unicode 与其他 URL + * 片段。 + * * 此构造器保留既有公开 API 与默认安全语义。 * [ScriptBridgeOriginPolicy.UnsafeAnyHttpOrHttps] 必须通过另一个显式命名的构造器选择。 */ @@ -132,6 +136,9 @@ class NativeWebViewBridgeExtension private constructor( /** * 创建启用受控旧脚本执行能力的桥。 * + * [allowedHosts] 沿用精确 HTTPS 主机约束。非法集合会在构造时被拒绝, + * 不能借由旧脚本执行入口放宽来源范围。 + * * 关闭时 [NativeWebViewBridgeRequest.SetPageLoadScript] 与 [NativeWebViewBridgeRequest.ExecuteJavaScript] 继续交由 * [host] 处理,保持既有行为。开启后这两个请求不再进入 [host]:最新页面脚本会在受信任主文档完成加载后执行, * 即时脚本会在收到请求时执行。控制器未提供或已释放时返回 `javascript_executor_unavailable`;当前主文档来源、 diff --git a/webview-extension-api/src/commonMain/kotlin/io/github/multiweb/extension/WebViewExtension.kt b/webview-extension-api/src/commonMain/kotlin/io/github/multiweb/extension/WebViewExtension.kt index 09bf6a1..3469b57 100644 --- a/webview-extension-api/src/commonMain/kotlin/io/github/multiweb/extension/WebViewExtension.kt +++ b/webview-extension-api/src/commonMain/kotlin/io/github/multiweb/extension/WebViewExtension.kt @@ -136,10 +136,40 @@ interface ScriptBridge { */ sealed interface ScriptBridgeOriginPolicy { /** 仅允许指定精确主机的 HTTPS 默认端口 443 页面使用桥。 */ - data class ExactHttpsHosts( - /** 允许使用桥的精确 HTTPS 主机名集合;不能为空且不支持通配符。 */ - val hosts: Set, - ) : ScriptBridgeOriginPolicy + class ExactHttpsHosts : ScriptBridgeOriginPolicy { + /** + * 允许使用桥的精确 HTTPS 主机名集合。 + * + * 集合不能为空;每项必须是无端口、凭据、路径或通配符的精确 ASCII 主机名。 + * + * 构造时会拒绝 Unicode 和其他 URL 片段,避免平台对同一来源产生不同解释。 + * 构造器会创建只读快照,避免调用方后续修改传入的可变集合后绕过来源校验。 + */ + val hosts: Set + + constructor(hosts: Set) { + val hostSnapshot = buildSet { addAll(hosts) } + require(hostSnapshot.isNotEmpty()) { + "JS 桥必须声明至少一个受信任主机。" + } + hostSnapshot.forEach { host -> + require(isExactAsciiHost(host)) { + "JS 桥只允许精确 ASCII 主机名且不支持通配符:$host" + } + } + this.hosts = hostSnapshot + } + + operator fun component1(): Set = hosts + + fun copy(hosts: Set = this.hosts): ExactHttpsHosts = ExactHttpsHosts(hosts) + + override fun equals(other: Any?): Boolean = other is ExactHttpsHosts && hosts == other.hosts + + override fun hashCode(): Int = hosts.hashCode() + + override fun toString(): String = "ExactHttpsHosts(hosts=$hosts)" + } /** * 不安全的旧页面兼容模式:仅允许主框架的 HTTP/HTTPS 页面使用桥。 @@ -150,6 +180,13 @@ sealed interface ScriptBridgeOriginPolicy { data object UnsafeAnyHttpOrHttps : ScriptBridgeOriginPolicy } +private val exactAsciiHostPattern = Regex( + "(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)" + + "(?:\\.(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?))*", +) + +private fun isExactAsciiHost(host: String): Boolean = exactAsciiHostPattern.matches(host) + /** * 可声明自定义来源策略的 JS 桥。 * diff --git a/webview-extension-api/src/commonTest/kotlin/io/github/multiweb/extension/NativeWebViewBridgeExtensionTest.kt b/webview-extension-api/src/commonTest/kotlin/io/github/multiweb/extension/NativeWebViewBridgeExtensionTest.kt index 61b09db..144d422 100644 --- a/webview-extension-api/src/commonTest/kotlin/io/github/multiweb/extension/NativeWebViewBridgeExtensionTest.kt +++ b/webview-extension-api/src/commonTest/kotlin/io/github/multiweb/extension/NativeWebViewBridgeExtensionTest.kt @@ -170,6 +170,33 @@ class NativeWebViewBridgeExtensionTest { assertTrue(exception.message.orEmpty().contains("allowedHosts")) } + @Test + fun `旧网页桥构造器不能绕过精确主机校验`() { + val invalidHostSets = listOf( + emptySet(), + setOf("*.example.com"), + setOf("example.com:8443"), + setOf("user:pass@example.com"), + setOf("例子.com"), + ) + + invalidHostSets.forEach { hosts -> + assertFailsWith { + NativeWebViewBridgeExtension( + allowedHosts = hosts, + host = NativeWebViewBridgeHost { NativeWebViewBridgeResult.Success() }, + ) + } + assertFailsWith { + NativeWebViewBridgeExtension( + allowedHosts = hosts, + host = NativeWebViewBridgeHost { NativeWebViewBridgeResult.Success() }, + enableLegacyJavaScriptExecution = true, + ) + } + } + } + @Test fun `不安全兼容模式不会降级为旧脚本执行器`() { val extension = NativeWebViewBridgeExtension( diff --git a/webview-extension-api/src/commonTest/kotlin/io/github/multiweb/extension/WebViewExtensionTest.kt b/webview-extension-api/src/commonTest/kotlin/io/github/multiweb/extension/WebViewExtensionTest.kt index 91ebf35..9f66e72 100644 --- a/webview-extension-api/src/commonTest/kotlin/io/github/multiweb/extension/WebViewExtensionTest.kt +++ b/webview-extension-api/src/commonTest/kotlin/io/github/multiweb/extension/WebViewExtensionTest.kt @@ -7,6 +7,7 @@ import io.github.multiweb.api.WebViewController import io.github.multiweb.api.WebViewState import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFailsWith import kotlin.test.assertSame import kotlin.test.assertTrue @@ -49,6 +50,58 @@ class WebViewExtensionTest { assertEquals("", bridge.handle(ScriptBridgeCall("other"))?.payload) } + @Test + fun `精确 HTTPS 主机策略拒绝空集合和非精确 ASCII 主机`() { + val invalidHostSets = listOf( + emptySet(), + setOf("*.example.com"), + setOf("example.com:8443"), + setOf("user:pass@example.com"), + setOf("例子.com"), + setOf("münich.example"), + setOf("https://example.com"), + setOf("example.com/path"), + ) + + invalidHostSets.forEach { hosts -> + assertFailsWith { + ScriptBridgeOriginPolicy.ExactHttpsHosts(hosts) + } + } + } + + @Test + fun `精确 HTTPS 主机策略接受合法大小写 ASCII 主机`() { + val hosts = setOf("EXAMPLE.com", "api.example.com") + + val policy = ScriptBridgeOriginPolicy.ExactHttpsHosts(hosts) + + assertEquals(hosts, policy.hosts) + } + + @Test + fun `精确 HTTPS 主机策略防御性复制传入的可变集合`() { + val hosts = mutableSetOf("trusted.example") + + val policy = ScriptBridgeOriginPolicy.ExactHttpsHosts(hosts) + hosts += "evil.example" + + assertEquals(setOf("trusted.example"), policy.hosts) + } + + @Test + fun `精确 HTTPS 主机策略的 copy 防御性复制传入的可变集合`() { + val policy = ScriptBridgeOriginPolicy.ExactHttpsHosts(setOf("trusted.example")) + val copiedHosts = mutableSetOf("trusted.example") + + val copiedPolicy = policy.copy(hosts = copiedHosts) + copiedHosts += "evil.example" + + assertEquals(setOf("trusted.example"), copiedPolicy.hosts) + assertEquals(copiedPolicy.hosts, copiedPolicy.component1()) + assertEquals(copiedPolicy, copiedPolicy.copy()) + } + @Test fun 控制器生命周期扩展仅在显式实现时接收控制器引用() { var attachedController: WebViewController? = null diff --git a/webview-ios/src/iosMain/kotlin/io/github/multiweb/ios/IosScriptBridgeInstaller.kt b/webview-ios/src/iosMain/kotlin/io/github/multiweb/ios/IosScriptBridgeInstaller.kt index c86fd68..ce14e83 100644 --- a/webview-ios/src/iosMain/kotlin/io/github/multiweb/ios/IosScriptBridgeInstaller.kt +++ b/webview-ios/src/iosMain/kotlin/io/github/multiweb/ios/IosScriptBridgeInstaller.kt @@ -296,6 +296,9 @@ internal data class IosScriptBridgeConfiguration( */ @OptIn(ExperimentalForeignApi::class) internal fun isTrustedJavaScriptUrl(url: String?, allowedHosts: Set): Boolean { + if (allowedHosts.isEmpty()) { + return false + } return isTrustedJavaScriptUrl(url, ScriptBridgeOriginPolicy.ExactHttpsHosts(allowedHosts)) } diff --git a/webview-ios/src/iosTest/kotlin/io/github/multiweb/ios/IosScriptBridgeConfigurationTest.kt b/webview-ios/src/iosTest/kotlin/io/github/multiweb/ios/IosScriptBridgeConfigurationTest.kt index 98007f8..2fd9676 100644 --- a/webview-ios/src/iosTest/kotlin/io/github/multiweb/ios/IosScriptBridgeConfigurationTest.kt +++ b/webview-ios/src/iosTest/kotlin/io/github/multiweb/ios/IosScriptBridgeConfigurationTest.kt @@ -39,6 +39,10 @@ class IosScriptBridgeConfigurationTest { assertTrue(isTrustedJavaScriptUrl("https://example.com:443/page", allowedHosts)) assertFalse(isTrustedJavaScriptUrl("https://example.com:8443/page", allowedHosts)) assertFalse(isTrustedJavaScriptUrl("http://example.com/page", allowedHosts)) + } + + @Test + fun `空的 HTTPS 主机集合拒绝可信 URL`() { assertFalse(isTrustedJavaScriptUrl("https://example.com/page", emptySet())) } From 36a7d65af377d14fc62dd64c8772b1488a75a436 Mon Sep 17 00:00:00 2001 From: generalio <1487144524@qq.com> Date: Tue, 18 Aug 2026 09:53:35 +0800 Subject: [PATCH 3/9] test(fixtures): restore cross-platform state flow coverage --- .../testing/FakeWebViewControllerTest.kt | 20 +++++-------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/webview-test-fixtures/src/commonTest/kotlin/io/github/multiweb/testing/FakeWebViewControllerTest.kt b/webview-test-fixtures/src/commonTest/kotlin/io/github/multiweb/testing/FakeWebViewControllerTest.kt index 685cde9..410ee3c 100644 --- a/webview-test-fixtures/src/commonTest/kotlin/io/github/multiweb/testing/FakeWebViewControllerTest.kt +++ b/webview-test-fixtures/src/commonTest/kotlin/io/github/multiweb/testing/FakeWebViewControllerTest.kt @@ -5,14 +5,7 @@ import io.github.multiweb.api.NavigationPolicy import io.github.multiweb.api.WebError import io.github.multiweb.api.WebErrorCategory import io.github.multiweb.api.WebRequest -import io.github.multiweb.api.WebViewState import io.github.multiweb.api.WebViewStateObservable -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.take -import kotlinx.coroutines.flow.toList -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -23,20 +16,17 @@ import kotlin.test.assertFailsWith class FakeWebViewControllerTest { @Test - fun `状态流会在页面状态改变时发出最新快照`() { + fun `状态流在页面状态改变时提供最新快照`() { val controller = FakeWebViewController() assertIs(controller) - val receivedStates = mutableListOf() - val collection = CoroutineScope(Dispatchers.Unconfined).launch { - controller.stateFlow.take(2).toList(receivedStates) - } + val initialUrl = controller.stateFlow.value.url controller.load(WebRequest("https://example.com/home")) - runBlocking { collection.join() } - assertEquals("https://example.com/home", controller.stateFlow.value.url) + val latestUrl = controller.stateFlow.value.url + + assertEquals(listOf(null, "https://example.com/home"), listOf(initialUrl, latestUrl)) assertEquals(controller.state, controller.stateFlow.value) - assertEquals(listOf(null, "https://example.com/home"), receivedStates.map { state -> state.url }) } @Test From 3f2ebfef198a58ae9bb34120a1fed4bec5bf3293 Mon Sep 17 00:00:00 2001 From: generalio <1487144524@qq.com> Date: Tue, 18 Aug 2026 09:54:24 +0800 Subject: [PATCH 4/9] docs(api): clarify persistent session default --- ...4\275\277\347\224\250\346\214\207\345\215\227.md" | 6 ++++-- .../kotlin/io/github/multiweb/api/WebViewPolicy.kt | 12 ++++++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git "a/docs/\344\275\277\347\224\250\346\214\207\345\215\227.md" "b/docs/\344\275\277\347\224\250\346\214\207\345\215\227.md" index fea78be..bc93818 100644 --- "a/docs/\344\275\277\347\224\250\346\214\207\345\215\227.md" +++ "b/docs/\344\275\277\347\224\250\346\214\207\345\215\227.md" @@ -282,14 +282,16 @@ controller.load(WebRequest("https://example.com")) ### 4.2 `WebViewConfig` -所有高风险配置默认按最小权限关闭: +除会话持久化外,所有高风险配置默认按最小权限关闭。persistentSessionEnabled 默认为 true, +原因是 Android 与 Desktop 无法可靠按控制器隔离临时会话。 +iOS 可显式设为 false 使用非持久化会话。 | 属性 | 默认值 | 行为与平台差异 | | --- | --- | --- | | `javaScriptEnabled` | `false` | 是否允许网页 JavaScript。Android 与 iOS 可关闭;Desktop 必须为 `true` 才能创建 JCEF 控制器;JS/Wasm 由用户浏览器控制。 | | `thirdPartyCookiesEnabled` | `false` | Android 可按 WebView 设置;iOS 由 WebKit 系统隐私策略决定;Desktop 必须为 `true` 才能创建控制器;JS/Wasm 由浏览器控制。 | | `fileAccessEnabled` | `false` | 是否允许 `file://` 导航。仅在确有本地内容需求时开启,并自行限制文件来源。 | -| `persistentSessionEnabled` | `true` | 是否持久化 Cookie、缓存等会话。iOS 为 `false` 时使用非持久化存储;Android 与 Desktop 当前不支持隔离临时会话,会拒绝该配置;JS/Wasm 不可控。 | +| `persistentSessionEnabled` | `true` | 会话。iOS 可设 false;Android/Desktop 拒绝;JS/Wasm 不可控。 | | `allowedHosts` | 空集合 | 内嵌页面精确主机名白名单;空集合只由导航策略决定。无通配符,`example.com` 不匹配 `m.example.com`。 | `allowedHosts` 是额外的控制层,不会替代 `NavigationPolicy`。白名单非空时,平台控制器会先执行精确匹配, diff --git a/webview-api/src/commonMain/kotlin/io/github/multiweb/api/WebViewPolicy.kt b/webview-api/src/commonMain/kotlin/io/github/multiweb/api/WebViewPolicy.kt index 45fe2ed..37a5fe7 100644 --- a/webview-api/src/commonMain/kotlin/io/github/multiweb/api/WebViewPolicy.kt +++ b/webview-api/src/commonMain/kotlin/io/github/multiweb/api/WebViewPolicy.kt @@ -3,7 +3,10 @@ package io.github.multiweb.api /** * WebView 的安全配置。 * - * 所有高风险能力均采用最小权限默认值;平台不支持的选项必须由对应实现明确说明。 + * 除会话持久化外,所有高风险能力均采用最小权限默认值。 + * + * 会话持久化的兼容默认值为 `true`,因为 Android 和 Desktop 无法按控制器隔离临时会话。 + * iOS 可显式设为 `false` 使用临时会话;平台不支持的选项必须由对应实现明确说明。 */ data class WebViewConfig( /** 是否允许页面执行 JavaScript,默认关闭。 */ @@ -12,7 +15,12 @@ data class WebViewConfig( val thirdPartyCookiesEnabled: Boolean = false, /** 是否允许页面访问本地文件,默认关闭。 */ val fileAccessEnabled: Boolean = false, - /** 是否持久化 Cookie、缓存等会话数据;关闭时使用临时会话。 */ + /** + * 是否持久化 Cookie、缓存等会话数据。 + * + * 默认 true 兼容 Android 和 Desktop 无法按控制器隔离临时会话。 + * iOS 可显式设为 false 使用临时会话。 + */ val persistentSessionEnabled: Boolean = true, /** 允许在内嵌浏览器中访问的主机名集合;空集合表示由导航策略决定。 */ val allowedHosts: Set = emptySet(), From 7bff172483b21134085e38d67b067e937e7c19ae Mon Sep 17 00:00:00 2001 From: generalio <1487144524@qq.com> Date: Tue, 18 Aug 2026 10:40:20 +0800 Subject: [PATCH 5/9] feat(workflow): make phase handoffs opt-in --- .codex/agents/workflow-implementer.toml | 11 +- .codex/agents/workflow-integrator.toml | 24 ++- .codex/agents/workflow-planner.toml | 4 + .codex/agents/workflow-verify-reviewer.toml | 4 + AGENTS.md | 23 ++- docs/agent-workflow-autonomous-handoff.md | 34 +++- docs/agent-workflow-task-template.md | 19 +- docs/agent-workflow.md | 25 ++- tools/agent_workflow.py | 46 ++++- tools/tests/test_agent_workflow.py | 211 ++++++++++++++++++-- 10 files changed, 357 insertions(+), 44 deletions(-) diff --git a/.codex/agents/workflow-implementer.toml b/.codex/agents/workflow-implementer.toml index 5a25a7a..059bd73 100644 --- a/.codex/agents/workflow-implementer.toml +++ b/.codex/agents/workflow-implementer.toml @@ -1,9 +1,16 @@ name = "workflow-implementer" -description = "在隔离 worktree 内实施任务契约明确授权的单一模块改动,并交接可复现的验证证据。" +description = "在隔离 worktree 内实施任务契约授权的单模块改动,并返回可复现验证证据。" developer_instructions = """ 你是 MultiWeb 的 Workflow Implementer。Supervisor 只在角色 worktree 中已存在 AGENTS.md、docs/agent-workflow.md 和 docs/agent-workflow-autonomous-handoff.md 三份文件时启动你;缺少任一文件时,必须报告“集成不完整”并停止。开始工作前必须阅读并遵守这三份文件;发生冲突时严格按 AGENTS.md > docs/agent-workflow.md > docs/agent-workflow-autonomous-handoff.md 的顺序处理并向 Supervisor 报告。仅在任务契约明确授予、且由你独占的文件和模块范围内工作。 -必须使用独立 worktree,实施最小改动并执行契约指定的最小相关验证。交接中必须列出实际修改文件与授权范围的对应关系、可应用 diff 或 worktree、执行命令及结果、未验证范围,以及公共 API、跨平台差异和安全默认值影响。 +默认不得生成或要求 `handoffs/` 阶段交接文档。 +只有维护者明确指出上下文超出并要求时,才允许创建并引用该目录中的文件。 +任务契约、计划、候选 SHA 和审查报告是账本或门禁证据,不属于交接文档。 + +必须使用独立 worktree,实施最小改动并执行契约指定的最小相关验证。 +返回 Supervisor 的阶段信息必须列出实际修改文件与授权范围的对应关系、 +可应用 diff 或 worktree、 +执行命令及结果、未验证范围,以及公共 API、跨平台差异和安全默认值影响。 不得写入其他任务的所有权范围,不得修改 API 基线、跨平台契约、发布或 Gradle 设置,除非任务契约明确且已串行授权;不得集成其他 Agent 改动、擅自解决接口签名、默认值、导航策略、JS 桥、安全设置或测试预期冲突。不得创建提交、推送或创建 PR,除非维护者另行明确授权。 """ diff --git a/.codex/agents/workflow-integrator.toml b/.codex/agents/workflow-integrator.toml index d0909ab..88b0e8e 100644 --- a/.codex/agents/workflow-integrator.toml +++ b/.codex/agents/workflow-integrator.toml @@ -1,9 +1,25 @@ name = "workflow-integrator" -description = "在专用集成 worktree 应用已交接改动,仅整理不改变语义的机械冲突并提供候选版本证据。" +description = "在专用集成 worktree 应用任务契约改动,仅整理机械冲突并提供候选版本证据。" developer_instructions = """ -你是 MultiWeb 的 Workflow Integrator。Supervisor 只在角色 worktree 中已存在 AGENTS.md、docs/agent-workflow.md 和 docs/agent-workflow-autonomous-handoff.md 三份文件时启动你;缺少任一文件时,必须报告“集成不完整”并停止。开始工作前必须阅读并遵守这三份文件;发生冲突时严格按 AGENTS.md > docs/agent-workflow.md > docs/agent-workflow-autonomous-handoff.md 的顺序处理并向 Supervisor 报告。仅在任务契约授权的专用集成 worktree 和已交接改动范围内工作。 +你是 MultiWeb 的 Workflow Integrator。Supervisor 只在角色 worktree 中已存在 AGENTS.md、 +docs/agent-workflow.md 和 docs/agent-workflow-autonomous-handoff.md 三份文件时启动你; +缺少任一文件时,必须报告“集成不完整”并停止。 +开始工作前必须阅读并遵守这三份文件; +发生冲突时严格按 AGENTS.md > docs/agent-workflow.md > docs/agent-workflow-autonomous-handoff.md +的顺序处理并向 Supervisor 报告。仅在任务契约授权的专用集成 worktree 和 Supervisor +转交的改动范围内工作。 -只可应用完整交接,并处理不改变语义的机械冲突,例如无关 import 排序或相邻文本合并。集成前检查交接范围不重叠,完成后提供合并清单、git diff --check 结果和固定候选提交所需证据;是否创建候选提交仍须遵守 AGENTS.md 和维护者的明确授权。 +默认不得生成或要求 `handoffs/` 阶段交接文档。 +只有维护者明确指出上下文超出并要求时,才允许创建并引用该目录中的文件。 +任务契约、计划、候选 SHA 和审查报告是账本或门禁证据,不属于交接文档。 -不得修改产品业务逻辑、公共 API、默认值、导航策略、JS 桥、安全设置、测试预期或 API 基线来消除冲突或失败。遇到这些语义冲突、P0/P1 或交接不完整时,必须退回原 Implementer 或 Planner,不得自行猜测。不得写入契约未授权范围、推送或创建 PR。 +只可应用任务契约授权且由 Supervisor 转交的改动,并处理不改变语义的机械冲突, +例如无关 import 排序或相邻文本合并。集成前检查改动范围不重叠,完成后提供合并清单、 +`git diff --check` 结果和固定候选提交所需证据;是否创建候选提交仍须遵守 AGENTS.md +和维护者的明确授权。 + +不得修改产品业务逻辑、公共 API、默认值、导航策略、JS 桥、安全设置、测试预期 +或 API 基线 +来消除冲突或失败。遇到这些语义冲突、P0/P1 或阶段信息不完整时,必须退回原 Implementer +或 Planner,不得自行猜测。不得写入契约未授权范围、推送或创建 PR。 """ diff --git a/.codex/agents/workflow-planner.toml b/.codex/agents/workflow-planner.toml index 65b08b8..9200676 100644 --- a/.codex/agents/workflow-planner.toml +++ b/.codex/agents/workflow-planner.toml @@ -6,5 +6,9 @@ developer_instructions = """ 仅进行只读分析。不得写入仓库、任务账本、产品代码、测试、API 基线、构建或发布配置,也不得创建提交、推送或创建 PR。将任务契约、任务 DAG、基线 SHA、独占文件/模块所有权、依赖、最小验证命令以及公共 API、跨平台和安全默认值影响仅以结构化结果返回 Supervisor;只能由 Supervisor 使用临时文件后原子替换落盘。 +默认不得生成或要求 `handoffs/` 阶段交接文档。 +只有维护者明确指出上下文超出并要求时,才允许创建并引用该目录中的文件。 +任务契约、计划、候选 SHA 和审查报告是账本或门禁证据,不属于交接文档。 + 不得实施修复、替其他角色裁决语义冲突,或通过省略风险、放宽默认安全策略、修改接口约定来使任务看似可执行。契约不完整、需要维护者授权或发现 P0/P1 时,明确标记阻断原因与唯一下一步。 """ diff --git a/.codex/agents/workflow-verify-reviewer.toml b/.codex/agents/workflow-verify-reviewer.toml index f76ba22..89c8900 100644 --- a/.codex/agents/workflow-verify-reviewer.toml +++ b/.codex/agents/workflow-verify-reviewer.toml @@ -6,5 +6,9 @@ developer_instructions = """ 先执行任务契约中的最小验证,再根据实际改动审查公共 API、跨平台一致性、JS 桥与安全默认值。结果必须包含候选 SHA、实际命令与退出结果、未验证项、按 P0/P1/P2 分类的问题、文件位置,以及唯一的 PASS 或 REJECT 裁决。P0/P1 必须 REJECT;P2 必须写入风险记录;候选 SHA 变化后旧裁决立即失效。 +默认不得生成或要求 `handoffs/` 阶段交接文档。 +只有维护者明确指出上下文超出并要求时,才允许创建并引用该目录中的文件。 +任务契约、计划、候选 SHA 和审查报告是账本或门禁证据,不属于交接文档。 + 不得写入候选代码、测试、API 基线、任务契约或账本,不得修改任何内容来制造 PASS,不得替 Integrator 处理语义冲突,也不得创建提交、推送或创建 PR。将审查报告仅返回 Supervisor;只能由 Supervisor 使用临时文件后原子替换落盘。 """ diff --git a/AGENTS.md b/AGENTS.md index f52400d..6d754d8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,21 +17,32 @@ ## 多 Agent 协作 - 中大型需求必须由 Supervisor 创建任务 ID、任务契约和 `.codex/workflow/runs//` 账本,再依次启动 - Planner、独占 worktree 的 Implementer、Integrator 和固定候选 SHA 的 Verify-Reviewer。没有交接产物、候选 + Planner、独占 worktree 的 Implementer、Integrator 和固定候选 SHA 的 Verify-Reviewer。 + 没有任务契约、候选 SHA 或 `PASS` 裁决不得进入下一状态。若当前会话无法启动所需子 Agent,必须记录阻断原因,不得静默降级为单 Agent。 +- 默认不得生成或要求 `handoffs/` 阶段交接文档。只有维护者明确指出上下文超出并要求时, + 才允许创建并引用该目录中的文件。任务契约、计划、候选 SHA 与审查报告 + 属于账本或门禁证据, + 不属于交接文档。 - Planner 必须记录基线 SHA、依赖、独占文件/模块范围、最小验证命令及公共 API、跨平台和安全影响。只有在契约明确 `TRIVIAL` 理由和快速路径验证命令时,才可不走完整并行流程。 - Supervisor 每次状态变化先调用 `tools/agent_workflow.py transition` 写入账本,再执行下一动作;进入 `VALIDATING` 必须传 `--candidate-sha`,进入 `REVIEWED` 必须传 `--verdict PASS|REJECT`。恢复前先 `validate`,再用 `resume` - 读取唯一下一步。任务目录不得保存凭据、Token 或完整环境变量。 + 读取唯一下一步。进入 `PAUSED` 或 `BLOCKED` 必须同时传入非空单行的 `--stop-reason` 和 + `--next-action`;其他状态传入任一停止参数必须失败且不写账本。任务目录 + 不得保存凭据、Token + 或完整环境变量。 - 并行实施必须使用相互隔离的 worktree;同一文件或模块同一时刻只能有一个 Implementer 写入。`webview-api`、 `webview-extension-api`、API 基线、Gradle 设置、发布配置和跨平台契约必须串行处理。 -- Integrator 只能应用已交接的改动并解决不改变语义的机械冲突。接口签名、默认值、导航策略、JS 桥、安全设置或测试 - 预期冲突必须退回原 Implementer 或 Planner,不得在集成阶段自行修复。 +- Integrator 只能应用任务契约授权且由 Supervisor 转交的改动,并解决不改变语义的 + 机械冲突。接口签名、默认值、导航策略、JS 桥、安全设置或测试预期冲突必须退回原 + Implementer 或 Planner, + 不得在集成阶段自行修复。 - Verify-Reviewer 只能在干净 worktree 对固定候选 SHA 运行验证和审查,禁止修改候选代码、测试或 API 基线。候选 SHA 变化后,先前裁决立即失效。P0/P1 必须 `REJECT` 并阻断提交/PR;P2 必须写入风险与兼容性说明。 -- 项目角色配置位于 `.codex/agents/`。角色须先阅读本文件、`docs/agent-workflow.md` 和运行协议;优先级为 - `AGENTS.md`、`docs/agent-workflow.md`、任务交接文档。 +- 项目角色配置位于 `.codex/agents/`。角色须先阅读本文件、`docs/agent-workflow.md` 和运行协议; + 优先级为 `AGENTS.md`、`docs/agent-workflow.md`、运行协议;维护者明确要求的额外交接文档 + 仅作为补充信息。 ## 注释与文档 diff --git a/docs/agent-workflow-autonomous-handoff.md b/docs/agent-workflow-autonomous-handoff.md index 019dece..1126f2d 100644 --- a/docs/agent-workflow-autonomous-handoff.md +++ b/docs/agent-workflow-autonomous-handoff.md @@ -3,20 +3,27 @@ 本文档说明既有多 Agent 工作流的交接约定,不增加角色、状态、权限或审批能力。出现冲突时,文件优先级严格为 `AGENTS.md` > `docs/agent-workflow.md` > 本文档。 -## 角色与最小交接 +## 角色与最小阶段信息 Supervisor 负责创建 `.codex/workflow/runs//` 账本、按既有状态机推进状态、启动下一角色,并保留公开的 相对证据路径和 Git SHA。进入下一状态前,先使用 `tools/agent_workflow.py transition` 记录状态变化和证据。 +默认不得生成或要求 `handoffs/` 阶段交接文档。只有维护者明确指出上下文超出并要求时, +才允许创建并引用该目录中的文件。任务契约、计划、候选 SHA 和审查报告 +是账本或门禁证据, +不属于交接文档。 + Planner 仅做只读分析,向 Supervisor 返回任务契约、基线 SHA、任务 DAG、独占文件或模块、依赖、最小验证命令、验收 标准,以及公共 API、平台和安全默认值影响。`TRIVIAL` 契约必须写明理由、独占范围和快速路径验证命令。 -Implementer 只能在独立 worktree 内修改任务契约明确授权且由其独占的范围。交接至少包含:任务和基线、实际修改文件 -与所有权映射、可应用 diff 或 worktree、执行命令及退出结果、未验证范围,以及公共 API、跨平台差异和安全默认值影响。 +Implementer 只能在独立 worktree 内修改任务契约明确授权且由其独占的范围。返回给 Supervisor +的阶段信息至少包含:任务和基线、实际修改文件与所有权映射、可应用 diff 或 worktree、 +执行命令及退出结果、未验证范围,以及公共 API、跨平台差异和安全默认值影响。 -Integrator 只能应用完整交接并处理不改变语义的机械冲突。交接至少包含:已合并的交接项、范围不重叠检查、机械冲突 -处理说明、`git diff --check` 结果,以及固定候选提交所需的候选 SHA 证据。是否创建提交仍须遵守 `AGENTS.md` 和维护者 -的明确授权。 +Integrator 只能应用任务契约授权且由 Supervisor 转交的改动,并处理不改变语义的机械冲突。 +返回的阶段信息至少包含:已合并改动、范围不重叠检查、机械冲突处理说明、 +`git diff --check` 结果,以及固定候选提交所需的候选 SHA 证据。是否创建提交 +仍须遵守 `AGENTS.md` 和维护者的明确授权。 Verify-Reviewer 只在干净 worktree 对固定候选 SHA 做只读验证和审查。报告至少包含:候选 SHA、实际执行命令及退出 结果、未验证范围、公共 API、跨平台一致性、JS 桥与安全默认值检查、按 P0/P1/P2 分类且带文件位置的问题,以及唯一的 @@ -24,8 +31,9 @@ Verify-Reviewer 只在干净 worktree 对固定候选 SHA 做只读验证和审 ## 账本、状态与证据 -账本目录只保存公开状态、相对证据路径和 Git SHA;既有文件包括 `state.json`、`events.jsonl`、`task-contract.md`、 -`plan.json`、`handoffs/`、`candidate.sha`、`review-report.md` 与 `resume.md`。事件必须连续且时间严格递增,最后一条 +账本目录只保存公开状态、相对证据路径和 Git SHA;默认文件包括 `state.json`、 +`events.jsonl`、`task-contract.md`、`plan.json`、`candidate.sha`、`review-report.md` +与 `resume.md`。事件必须连续且时间严格递增,最后一条 事件时间必须与 `state.json` 的 `lastEventAt` 相同。 状态仅按既有状态机迁移:`PLANNED` 可进入 `IMPLEMENTING` 或 `PAUSED`;`IMPLEMENTING` 可进入 @@ -38,11 +46,17 @@ Verify-Reviewer 只在干净 worktree 对固定候选 SHA 做只读验证和审 `review-report.md` 证据。进入 `COMMIT_READY` 前,`candidate.sha` 与 `review-report.md` 必须绑定同一候选 SHA,裁决为 `PASS`,且不存在未解决 P0/P1。 +普通阶段迁移使用既有 `task-contract.md`;`VALIDATING` 使用 `candidate.sha`;`REVIEWED`、 +`COMMIT_READY`、`PR_READY` 和返工使用 `review-report.md`。进入 `PAUSED` 或 `BLOCKED` 时, +必须同时传入非空单行的 `--stop-reason` 与 `--next-action`;其他状态传入任一停止参数 +必须失败且不写账本。成功停止会写入现有状态字段及事件摘要。 + ## 退回、阻断与恢复 接口签名、默认值、导航策略、JS 桥、安全设置或测试预期发生语义冲突时,Integrator 不得自行解决,必须退回原 -Implementer 或 Planner。交接不完整、需要维护者或凭据授权、无法启动所需角色、发现 P0/P1 时,Supervisor 必须暂停或 -阻断,记录原因和唯一下一步,不能静默以单 Agent 完成。 +Implementer 或 Planner。阶段信息不完整、需要维护者或凭据授权、无法启动所需角色、 +发现 P0/P1 时, +Supervisor 必须暂停或阻断,记录原因和唯一下一步,不能静默以单 Agent 完成。 P0/P1 必须使 Verify-Reviewer 给出 `REJECT`,并阻断提交或 PR;P2 不阻断,但必须写入 PR 风险与兼容性说明。候选 SHA 一旦变化,之前的审查裁决立即失效;返工必须从 `REVIEWED` 回到 `IMPLEMENTING`,清除旧审查绑定后重新集成和审查。 diff --git a/docs/agent-workflow-task-template.md b/docs/agent-workflow-task-template.md index b59664e..0fbbb9a 100644 --- a/docs/agent-workflow-task-template.md +++ b/docs/agent-workflow-task-template.md @@ -2,6 +2,11 @@ > 未填写字段不得以“默认”或口头说明替代;契约完整并初始化账本后才能进入 `IMPLEMENTING`。 +> 默认不得生成或要求 `handoffs/` 阶段交接文档。只有维护者明确指出上下文超出并要求时, +> 才允许创建并引用该目录中的文件。任务契约、计划、候选 SHA 和审查报告 +> 属于账本或门禁证据, +> 不属于交接文档。 + ## 基本信息 - 任务编号: @@ -14,7 +19,7 @@ ## 计划与所有权 -| Agent | 独占文件或模块 | 前置依赖 | 交接产物 | 最小验证 | +| Agent | 独占文件或模块 | 前置依赖 | 返回的阶段信息 | 最小验证 | | --- | --- | --- | --- | --- | | | | | | | @@ -50,6 +55,12 @@ `plan.json` 至少包含 `schemaVersion`、`taskId`、`baselineSha`、任务 DAG、独占范围、依赖、验证命令和验收标准。 +进入 `PAUSED` 或 `BLOCKED` 时,Supervisor 必须同时传入非空单行的 `--stop-reason` 和 +`--next-action`;其他状态传入任一停止参数必须失败且不写账本。普通阶段 +使用 `task-contract.md`, +`VALIDATING` 使用 `candidate.sha`,`REVIEWED`、`COMMIT_READY`、`PR_READY` 和返工使用 +`review-report.md`。 + ## 验收与验证 - 实施阶段命令: @@ -57,9 +68,9 @@ - 无法执行的运行时范围、原因和替代证据: - 公共 API 变更时的 `./gradlew apiCheck` 结果: -## 交接与裁决 +## 阶段信息与裁决 -- 实施 Agent:修改文件、worktree/diff、命令结果、风险: -- Integrator:合并交接项、机械冲突、候选 SHA、`git diff --check`: +- 实施 Agent 返回:修改文件、worktree/diff、命令结果、风险: +- Integrator 返回:合并改动、机械冲突、候选 SHA、`git diff --check`: - Verify-Reviewer:审查 SHA、命令结果、未验证范围、P0/P1/P2、`PASS` / `REJECT`: - PR 记录:任务契约路径、候选提交 SHA、账本验证 `PASS`、审查裁决 `PASS` 及平台验证证据: diff --git a/docs/agent-workflow.md b/docs/agent-workflow.md index 785c983..0fd01b6 100644 --- a/docs/agent-workflow.md +++ b/docs/agent-workflow.md @@ -14,7 +14,7 @@ P0/P1、候选 SHA 变化或需要 GitHub/凭据授权时,Supervisor 必须暂 每个任务使用 `.codex/workflow/runs//`,只保存公开的状态、相对证据路径和 Git SHA: ```text -state.json events.jsonl task-contract.md plan.json handoffs/ +state.json events.jsonl task-contract.md plan.json candidate.sha review-report.md resume.md ``` @@ -22,6 +22,11 @@ candidate.sha review-report.md resume.md `candidateSha`、`verdict`、`nextAction`、`lastEventAt` 和 `stopReason`;`PAUSED` 额外记录 `pausedFromStatus`。事件必须 连续、时间严格递增,末事件时间必须等于 `lastEventAt`。 +默认不得生成或要求 `handoffs/` 阶段交接文档。只有维护者明确指出上下文超出并要求时, +才允许创建并引用该目录中的文件。历史账本中的交接文件保留不删。任务契约、计划、 +候选 SHA +和审查报告是账本或门禁证据,不属于交接文档。 + | 状态 | 可进入的下一状态 | | --- | --- | | `PLANNED` | `IMPLEMENTING`、`PAUSED` | @@ -41,17 +46,29 @@ python3 tools/agent_workflow.py validate .codex/workflow/runs/ python3 tools/agent_workflow.py transition .codex/workflow/runs/ --actor --evidence python3 tools/agent_workflow.py transition .codex/workflow/runs/ VALIDATING --actor INTEGRATOR --evidence candidate.sha --candidate-sha python3 tools/agent_workflow.py transition .codex/workflow/runs/ REVIEWED --actor VERIFY_REVIEWER --evidence review-report.md --verdict PASS +python3 tools/agent_workflow.py transition .codex/workflow/runs/ PAUSED \ + --actor SUPERVISOR --evidence task-contract.md \ + --stop-reason "<原因>" --next-action "<唯一下一步>" python3 tools/agent_workflow.py resume .codex/workflow/runs/ ``` +普通阶段迁移使用既有 `task-contract.md`;`VALIDATING` 使用 `candidate.sha`;`REVIEWED`、 +`COMMIT_READY`、`PR_READY` 和返工使用 `review-report.md`。进入 `PAUSED` 或 `BLOCKED` 时, +`--stop-reason` 与 `--next-action` 必须同时为非空单行;其他目标状态携带任一停止参数 +必须失败,且 `state.json` 与 `events.jsonl` 不得变更。成功停止会把两项写入 +现有状态字段,并在事件摘要中记录。 + 验证器只接受任务目录内常规证据文件,拒绝关键账本符号链接。它使用 Unix `fcntl` 非阻塞租约,要求 Python `>=3.11`; Windows 或不具备 `fcntl` 的环境应记录为验证限制。进入 `COMMIT_READY` 前必须存在同一候选 SHA 的 `candidate.sha`、 `review-report.md`、`PASS` 及无未解决 P0/P1。 -## 交接与裁决 +## 阶段信息与裁决 -Implementer 交接必须包含任务/基线、实际修改文件与所有权映射、可应用 diff 或 worktree、命令退出结果、未验证范围, -以及公共 API、平台差异和安全默认值影响。Integrator 必须检查交接范围不重叠并记录 `git diff --check`。 +Implementer 必须向 Supervisor 返回任务/基线、实际修改文件与所有权映射、 +可应用 diff 或 worktree、命令退出结果、未验证范围,以及公共 API、平台差异和 +安全默认值影响。 +Integrator 必须检查改动范围 +不重叠并记录 `git diff --check`。 Verify-Reviewer 先运行契约的最小验证,再检查公共 API、跨平台一致性、JS 桥、安全默认值和受影响测试。报告必须写入 候选 SHA、命令与退出结果、未验证范围、P0/P1/P2 和唯一的 `PASS` 或 `REJECT`。P0/P1 必须 `REJECT`;P2 不阻断但 diff --git a/tools/agent_workflow.py b/tools/agent_workflow.py index 6ff0db9..0955842 100644 --- a/tools/agent_workflow.py +++ b/tools/agent_workflow.py @@ -77,6 +77,7 @@ ) ATTEMPT_FIELDS = ("PLANNING", "IMPLEMENTING", "VALIDATING", "REVIEWING") +STOPPED_STATUSES = frozenset({"PAUSED", "BLOCKED"}) EVENT_FIELDS = ("at", "actor", "from", "to", "evidence", "summary") @@ -398,6 +399,30 @@ def resolve_evidence_path(run_directory: Path, evidence: Path) -> Path: return evidence_path +def is_nonempty_single_line(value: object) -> bool: + """确认停止原因和下一步可直接写入单行账本字段。""" + return isinstance(value, str) and bool(value.strip()) and value.splitlines() == [value] + + +def validate_stop_options( + run_directory: Path, + next_status: str, + stop_reason: str | None, + next_action: str | None, +) -> None: + """限制停止参数只能用于需要停止上下文的状态。""" + if next_status in STOPPED_STATUSES: + if not is_nonempty_single_line(stop_reason) or not is_nonempty_single_line(next_action): + raise ValueError( + f"任务目录 {run_directory} 进入 PAUSED 或 BLOCKED 必须同时提供 " + "--stop-reason 和 --next-action,且均为非空单行。" + ) + elif stop_reason is not None or next_action is not None: + raise ValueError( + f"任务目录 {run_directory} 仅在进入 PAUSED 或 BLOCKED 时接受 --stop-reason 和 --next-action。" + ) + + def transition_run( run_directory: Path, next_status: str, @@ -405,12 +430,15 @@ def transition_run( evidence: Path, candidate_sha: str | None, verdict: str | None, + stop_reason: str | None, + next_action: str | None, ) -> None: """验证状态迁移并原子更新当前账本,再追加审计事件。""" state = validate_run(run_directory) old_status = state["status"] if next_status not in STATUSES: raise ValueError(f"任务目录 {run_directory} 的目标状态 {next_status} 非法;请使用受支持的工作流状态。") + validate_stop_options(run_directory, next_status, stop_reason, next_action) if not is_allowed_transition(state, next_status): raise ValueError( f"任务目录 {run_directory} 不允许从 {old_status} 迁移到 {next_status};请迁移到该状态允许的下一阶段。" @@ -465,8 +493,14 @@ def transition_run( else: state.pop("pausedFromStatus", None) state["currentStage"] = STAGE_FOR_STATUS[next_status] + if next_status in STOPPED_STATUSES: + state["stopReason"] = stop_reason + state["nextAction"] = next_action state["status"] = next_status state["lastEventAt"] = next_event_time(state["lastEventAt"]) + summary = "状态迁移已验证" + if next_status in STOPPED_STATUSES: + summary = f"停止原因:{stop_reason};唯一下一步:{next_action}" append_event( run_directory, { @@ -475,7 +509,7 @@ def transition_run( "from": old_status, "to": next_status, "evidence": evidence_path.relative_to(run_directory).as_posix(), - "summary": "状态迁移已验证", + "summary": summary, }, ) atomic_write_json(run_directory / "state.json", state) @@ -647,6 +681,8 @@ def main() -> int: transition_parser.add_argument("--evidence", type=Path, required=True) transition_parser.add_argument("--candidate-sha") transition_parser.add_argument("--verdict", choices=("PASS", "REJECT")) + transition_parser.add_argument("--stop-reason") + transition_parser.add_argument("--next-action") resume_parser = subparsers.add_parser("resume") resume_parser.add_argument("run_directory", type=Path) arguments = parser.parse_args() @@ -661,6 +697,12 @@ def main() -> int: return 0 if arguments.command == "transition": run_directory = arguments.run_directory.resolve() + validate_stop_options( + run_directory, + arguments.next_status, + arguments.stop_reason, + arguments.next_action, + ) with workflow_lock(run_directory): transition_run( run_directory, @@ -669,6 +711,8 @@ def main() -> int: arguments.evidence, arguments.candidate_sha, arguments.verdict, + arguments.stop_reason, + arguments.next_action, ) print(f"状态迁移成功:{run_directory} -> {arguments.next_status}") return 0 diff --git a/tools/tests/test_agent_workflow.py b/tools/tests/test_agent_workflow.py index 6f7555a..5e765ff 100644 --- a/tools/tests/test_agent_workflow.py +++ b/tools/tests/test_agent_workflow.py @@ -45,6 +45,13 @@ def run_cli(self, *arguments: str) -> subprocess.CompletedProcess[str]: check=False, ) + def snapshot_run_directory(self, run_directory: Path) -> dict[str, bytes | None]: + """记录运行目录的文件集合、空目录和文件字节,用于验证失败路径无副作用。""" + return { + path.relative_to(run_directory).as_posix(): path.read_bytes() if path.is_file() else None + for path in sorted(run_directory.rglob("*"), key=lambda item: item.as_posix()) + } + def test_all_workflow_role_configs_require_the_autonomous_handoff_protocol(self) -> None: protocol_path = REPOSITORY_ROOT / "docs" / "agent-workflow-autonomous-handoff.md" protocol_relative_path = protocol_path.relative_to(REPOSITORY_ROOT).as_posix() @@ -63,6 +70,34 @@ def test_all_workflow_role_configs_require_the_autonomous_handoff_protocol(self) f"{role_configuration_path.relative_to(REPOSITORY_ROOT)} 必须声明工作流协议文件", ) + def test_workflow_documents_and_role_configs_forbid_default_handoff_documents(self) -> None: + default_handoff_policy = "默认不得生成或要求 `handoffs/` 阶段交接文档" + workflow_document_paths = ( + REPOSITORY_ROOT / "AGENTS.md", + REPOSITORY_ROOT / "docs" / "agent-workflow.md", + REPOSITORY_ROOT / "docs" / "agent-workflow-autonomous-handoff.md", + REPOSITORY_ROOT / "docs" / "agent-workflow-task-template.md", + ) + role_configuration_paths = ( + REPOSITORY_ROOT / ".codex" / "agents" / "workflow-implementer.toml", + REPOSITORY_ROOT / ".codex" / "agents" / "workflow-integrator.toml", + REPOSITORY_ROOT / ".codex" / "agents" / "workflow-planner.toml", + REPOSITORY_ROOT / ".codex" / "agents" / "workflow-verify-reviewer.toml", + ) + + for workflow_document_path in workflow_document_paths: + self.assertIn( + default_handoff_policy, + workflow_document_path.read_text(encoding="utf-8"), + f"{workflow_document_path.relative_to(REPOSITORY_ROOT)} 必须禁止默认阶段交接文档", + ) + for role_configuration_path in role_configuration_paths: + self.assertIn( + default_handoff_policy, + role_configuration_path.read_text(encoding="utf-8"), + f"{role_configuration_path.relative_to(REPOSITORY_ROOT)} 必须禁止默认阶段交接文档", + ) + def test_validate_accepts_a_planned_run(self) -> None: run_directory = self.copy_fixture("valid") @@ -129,6 +164,155 @@ def test_transition_rejects_an_illegal_status_change(self) -> None: self.assertIn("PLANNED", result.stderr) self.assertIn("允许", result.stderr) + def test_transition_to_stopped_states_requires_both_stop_options_without_mutating_ledger(self) -> None: + option_sets = ( + ("neither", ()), + ("reason-only", ("--stop-reason", "等待维护者授权")), + ("action-only", ("--next-action", "恢复实施阶段")), + ) + for next_status in ("PAUSED", "BLOCKED"): + for label, stop_options in option_sets: + with self.subTest(next_status=next_status, label=label): + run_directory = self.copy_fixture("valid", f"stop-options-{next_status}-{label}") + if next_status == "BLOCKED": + implementing = self.run_cli( + "transition", + str(run_directory), + "IMPLEMENTING", + "--actor", + "SUPERVISOR", + "--evidence", + "task-contract.md", + ) + self.assertEqual(implementing.returncode, 0, implementing.stderr) + (run_directory / ".workflow.lock").unlink() + before_run_directory = self.snapshot_run_directory(run_directory) + self.assertNotIn(".workflow.lock", before_run_directory) + + result = self.run_cli( + "transition", + str(run_directory), + next_status, + "--actor", + "SUPERVISOR", + "--evidence", + "task-contract.md", + *stop_options, + ) + + self.assertNotEqual(result.returncode, 0) + self.assertIn("--stop-reason", result.stderr) + self.assertIn("--next-action", result.stderr) + self.assertEqual(self.snapshot_run_directory(run_directory), before_run_directory) + + def test_transition_to_stopped_states_rejects_empty_or_multiline_stop_options(self) -> None: + invalid_stop_options = ( + ("empty-reason", "", "恢复实施阶段"), + ("blank-reason", " ", "恢复实施阶段"), + ("multiline-reason", "等待维护者\n授权", "恢复实施阶段"), + ("empty-action", "等待维护者授权", ""), + ("blank-action", "等待维护者授权", " "), + ("multiline-action", "等待维护者授权", "恢复\n实施阶段"), + ) + for label, stop_reason, next_action in invalid_stop_options: + with self.subTest(label=label): + run_directory = self.copy_fixture("valid", f"invalid-stop-options-{label}") + before_run_directory = self.snapshot_run_directory(run_directory) + self.assertNotIn(".workflow.lock", before_run_directory) + + result = self.run_cli( + "transition", + str(run_directory), + "PAUSED", + "--actor", + "SUPERVISOR", + "--evidence", + "task-contract.md", + "--stop-reason", + stop_reason, + "--next-action", + next_action, + ) + + self.assertNotEqual(result.returncode, 0) + self.assertIn("非空单行", result.stderr) + self.assertEqual(self.snapshot_run_directory(run_directory), before_run_directory) + + def test_transition_rejects_stop_options_for_non_stopped_states_without_mutating_ledger(self) -> None: + option_sets = ( + ("reason-only", ("--stop-reason", "等待维护者授权")), + ("action-only", ("--next-action", "恢复实施阶段")), + ( + "both", + ("--stop-reason", "等待维护者授权", "--next-action", "恢复实施阶段"), + ), + ) + for label, stop_options in option_sets: + with self.subTest(label=label): + run_directory = self.copy_fixture("valid", f"non-stop-options-{label}") + before_run_directory = self.snapshot_run_directory(run_directory) + self.assertNotIn(".workflow.lock", before_run_directory) + + result = self.run_cli( + "transition", + str(run_directory), + "IMPLEMENTING", + "--actor", + "SUPERVISOR", + "--evidence", + "task-contract.md", + *stop_options, + ) + + self.assertNotEqual(result.returncode, 0) + self.assertIn("PAUSED 或 BLOCKED", result.stderr) + self.assertEqual(self.snapshot_run_directory(run_directory), before_run_directory) + + def test_transition_to_stopped_states_records_stop_context_in_state_event_and_resume(self) -> None: + for next_status in ("PAUSED", "BLOCKED"): + with self.subTest(next_status=next_status): + run_directory = self.copy_fixture("valid", f"stopped-state-{next_status}") + if next_status == "BLOCKED": + implementing = self.run_cli( + "transition", + str(run_directory), + "IMPLEMENTING", + "--actor", + "SUPERVISOR", + "--evidence", + "task-contract.md", + ) + self.assertEqual(implementing.returncode, 0, implementing.stderr) + stop_reason = f"{next_status} 等待维护者授权" + next_action = f"{next_status} 恢复实施阶段" + + transition = self.run_cli( + "transition", + str(run_directory), + next_status, + "--actor", + "SUPERVISOR", + "--evidence", + "task-contract.md", + "--stop-reason", + stop_reason, + "--next-action", + next_action, + ) + + self.assertEqual(transition.returncode, 0, transition.stderr) + state = json.loads((run_directory / "state.json").read_text(encoding="utf-8")) + event = json.loads((run_directory / "events.jsonl").read_text(encoding="utf-8").splitlines()[-1]) + resume = self.run_cli("resume", str(run_directory)) + self.assertEqual(state["status"], next_status) + self.assertEqual(state["stopReason"], stop_reason) + self.assertEqual(state["nextAction"], next_action) + self.assertIn(stop_reason, event["summary"]) + self.assertIn(next_action, event["summary"]) + self.assertEqual(resume.returncode, 0, resume.stderr) + self.assertIn(stop_reason, resume.stdout) + self.assertIn(next_action, resume.stdout) + def test_transition_records_a_legal_status_chain(self) -> None: run_directory = self.copy_fixture("valid") @@ -141,9 +325,6 @@ def test_transition_records_a_legal_status_chain(self) -> None: "--evidence", "task-contract.md", ) - handoff_directory = run_directory / "handoffs" - handoff_directory.mkdir() - (handoff_directory / "implementer.md").write_text("完成交接\n", encoding="utf-8") integrating = self.run_cli( "transition", str(run_directory), @@ -151,7 +332,7 @@ def test_transition_records_a_legal_status_chain(self) -> None: "--actor", "IMPLEMENTER", "--evidence", - "handoffs/implementer.md", + "task-contract.md", ) state = json.loads((run_directory / "state.json").read_text(encoding="utf-8")) @@ -162,6 +343,7 @@ def test_transition_records_a_legal_status_chain(self) -> None: self.assertEqual(state["currentStage"], "INTEGRATING") self.assertEqual(state["attempts"]["IMPLEMENTING"], 1) self.assertEqual(len(events), 2) + self.assertFalse((run_directory / "handoffs").exists()) def test_transition_cli_completes_a_full_passed_chain_without_editing_state(self) -> None: run_directory = self.copy_fixture("valid") @@ -178,9 +360,6 @@ def test_transition_cli_completes_a_full_passed_chain_without_editing_state(self "task-contract.md", ) ] - handoff_directory = run_directory / "handoffs" - handoff_directory.mkdir() - (handoff_directory / "implementer.md").write_text("完成交接\n", encoding="utf-8") results.append( self.run_cli( "transition", @@ -189,7 +368,7 @@ def test_transition_cli_completes_a_full_passed_chain_without_editing_state(self "--actor", "IMPLEMENTER", "--evidence", - "handoffs/implementer.md", + "task-contract.md", ) ) (run_directory / "candidate.sha").write_text(f"{candidate_sha}\n", encoding="utf-8") @@ -254,6 +433,7 @@ def test_transition_cli_completes_a_full_passed_chain_without_editing_state(self self.assertEqual(state["status"], "PR_READY") self.assertEqual(state["candidateSha"], candidate_sha) self.assertEqual(state["verdict"], "PASS") + self.assertFalse((run_directory / "handoffs").exists()) events = [ json.loads(line) for line in (run_directory / "events.jsonl").read_text(encoding="utf-8").splitlines() @@ -276,9 +456,6 @@ def test_transition_to_validating_requires_candidate_sha_option(self) -> None: ).returncode, 0, ) - handoff_directory = run_directory / "handoffs" - handoff_directory.mkdir() - (handoff_directory / "implementer.md").write_text("完成交接\n", encoding="utf-8") self.assertEqual( self.run_cli( "transition", @@ -287,7 +464,7 @@ def test_transition_to_validating_requires_candidate_sha_option(self) -> None: "--actor", "IMPLEMENTER", "--evidence", - "handoffs/implementer.md", + "task-contract.md", ).returncode, 0, ) @@ -844,6 +1021,10 @@ def test_planning_retry_is_limited_when_resuming_a_paused_plan(self) -> None: "SUPERVISOR", "--evidence", "task-contract.md", + "--stop-reason", + "等待维护者授权", + "--next-action", + "恢复 PLANNED 阶段并等待维护者授权", ) first_resume = self.run_cli( "transition", @@ -862,6 +1043,10 @@ def test_planning_retry_is_limited_when_resuming_a_paused_plan(self) -> None: "SUPERVISOR", "--evidence", "task-contract.md", + "--stop-reason", + "等待维护者授权", + "--next-action", + "恢复 PLANNED 阶段并等待维护者授权", ) exhausted_resume = self.run_cli( "transition", @@ -918,7 +1103,7 @@ def test_rework_can_review_a_new_candidate_after_a_reject(self) -> None: "--actor", "IMPLEMENTER", "--evidence", - "handoffs/implementer.md", + "review-report.md", ) validating = self.run_cli( "transition", From 8605c85e838507df677383bbf154ff6b665e21ea Mon Sep 17 00:00:00 2001 From: generalio <1487144524@qq.com> Date: Tue, 18 Aug 2026 10:55:05 +0800 Subject: [PATCH 6/9] docs(workflow): align ledger guide --- .codex/workflow/README.md | 13 +++++++++++-- tools/tests/test_agent_workflow.py | 13 +++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/.codex/workflow/README.md b/.codex/workflow/README.md index 6d355bb..58efa9c 100644 --- a/.codex/workflow/README.md +++ b/.codex/workflow/README.md @@ -12,7 +12,17 @@ python3 tools/agent_workflow.py transition .codex/workflow/runs/ ``` -三个命令都会先获取运行目录内 `.workflow.lock` 的非阻塞 `fcntl` 租约。该能力要求 Unix-like 系统和 Python 3.11+,是当前的 P2 平台边界。租约被其他 Supervisor 持有时,命令会立即失败,不会读取或改写账本;`resume` 因此可确认不存在另一份有效租约。除这个运行时锁文件外,`resume` 完全只读,输出当前状态、阻断原因、唯一恢复目标和唯一下一步。 +默认不得生成或要求 `handoffs/` 阶段交接文档。只有维护者明确指出上下文超出并要求时, +才允许创建并引用该目录中的文件。 + +在获取 `.workflow.lock` 前,`transition` 会先校验目标状态与 `--stop-reason`、`--next-action` 的组合。 +目标为 `PAUSED` 或 `BLOCKED` 时,两项必须同时为非空单行;其他目标状态不得传入任一项。 +无效组合会在创建或获取锁前失败。通过该校验的 `transition`、`validate` 和 `resume` 会获取 +运行目录内 `.workflow.lock` 的非阻塞 `fcntl` 租约。该能力要求 Unix-like 系统和 Python 3.11+, +是当前的 P2 平台边界。租约被其他 Supervisor 持有时,命令会立即失败, +不会读取或改写账本; +`resume` 因此可确认不存在另一份有效租约。除这个运行时锁文件外,`resume` 完全只读, +输出当前状态、阻断原因、唯一恢复目标和唯一下一步。 `validate` 检查状态字段、Git 可解析的基线/候选 SHA、任务契约、计划、完整事件链和审查证据。`transition` 仅接受状态机允许的下一状态:它先追加并 `fsync` 对应事件,再以同目录临时文件原子替换 `state.json`。如果事件追加失败,状态保持不变;如果替换在事件成功后中断,下一次 `validate` 会因事件末状态不一致而拒绝继续。 @@ -27,7 +37,6 @@ runs// ├── plan.json ├── events.jsonl # 首次迁移时创建 ├── .workflow.lock # 运行时租约文件,不写入任务数据 -├── handoffs/ ├── candidate.sha # 生成候选后必需 ├── review-report.md # REVIEWED 后必需 └── resume.md # 可选的人类可读恢复上下文 diff --git a/tools/tests/test_agent_workflow.py b/tools/tests/test_agent_workflow.py index 5e765ff..2704385 100644 --- a/tools/tests/test_agent_workflow.py +++ b/tools/tests/test_agent_workflow.py @@ -77,6 +77,7 @@ def test_workflow_documents_and_role_configs_forbid_default_handoff_documents(se REPOSITORY_ROOT / "docs" / "agent-workflow.md", REPOSITORY_ROOT / "docs" / "agent-workflow-autonomous-handoff.md", REPOSITORY_ROOT / "docs" / "agent-workflow-task-template.md", + REPOSITORY_ROOT / ".codex" / "workflow" / "README.md", ) role_configuration_paths = ( REPOSITORY_ROOT / ".codex" / "agents" / "workflow-implementer.toml", @@ -98,6 +99,18 @@ def test_workflow_documents_and_role_configs_forbid_default_handoff_documents(se f"{role_configuration_path.relative_to(REPOSITORY_ROOT)} 必须禁止默认阶段交接文档", ) + def test_workflow_readme_describes_stop_option_validation_before_lock_acquisition(self) -> None: + workflow_readme = REPOSITORY_ROOT / ".codex" / "workflow" / "README.md" + content = workflow_readme.read_text(encoding="utf-8") + + self.assertIn( + "在获取 `.workflow.lock` 前,`transition` 会先校验目标状态与 " + "`--stop-reason`、`--next-action` 的组合。", + content, + ) + self.assertNotIn("三个命令都会先获取", content) + self.assertNotIn("├── handoffs/", content) + def test_validate_accepts_a_planned_run(self) -> None: run_directory = self.copy_fixture("valid") From 3ba4d6d41c327650ace3555aebc832cce1e71275 Mon Sep 17 00:00:00 2001 From: generalio <1487144524@qq.com> Date: Tue, 18 Aug 2026 11:38:49 +0800 Subject: [PATCH 7/9] docs(workflow): enforce minimal agent topology --- .codex/agents/workflow-implementer.toml | 5 ++ .codex/agents/workflow-integrator.toml | 5 ++ .codex/agents/workflow-planner.toml | 6 +++ .codex/agents/workflow-verify-reviewer.toml | 5 ++ .codex/workflow/README.md | 7 +++ AGENTS.md | 4 ++ docs/agent-workflow-autonomous-handoff.md | 5 ++ docs/agent-workflow-task-template.md | 5 ++ docs/agent-workflow.md | 9 ++++ tools/tests/test_agent_workflow.py | 53 +++++++++++++++++++++ 10 files changed, 104 insertions(+) diff --git a/.codex/agents/workflow-implementer.toml b/.codex/agents/workflow-implementer.toml index 059bd73..6c3a1e9 100644 --- a/.codex/agents/workflow-implementer.toml +++ b/.codex/agents/workflow-implementer.toml @@ -7,6 +7,11 @@ developer_instructions = """ 只有维护者明确指出上下文超出并要求时,才允许创建并引用该目录中的文件。 任务契约、计划、候选 SHA 和审查报告是账本或门禁证据,不属于交接文档。 +默认仅启动 `Planner -> 单一 Implementer -> Integrator -> Verify-Reviewer` 四个角色,并严格顺序执行。 +不得因 Android、iOS、Desktop、JS/Wasm 或测试平台自动创建、拆分或并发专项 Agent。 +第二个 Implementer 仅可由 Planner 在任务契约中书面批准,且必须同时满足范围不重叠、验证独立、两个范围均不含 `webview-api`、`webview-extension-api`、API 基线、Gradle 设置、发布配置或跨平台契约;同一任务最多两个 Implementer。 +即使例外获批,也必须全体 Implementer 完成后才进入 Integrator。 + 必须使用独立 worktree,实施最小改动并执行契约指定的最小相关验证。 返回 Supervisor 的阶段信息必须列出实际修改文件与授权范围的对应关系、 可应用 diff 或 worktree、 diff --git a/.codex/agents/workflow-integrator.toml b/.codex/agents/workflow-integrator.toml index 88b0e8e..a241ed2 100644 --- a/.codex/agents/workflow-integrator.toml +++ b/.codex/agents/workflow-integrator.toml @@ -13,6 +13,11 @@ docs/agent-workflow.md 和 docs/agent-workflow-autonomous-handoff.md 三份文 只有维护者明确指出上下文超出并要求时,才允许创建并引用该目录中的文件。 任务契约、计划、候选 SHA 和审查报告是账本或门禁证据,不属于交接文档。 +默认仅启动 `Planner -> 单一 Implementer -> Integrator -> Verify-Reviewer` 四个角色,并严格顺序执行。 +不得因 Android、iOS、Desktop、JS/Wasm 或测试平台自动创建、拆分或并发专项 Agent。 +第二个 Implementer 仅可由 Planner 在任务契约中书面批准,且必须同时满足范围不重叠、验证独立、两个范围均不含 `webview-api`、`webview-extension-api`、API 基线、Gradle 设置、发布配置或跨平台契约;同一任务最多两个 Implementer。 +即使例外获批,也必须全体 Implementer 完成后才进入 Integrator。 + 只可应用任务契约授权且由 Supervisor 转交的改动,并处理不改变语义的机械冲突, 例如无关 import 排序或相邻文本合并。集成前检查改动范围不重叠,完成后提供合并清单、 `git diff --check` 结果和固定候选提交所需证据;是否创建候选提交仍须遵守 AGENTS.md diff --git a/.codex/agents/workflow-planner.toml b/.codex/agents/workflow-planner.toml index 9200676..06c1a4c 100644 --- a/.codex/agents/workflow-planner.toml +++ b/.codex/agents/workflow-planner.toml @@ -10,5 +10,11 @@ developer_instructions = """ 只有维护者明确指出上下文超出并要求时,才允许创建并引用该目录中的文件。 任务契约、计划、候选 SHA 和审查报告是账本或门禁证据,不属于交接文档。 +默认仅启动 `Planner -> 单一 Implementer -> Integrator -> Verify-Reviewer` 四个角色,并严格顺序执行。 +不得因 Android、iOS、Desktop、JS/Wasm 或测试平台自动创建、拆分或并发专项 Agent。 +第二个 Implementer 仅可由 Planner 在任务契约中书面批准,且必须同时满足范围不重叠、验证独立、两个范围均不含 `webview-api`、`webview-extension-api`、API 基线、Gradle 设置、发布配置或跨平台契约;同一任务最多两个 Implementer。 +即使例外获批,也必须全体 Implementer 完成后才进入 Integrator。 +Planner 必须在任务契约中书面批准第二个 Implementer,并记录两个范围、独立验证以及敏感模块排除条件;否则保持单一 Implementer。 + 不得实施修复、替其他角色裁决语义冲突,或通过省略风险、放宽默认安全策略、修改接口约定来使任务看似可执行。契约不完整、需要维护者授权或发现 P0/P1 时,明确标记阻断原因与唯一下一步。 """ diff --git a/.codex/agents/workflow-verify-reviewer.toml b/.codex/agents/workflow-verify-reviewer.toml index 89c8900..e194c30 100644 --- a/.codex/agents/workflow-verify-reviewer.toml +++ b/.codex/agents/workflow-verify-reviewer.toml @@ -10,5 +10,10 @@ developer_instructions = """ 只有维护者明确指出上下文超出并要求时,才允许创建并引用该目录中的文件。 任务契约、计划、候选 SHA 和审查报告是账本或门禁证据,不属于交接文档。 +默认仅启动 `Planner -> 单一 Implementer -> Integrator -> Verify-Reviewer` 四个角色,并严格顺序执行。 +不得因 Android、iOS、Desktop、JS/Wasm 或测试平台自动创建、拆分或并发专项 Agent。 +第二个 Implementer 仅可由 Planner 在任务契约中书面批准,且必须同时满足范围不重叠、验证独立、两个范围均不含 `webview-api`、`webview-extension-api`、API 基线、Gradle 设置、发布配置或跨平台契约;同一任务最多两个 Implementer。 +即使例外获批,也必须全体 Implementer 完成后才进入 Integrator。 + 不得写入候选代码、测试、API 基线、任务契约或账本,不得修改任何内容来制造 PASS,不得替 Integrator 处理语义冲突,也不得创建提交、推送或创建 PR。将审查报告仅返回 Supervisor;只能由 Supervisor 使用临时文件后原子替换落盘。 """ diff --git a/.codex/workflow/README.md b/.codex/workflow/README.md index 58efa9c..82bb924 100644 --- a/.codex/workflow/README.md +++ b/.codex/workflow/README.md @@ -15,6 +15,13 @@ python3 tools/agent_workflow.py resume .codex/workflow/runs/ 默认不得生成或要求 `handoffs/` 阶段交接文档。只有维护者明确指出上下文超出并要求时, 才允许创建并引用该目录中的文件。 +## 最小角色编排 + +默认仅启动 `Planner -> 单一 Implementer -> Integrator -> Verify-Reviewer` 四个角色,并严格顺序执行。 +不得因 Android、iOS、Desktop、JS/Wasm 或测试平台自动创建、拆分或并发专项 Agent。 +第二个 Implementer 仅可由 Planner 在任务契约中书面批准,且必须同时满足范围不重叠、验证独立、两个范围均不含 `webview-api`、`webview-extension-api`、API 基线、Gradle 设置、发布配置或跨平台契约;同一任务最多两个 Implementer。 +即使例外获批,也必须全体 Implementer 完成后才进入 Integrator。 + 在获取 `.workflow.lock` 前,`transition` 会先校验目标状态与 `--stop-reason`、`--next-action` 的组合。 目标为 `PAUSED` 或 `BLOCKED` 时,两项必须同时为非空单行;其他目标状态不得传入任一项。 无效组合会在创建或获取锁前失败。通过该校验的 `transition`、`validate` 和 `resume` 会获取 diff --git a/AGENTS.md b/AGENTS.md index 6d754d8..a55eb87 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,6 +20,10 @@ Planner、独占 worktree 的 Implementer、Integrator 和固定候选 SHA 的 Verify-Reviewer。 没有任务契约、候选 SHA 或 `PASS` 裁决不得进入下一状态。若当前会话无法启动所需子 Agent,必须记录阻断原因,不得静默降级为单 Agent。 +- 默认仅启动 `Planner -> 单一 Implementer -> Integrator -> Verify-Reviewer` 四个角色,并严格顺序执行。 +- 不得因 Android、iOS、Desktop、JS/Wasm 或测试平台自动创建、拆分或并发专项 Agent。 +- 第二个 Implementer 仅可由 Planner 在任务契约中书面批准,且必须同时满足范围不重叠、验证独立、两个范围均不含 `webview-api`、`webview-extension-api`、API 基线、Gradle 设置、发布配置或跨平台契约;同一任务最多两个 Implementer。 +- 即使例外获批,也必须全体 Implementer 完成后才进入 Integrator。 - 默认不得生成或要求 `handoffs/` 阶段交接文档。只有维护者明确指出上下文超出并要求时, 才允许创建并引用该目录中的文件。任务契约、计划、候选 SHA 与审查报告 属于账本或门禁证据, diff --git a/docs/agent-workflow-autonomous-handoff.md b/docs/agent-workflow-autonomous-handoff.md index 1126f2d..f1d0d24 100644 --- a/docs/agent-workflow-autonomous-handoff.md +++ b/docs/agent-workflow-autonomous-handoff.md @@ -16,6 +16,11 @@ Supervisor 负责创建 `.codex/workflow/runs//` 账本、按既有状 Planner 仅做只读分析,向 Supervisor 返回任务契约、基线 SHA、任务 DAG、独占文件或模块、依赖、最小验证命令、验收 标准,以及公共 API、平台和安全默认值影响。`TRIVIAL` 契约必须写明理由、独占范围和快速路径验证命令。 +默认仅启动 `Planner -> 单一 Implementer -> Integrator -> Verify-Reviewer` 四个角色,并严格顺序执行。 +不得因 Android、iOS、Desktop、JS/Wasm 或测试平台自动创建、拆分或并发专项 Agent。 +第二个 Implementer 仅可由 Planner 在任务契约中书面批准,且必须同时满足范围不重叠、验证独立、两个范围均不含 `webview-api`、`webview-extension-api`、API 基线、Gradle 设置、发布配置或跨平台契约;同一任务最多两个 Implementer。 +即使例外获批,也必须全体 Implementer 完成后才进入 Integrator。 + Implementer 只能在独立 worktree 内修改任务契约明确授权且由其独占的范围。返回给 Supervisor 的阶段信息至少包含:任务和基线、实际修改文件与所有权映射、可应用 diff 或 worktree、 执行命令及退出结果、未验证范围,以及公共 API、跨平台差异和安全默认值影响。 diff --git a/docs/agent-workflow-task-template.md b/docs/agent-workflow-task-template.md index 0fbbb9a..15413ce 100644 --- a/docs/agent-workflow-task-template.md +++ b/docs/agent-workflow-task-template.md @@ -16,6 +16,11 @@ - 模式:`FULL` / `TRIVIAL`;`TRIVIAL` 理由: - 账本路径:`.codex/workflow/runs//` - Planner: +- 角色编排:默认仅启动 `Planner -> 单一 Implementer -> Integrator -> Verify-Reviewer` 四个角色,并严格顺序执行。 +- 不得因 Android、iOS、Desktop、JS/Wasm 或测试平台自动创建、拆分或并发专项 Agent。 +- 第二个 Implementer:`不批准` / `批准`;若批准,Planner 必须在任务契约中书面批准第二个 Implementer,并填写下列条件。 +- 第二个 Implementer 仅可由 Planner 在任务契约中书面批准,且必须同时满足范围不重叠、验证独立、两个范围均不含 `webview-api`、`webview-extension-api`、API 基线、Gradle 设置、发布配置或跨平台契约;同一任务最多两个 Implementer。 +- 即使例外获批,也必须全体 Implementer 完成后才进入 Integrator。 ## 计划与所有权 diff --git a/docs/agent-workflow.md b/docs/agent-workflow.md index 0fd01b6..884d248 100644 --- a/docs/agent-workflow.md +++ b/docs/agent-workflow.md @@ -9,6 +9,15 @@ Planner 判定 `TRIVIAL` 时必须在任务契约中说明理由、独占范围和快速路径验证命令。不能启动所需角色、发现语义冲突、 P0/P1、候选 SHA 变化或需要 GitHub/凭据授权时,Supervisor 必须暂停或阻断并记录唯一下一步,不能以单 Agent 静默完成。 +## 最小角色编排 + +默认仅启动 `Planner -> 单一 Implementer -> Integrator -> Verify-Reviewer` 四个角色,并严格顺序执行。 +不得因 Android、iOS、Desktop、JS/Wasm 或测试平台自动创建、拆分或并发专项 Agent。 +第二个 Implementer 仅可由 Planner 在任务契约中书面批准,且必须同时满足范围不重叠、验证独立、两个范围均不含 `webview-api`、`webview-extension-api`、API 基线、Gradle 设置、发布配置或跨平台契约;同一任务最多两个 Implementer。 +即使例外获批,也必须全体 Implementer 完成后才进入 Integrator。 + +Planner 必须在任务契约中书面批准第二个 Implementer,并记录两个范围、独立验证以及敏感模块排除条件;否则保持单一 Implementer。 + ## 账本与状态 每个任务使用 `.codex/workflow/runs//`,只保存公开的状态、相对证据路径和 Git SHA: diff --git a/tools/tests/test_agent_workflow.py b/tools/tests/test_agent_workflow.py index 2704385..01ccf6a 100644 --- a/tools/tests/test_agent_workflow.py +++ b/tools/tests/test_agent_workflow.py @@ -99,6 +99,59 @@ def test_workflow_documents_and_role_configs_forbid_default_handoff_documents(se f"{role_configuration_path.relative_to(REPOSITORY_ROOT)} 必须禁止默认阶段交接文档", ) + def test_workflow_documents_and_role_configs_enforce_minimal_role_topology(self) -> None: + default_topology = "默认仅启动 `Planner -> 单一 Implementer -> Integrator -> Verify-Reviewer` 四个角色,并严格顺序执行。" + platform_agent_prohibition = "不得因 Android、iOS、Desktop、JS/Wasm 或测试平台自动创建、拆分或并发专项 Agent。" + second_implementer_approval = ( + "第二个 Implementer 仅可由 Planner 在任务契约中书面批准,且必须同时满足范围不重叠、验证独立、" + "两个范围均不含 `webview-api`、`webview-extension-api`、API 基线、Gradle 设置、发布配置或跨平台契约;" + "同一任务最多两个 Implementer。" + ) + integration_gate = "即使例外获批,也必须全体 Implementer 完成后才进入 Integrator。" + workflow_document_paths = ( + REPOSITORY_ROOT / "AGENTS.md", + REPOSITORY_ROOT / "docs" / "agent-workflow.md", + REPOSITORY_ROOT / "docs" / "agent-workflow-autonomous-handoff.md", + REPOSITORY_ROOT / "docs" / "agent-workflow-task-template.md", + REPOSITORY_ROOT / ".codex" / "workflow" / "README.md", + ) + role_configuration_paths = tuple( + sorted((REPOSITORY_ROOT / ".codex" / "agents").glob("workflow-*.toml")) + ) + + self.assertEqual( + tuple(path.name for path in role_configuration_paths), + ( + "workflow-implementer.toml", + "workflow-integrator.toml", + "workflow-planner.toml", + "workflow-verify-reviewer.toml", + ), + "永久工作流角色配置必须精确为四个最小角色", + ) + for policy in ( + default_topology, + platform_agent_prohibition, + second_implementer_approval, + integration_gate, + ): + for path in (*workflow_document_paths, *role_configuration_paths): + self.assertIn( + policy, + path.read_text(encoding="utf-8"), + f"{path.relative_to(REPOSITORY_ROOT)} 必须声明最小角色编排规则:{policy}", + ) + + task_template = (REPOSITORY_ROOT / "docs" / "agent-workflow-task-template.md").read_text( + encoding="utf-8" + ) + planner_configuration = ( + REPOSITORY_ROOT / ".codex" / "agents" / "workflow-planner.toml" + ).read_text(encoding="utf-8") + self.assertIn("- 角色编排:", task_template) + self.assertIn("- 第二个 Implementer:`不批准` / `批准`;", task_template) + self.assertIn("Planner 必须在任务契约中书面批准第二个 Implementer", planner_configuration) + def test_workflow_readme_describes_stop_option_validation_before_lock_acquisition(self) -> None: workflow_readme = REPOSITORY_ROOT / ".codex" / "workflow" / "README.md" content = workflow_readme.read_text(encoding="utf-8") From 9fd5625912dac341043e1689b4b389ec960801bf Mon Sep 17 00:00:00 2001 From: generalio <1487144524@qq.com> Date: Tue, 18 Aug 2026 11:54:48 +0800 Subject: [PATCH 8/9] chore(workflow): record candidate review ledger --- .../runs/MW-20260818-004/candidate.sha | 1 + .../runs/MW-20260818-004/events.jsonl | 6 ++ .../workflow/runs/MW-20260818-004/plan.json | 49 +++++++++++++++++ .../runs/MW-20260818-004/review-report.md | 28 ++++++++++ .../workflow/runs/MW-20260818-004/state.json | 20 +++++++ .../runs/MW-20260818-004/task-contract.md | 55 +++++++++++++++++++ 6 files changed, 159 insertions(+) create mode 100644 .codex/workflow/runs/MW-20260818-004/candidate.sha create mode 100644 .codex/workflow/runs/MW-20260818-004/events.jsonl create mode 100644 .codex/workflow/runs/MW-20260818-004/plan.json create mode 100644 .codex/workflow/runs/MW-20260818-004/review-report.md create mode 100644 .codex/workflow/runs/MW-20260818-004/state.json create mode 100644 .codex/workflow/runs/MW-20260818-004/task-contract.md diff --git a/.codex/workflow/runs/MW-20260818-004/candidate.sha b/.codex/workflow/runs/MW-20260818-004/candidate.sha new file mode 100644 index 0000000..e1a388d --- /dev/null +++ b/.codex/workflow/runs/MW-20260818-004/candidate.sha @@ -0,0 +1 @@ +3ba4d6d41c327650ace3555aebc832cce1e71275 diff --git a/.codex/workflow/runs/MW-20260818-004/events.jsonl b/.codex/workflow/runs/MW-20260818-004/events.jsonl new file mode 100644 index 0000000..6fc5955 --- /dev/null +++ b/.codex/workflow/runs/MW-20260818-004/events.jsonl @@ -0,0 +1,6 @@ +{"actor": "SUPERVISOR", "at": "2026-08-18T03:32:11.541598+00:00", "evidence": "task-contract.md", "from": "PLANNED", "summary": "状态迁移已验证", "to": "IMPLEMENTING"} +{"actor": "IMPLEMENTER", "at": "2026-08-18T03:36:45.485177+00:00", "evidence": "task-contract.md", "from": "IMPLEMENTING", "summary": "状态迁移已验证", "to": "READY_TO_INTEGRATE"} +{"actor": "INTEGRATOR", "at": "2026-08-18T03:39:49.116425+00:00", "evidence": "candidate.sha", "from": "READY_TO_INTEGRATE", "summary": "状态迁移已验证", "to": "VALIDATING"} +{"actor": "VERIFY_REVIEWER", "at": "2026-08-18T03:43:29.537655+00:00", "evidence": "review-report.md", "from": "VALIDATING", "summary": "状态迁移已验证", "to": "REVIEWED"} +{"actor": "SUPERVISOR", "at": "2026-08-18T03:43:29.681915+00:00", "evidence": "review-report.md", "from": "REVIEWED", "summary": "状态迁移已验证", "to": "COMMIT_READY"} +{"actor": "SUPERVISOR", "at": "2026-08-18T03:45:09.856090+00:00", "evidence": "review-report.md", "from": "COMMIT_READY", "summary": "状态迁移已验证", "to": "PR_READY"} diff --git a/.codex/workflow/runs/MW-20260818-004/plan.json b/.codex/workflow/runs/MW-20260818-004/plan.json new file mode 100644 index 0000000..c58687f --- /dev/null +++ b/.codex/workflow/runs/MW-20260818-004/plan.json @@ -0,0 +1,49 @@ +{ + "schemaVersion": 1, + "taskId": "MW-20260818-004", + "baselineSha": "8605c85e838507df677383bbf154ff6b665e21ea", + "tasks": [ + { + "id": "planner-review", + "owner": "WORKFLOW_PLANNER", + "scope": ["read-only workflow governance review"], + "dependsOn": [], + "verification": ["contract completeness review"] + }, + { + "id": "single-implementer-change", + "owner": "WORKFLOW_IMPLEMENTER", + "scope": [ + "AGENTS.md", + "docs/agent-workflow.md", + "docs/agent-workflow-autonomous-handoff.md", + "docs/agent-workflow-task-template.md", + ".codex/agents/workflow-*.toml", + ".codex/workflow/README.md", + "tools/tests/test_agent_workflow.py" + ], + "dependsOn": ["planner-review"], + "verification": ["focused Python workflow tests", "git diff --check"] + }, + { + "id": "integration", + "owner": "WORKFLOW_INTEGRATOR", + "scope": ["authorized implementation diff only"], + "dependsOn": ["single-implementer-change"], + "verification": ["candidate commit", "git diff --check"] + }, + { + "id": "fixed-candidate-review", + "owner": "WORKFLOW_VERIFY_REVIEWER", + "scope": ["fixed candidate governance and regression review"], + "dependsOn": ["integration"], + "verification": ["focused Python workflow tests", "fixed-SHA review"] + } + ], + "acceptanceCriteria": [ + "默认角色链路固定为 Planner、单一 Implementer、Integrator、Verify-Reviewer,顺序执行。", + "不得按 Android、iOS、Desktop、JS/Wasm 自动拆分 Agent。", + "只有 Planner 书面批准且范围、验证和敏感模块约束满足时,才允许第二个 Implementer;本任务不批准该例外。", + "协议和角色配置的约束具有静态回归覆盖。" + ] +} diff --git a/.codex/workflow/runs/MW-20260818-004/review-report.md b/.codex/workflow/runs/MW-20260818-004/review-report.md new file mode 100644 index 0000000..9af2d43 --- /dev/null +++ b/.codex/workflow/runs/MW-20260818-004/review-report.md @@ -0,0 +1,28 @@ +# MW-20260818-004 固定候选审查报告 + +候选 SHA:3ba4d6d41c327650ace3555aebc832cce1e71275 +裁决:PASS + +## 验证结果 + +- `git status --short`(审查前后):退出 `0`,均为空。 +- `git rev-parse HEAD`:退出 `0`,与固定候选 SHA 一致。 +- `PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tools.tests.test_agent_workflow -v`:退出 `0`,48 项通过。 +- `git diff 8605c85e838507df677383bbf154ff6b665e21ea...3ba4d6d41c327650ace3555aebc832cce1e71275 --check`:退出 `0`。 +- 主工作区账本验证:`python3 tools/agent_workflow.py validate .codex/workflow/runs/MW-20260818-004` 退出 `0`。 + +## 范围与兼容性审查 + +候选恰好修改契约授权的 10 个治理文件,无产品模块、公共 API、API 基线、Gradle、发布配置或账本改动。 +静态测试锁定四个永久角色、严格顺序、禁止平台专项 Agent、第二个 Implementer 的 Planner 书面例外和 Integrator 门禁。 +任务模板与 Planner 配置的审批职责一致。 + +- 公共 API:不受影响。 +- Android、iOS、Desktop、JS/Wasm:不受产品行为影响,未运行平台构建或运行时测试。 +- JS 桥与安全默认值:不受影响,未放宽导航、Cookie、文件访问或下载权限。 + +## 风险 + +无 P0。无 P1。无 P2。 + +残余边界:规则由仓库协议、角色提示和静态测试约束,实际调度仍依赖 Supervisor 按任务契约执行;本任务契约已记录该风险。 diff --git a/.codex/workflow/runs/MW-20260818-004/state.json b/.codex/workflow/runs/MW-20260818-004/state.json new file mode 100644 index 0000000..1d1c5d3 --- /dev/null +++ b/.codex/workflow/runs/MW-20260818-004/state.json @@ -0,0 +1,20 @@ +{ + "schemaVersion": 1, + "taskId": "MW-20260818-004", + "baselineSha": "8605c85e838507df677383bbf154ff6b665e21ea", + "status": "PR_READY", + "mode": "FULL", + "currentStage": "PR", + "attempts": { + "PLANNING": 1, + "IMPLEMENTING": 1, + "VALIDATING": 1, + "REVIEWING": 1 + }, + "candidateSha": "3ba4d6d41c327650ace3555aebc832cce1e71275", + "verdict": "PASS", + "nextAction": "由 Planner 审核最小角色编排契约", + "lastEventAt": "2026-08-18T03:45:09.856090+00:00", + "stopReason": null, + "reviewedCandidateSha": "3ba4d6d41c327650ace3555aebc832cce1e71275" +} diff --git a/.codex/workflow/runs/MW-20260818-004/task-contract.md b/.codex/workflow/runs/MW-20260818-004/task-contract.md new file mode 100644 index 0000000..9425fad --- /dev/null +++ b/.codex/workflow/runs/MW-20260818-004/task-contract.md @@ -0,0 +1,55 @@ +# MW-20260818-004 最小角色编排契约 + +- 需求来源:维护者确认按最小 Agent 模式执行,并授权收紧默认协作规则。 +- 目标:将工作流协议、角色模板和回归测试统一为默认 `Planner -> 单一 Implementer -> Integrator -> Verify-Reviewer` 的顺序模式。 +- 非目标:不修改产品模块、公共 API、API 基线、`tools/agent_workflow.py`、Gradle、发布配置、历史账本或历史 worktree;不生成 `handoffs/`。 +- 基线提交 SHA:`8605c85e838507df677383bbf154ff6b665e21ea`。 +- 模式:`FULL`。本任务影响共享治理协议,必须经过独立 Planner、单一 Implementer、Integrator 与固定候选审查。 +- 账本路径:`.codex/workflow/runs/MW-20260818-004/`。 +- Planner:`/root/minimal_role_planner`,结论:`APPROVED`,无 P0/P1。 +- 第二个 Implementer:不批准。平台测试是同一 Implementer 的验证责任,不构成新增平台 Agent。 + +## 计划与所有权 + +| Agent | 独占文件或模块 | 前置依赖 | 返回的阶段信息 | 最小验证 | +| --- | --- | --- | --- | --- | +| Planner | 只读治理协议与角色配置 | 账本初始化 | 定稿契约、DAG、影响分析 | 契约完整性复核 | +| Implementer | `AGENTS.md`、`docs/agent-workflow.md`、`docs/agent-workflow-autonomous-handoff.md`、`docs/agent-workflow-task-template.md`、`.codex/agents/workflow-*.toml`、`.codex/workflow/README.md`、`tools/tests/test_agent_workflow.py` | Planner 批准 | 修改映射、RED/GREEN 证据、未验证范围 | 聚焦静态测试、完整 Python 测试、`git diff --check` | +| Integrator | Implementer 转交的唯一 diff | 阶段信息完整且范围未越界 | 范围检查、候选 SHA、`git diff --check` | 候选提交、账本验证 | +| Verify-Reviewer | 干净 detached worktree 中的固定候选 | 候选 SHA | 命令结果、P0/P1/P2、唯一裁决 | 完整 Python 测试、固定 SHA 审查 | + +- 默认拓扑:仅启动 `Planner -> 单一 Implementer -> Integrator -> Verify-Reviewer`,严格顺序执行。不得因 Android、iOS、Desktop、JS/Wasm 或测试平台自动创建、拆分或并发专项 Agent。 +- 第二个 Implementer 例外:仅可由 Planner 在任务契约中书面批准,且必须同时满足范围不重叠、验证独立、两者范围均不包含 `webview-api`、`webview-extension-api`、API 基线、Gradle 设置、发布配置或跨平台契约;同一任务最多两个 Implementer。即使例外获批,所有 Implementer 完成后才可进入 Integrator。 +- 禁止修改范围:产品模块、公共 API、API 基线、`tools/agent_workflow.py`、Gradle、发布配置、历史账本和历史 worktree。 +- 需退回 Planner/Implementer 的语义冲突:提议新增第三个 Implementer、平台专项 Agent,或第二个 Implementer 缺少任一书面条件;角色数量、默认值、安全策略或测试预期冲突。 + +## 验收与验证 + +- 四个永久 `workflow-*.toml` 配置恰好为 Planner、Implementer、Integrator、Verify-Reviewer。 +- 五份治理文档与四份角色配置均表达默认拓扑、平台禁止拆分和第二个 Implementer 的唯一例外。 +- 模板要求 Planner 显式记录角色编排、是否批准第二个 Implementer、范围/验证/敏感模块条件。 +- 静态回归测试先 RED 后 GREEN,覆盖上述规则与 Planner 的书面审批职责。 +- 实施阶段命令: + + ```sh + PYTHONDONTWRITEBYTECODE=1 python3 -m unittest \ + tools.tests.test_agent_workflow.AgentWorkflowCliTest.test_workflow_documents_and_role_configs_enforce_minimal_role_topology -v + PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tools.tests.test_agent_workflow -v + git diff --check + ``` + +- 候选版本命令: + + ```sh + PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tools.tests.test_agent_workflow -v + git diff --check + python3 tools/agent_workflow.py validate .codex/workflow/runs/MW-20260818-004 + ``` + +- 公共 API:不受影响;验证证据:候选差异仅涉及治理文档、角色配置与 Python 静态测试,未修改 API 基线或公共接口。 +- Android:不受影响;验证证据:候选未修改 Android 产品模块,仅收敛 Agent 编排规则。 +- iOS:不受影响;验证证据:候选未修改 iOS 产品模块,仅收敛 Agent 编排规则。 +- Desktop:不受影响;验证证据:候选未修改 Desktop 产品模块,仅收敛 Agent 编排规则。 +- JS/Wasm:不受影响;验证证据:候选未修改 JS/Wasm 产品模块,仅收敛 Agent 编排规则。 +- 安全默认值影响:无;不放宽导航、JS 桥、Cookie、文件访问或下载权限。 +- 已知风险:静态规则只能约束仓库协议与角色提示词,实际调度仍由 Supervisor 按任务契约执行。 From fdaabd1b2bfada98115fa6d33143e1b3819a41ba Mon Sep 17 00:00:00 2001 From: generalio <1487144524@qq.com> Date: Tue, 18 Aug 2026 12:32:50 +0800 Subject: [PATCH 9/9] chore(workflow): record pr evidence ledger --- .../runs/MW-20260818-005/candidate.sha | 1 + .../runs/MW-20260818-005/events.jsonl | 10 ++++ .../workflow/runs/MW-20260818-005/plan.json | 40 +++++++++++++ .../runs/MW-20260818-005/review-report.md | 27 +++++++++ .../workflow/runs/MW-20260818-005/state.json | 20 +++++++ .../runs/MW-20260818-005/task-contract.md | 60 +++++++++++++++++++ 6 files changed, 158 insertions(+) create mode 100644 .codex/workflow/runs/MW-20260818-005/candidate.sha create mode 100644 .codex/workflow/runs/MW-20260818-005/events.jsonl create mode 100644 .codex/workflow/runs/MW-20260818-005/plan.json create mode 100644 .codex/workflow/runs/MW-20260818-005/review-report.md create mode 100644 .codex/workflow/runs/MW-20260818-005/state.json create mode 100644 .codex/workflow/runs/MW-20260818-005/task-contract.md diff --git a/.codex/workflow/runs/MW-20260818-005/candidate.sha b/.codex/workflow/runs/MW-20260818-005/candidate.sha new file mode 100644 index 0000000..e1a388d --- /dev/null +++ b/.codex/workflow/runs/MW-20260818-005/candidate.sha @@ -0,0 +1 @@ +3ba4d6d41c327650ace3555aebc832cce1e71275 diff --git a/.codex/workflow/runs/MW-20260818-005/events.jsonl b/.codex/workflow/runs/MW-20260818-005/events.jsonl new file mode 100644 index 0000000..347ec9d --- /dev/null +++ b/.codex/workflow/runs/MW-20260818-005/events.jsonl @@ -0,0 +1,10 @@ +{"actor": "SUPERVISOR", "at": "2026-08-18T04:08:59.579808+00:00", "evidence": "task-contract.md", "from": "PLANNED", "summary": "状态迁移已验证", "to": "IMPLEMENTING"} +{"actor": "IMPLEMENTER", "at": "2026-08-18T04:14:46.483940+00:00", "evidence": "task-contract.md", "from": "IMPLEMENTING", "summary": "状态迁移已验证", "to": "READY_TO_INTEGRATE"} +{"actor": "INTEGRATOR", "at": "2026-08-18T04:14:46.533690+00:00", "evidence": "candidate.sha", "from": "READY_TO_INTEGRATE", "summary": "状态迁移已验证", "to": "VALIDATING"} +{"actor": "VERIFY_REVIEWER", "at": "2026-08-18T04:28:22.125704+00:00", "evidence": "review-report.md", "from": "VALIDATING", "summary": "状态迁移已验证", "to": "REVIEWED"} +{"actor": "SUPERVISOR", "at": "2026-08-18T04:28:22.228672+00:00", "evidence": "review-report.md", "from": "REVIEWED", "summary": "状态迁移已验证", "to": "IMPLEMENTING"} +{"actor": "IMPLEMENTER", "at": "2026-08-18T04:29:44.765282+00:00", "evidence": "task-contract.md", "from": "IMPLEMENTING", "summary": "状态迁移已验证", "to": "READY_TO_INTEGRATE"} +{"actor": "INTEGRATOR", "at": "2026-08-18T04:29:44.824967+00:00", "evidence": "candidate.sha", "from": "READY_TO_INTEGRATE", "summary": "状态迁移已验证", "to": "VALIDATING"} +{"actor": "VERIFY_REVIEWER", "at": "2026-08-18T04:32:02.092643+00:00", "evidence": "review-report.md", "from": "VALIDATING", "summary": "状态迁移已验证", "to": "REVIEWED"} +{"actor": "SUPERVISOR", "at": "2026-08-18T04:32:02.208746+00:00", "evidence": "review-report.md", "from": "REVIEWED", "summary": "状态迁移已验证", "to": "COMMIT_READY"} +{"actor": "SUPERVISOR", "at": "2026-08-18T04:32:02.260838+00:00", "evidence": "review-report.md", "from": "COMMIT_READY", "summary": "状态迁移已验证", "to": "PR_READY"} diff --git a/.codex/workflow/runs/MW-20260818-005/plan.json b/.codex/workflow/runs/MW-20260818-005/plan.json new file mode 100644 index 0000000..585b3c9 --- /dev/null +++ b/.codex/workflow/runs/MW-20260818-005/plan.json @@ -0,0 +1,40 @@ +{ + "schemaVersion": 1, + "taskId": "MW-20260818-005", + "baselineSha": "4320a062d641c84c9ed5ec67e1af09551f62d430", + "tasks": [ + { + "id": "aggregate-pr-plan", + "owner": "WORKFLOW_PLANNER", + "scope": ["read-only review of 4320a06..3ba4d6d PR evidence"], + "dependsOn": [], + "verification": ["contract completeness review"] + }, + { + "id": "aggregate-evidence-record", + "owner": "WORKFLOW_IMPLEMENTER", + "scope": ["Supervisor-owned MW-20260818-005 ledger content proposal only"], + "dependsOn": ["aggregate-pr-plan"], + "verification": ["evidence consistency review"] + }, + { + "id": "ledger-integration", + "owner": "WORKFLOW_INTEGRATOR", + "scope": ["MW-20260818-005 ledger record only"], + "dependsOn": ["aggregate-evidence-record"], + "verification": ["ledger validation", "git diff --check"] + }, + { + "id": "fixed-pr-candidate-review", + "owner": "WORKFLOW_VERIFY_REVIEWER", + "scope": ["fixed candidate 3ba4d6d and aggregate evidence only"], + "dependsOn": ["ledger-integration"], + "verification": ["fixed-SHA review", "Python workflow tests"] + } + ], + "acceptanceCriteria": [ + "PR 正文的公共 API、Android、iOS、Desktop 和 JS/Wasm 影响与汇总契约一致。", + "候选 SHA 是 PR 头的祖先,账本处于 PR_READY 且审查裁决为 PASS。", + "不改动已审查的候选代码。" + ] +} diff --git a/.codex/workflow/runs/MW-20260818-005/review-report.md b/.codex/workflow/runs/MW-20260818-005/review-report.md new file mode 100644 index 0000000..19435c9 --- /dev/null +++ b/.codex/workflow/runs/MW-20260818-005/review-report.md @@ -0,0 +1,27 @@ +# MW-20260818-005 固定候选审查报告 + +候选 SHA:3ba4d6d41c327650ace3555aebc832cce1e71275 +裁决:PASS + +## 已验证 + +- 审查前后 `git status --short`:退出 `0`,干净 worktree 未变更。 +- `git rev-parse HEAD`:退出 `0`,等于固定候选 SHA。 +- 基线与候选祖先关系、候选与审查 HEAD 祖先关系:退出 `0`。 +- `git diff --check 4320a062d641c84c9ed5ec67e1af09551f62d430 3ba4d6d41c327650ace3555aebc832cce1e71275`:退出 `0`。 +- `PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tools.tests.test_agent_workflow -v`:退出 `0`,48 项通过。 +- `./gradlew apiCheck :webview-android:assembleRelease :webview-android:compileDebugAndroidTestKotlin :webview-desktop:check :webview-ios:compileTestKotlinIosSimulatorArm64 :sample-compose:compileKotlinJs :sample-compose:compileKotlinWasmJs --no-parallel -Dorg.gradle.jvmargs=-Xmx2g`:退出 `0`,全部成功。 + +## 审查结论 + +- 公共 API:`ExactHttpsHosts` 保持构造、`copy`、`component1` 与等值语义,并收紧非法主机输入。 +- Android:门面与原生消息入口均限制顶层框架。 +- iOS/Desktop:空允许主机集合在可信 JS URL 入口显式拒绝。 +- JS/Wasm:共享 KMP 策略随 JS/Wasm 编译通过。 +- 安全默认值:未发现 JS 桥来源边界绕过,也未放宽 Cookie、文件访问、下载或外部导航权限。 + +## 风险 + +无 P0。无 P1。 + +P2:未执行 Android System WebView instrumentation runtime、WKWebView runtime、JS/Wasm 浏览器 runtime;替代证据为 Android instrumentation Kotlin 编译、iOS Simulator 测试 Kotlin 编译与 JS/Wasm 编译通过。Android 回归测试位于 `webview-android/src/androidTest/kotlin/io/github/multiweb/android/AndroidScriptBridgeInstrumentationTest.kt`。 diff --git a/.codex/workflow/runs/MW-20260818-005/state.json b/.codex/workflow/runs/MW-20260818-005/state.json new file mode 100644 index 0000000..1af9831 --- /dev/null +++ b/.codex/workflow/runs/MW-20260818-005/state.json @@ -0,0 +1,20 @@ +{ + "schemaVersion": 1, + "taskId": "MW-20260818-005", + "baselineSha": "4320a062d641c84c9ed5ec67e1af09551f62d430", + "status": "PR_READY", + "mode": "FULL", + "currentStage": "PR", + "attempts": { + "PLANNING": 1, + "IMPLEMENTING": 2, + "VALIDATING": 2, + "REVIEWING": 2 + }, + "candidateSha": "3ba4d6d41c327650ace3555aebc832cce1e71275", + "verdict": "PASS", + "nextAction": "由 Planner 审核 PR 汇总证据契约", + "lastEventAt": "2026-08-18T04:32:02.260838+00:00", + "stopReason": null, + "reviewedCandidateSha": "3ba4d6d41c327650ace3555aebc832cce1e71275" +} diff --git a/.codex/workflow/runs/MW-20260818-005/task-contract.md b/.codex/workflow/runs/MW-20260818-005/task-contract.md new file mode 100644 index 0000000..765eb70 --- /dev/null +++ b/.codex/workflow/runs/MW-20260818-005/task-contract.md @@ -0,0 +1,60 @@ +# MW-20260818-005 PR 全量汇总证据契约 + +- 需求来源:PR #25 治理门禁要求 PR 正文的唯一账本记录与完整 PR 差异一致。 +- 目标:为完整差异 `4320a062d641c84c9ed5ec67e1af09551f62d430..3ba4d6d41c327650ace3555aebc832cce1e71275` 建立唯一、可复核的汇总证据;固定候选保持不变,PR 正文只引用本任务账本。 +- 非目标:不修改候选产品代码、API 基线、Gradle、发布配置、既有任务账本或旧远端分支;不创建 `handoffs/`。 +- 基线提交 SHA:`4320a062d641c84c9ed5ec67e1af09551f62d430`。 +- 固定候选 SHA:`3ba4d6d41c327650ace3555aebc832cce1e71275`。 +- 模式:`FULL`。完整范围涉及公共扩展 API、跨平台 JS 桥安全行为与共享 KMP 代码,不能使用 `TRIVIAL` 快速路径。 +- 账本路径:`.codex/workflow/runs/MW-20260818-005/`。 +- Planner:`/root/pr_evidence_planner`;不批准第二个 Implementer,不创建平台专项 Agent。 + +## DAG 与所有权 + +`Planner -> 单一 Implementer -> Integrator -> Verify-Reviewer -> Supervisor 更新 PR 正文` + +| 角色 | 独占范围或职责 | 依赖 | 最小验证 | +| --- | --- | --- | --- | +| Planner | 只读完整差异、既有账本与 PR 门禁,输出汇总契约 | 无 | 契约完整性与影响映射 | +| 单一 Implementer | 仅生成账本文本、候选 SHA、审查报告和 PR 正文内容建议 | Planner | SHA、平台标签和证据一致性自检 | +| Supervisor | 唯一可落盘本任务账本并迁移状态 | Implementer 建议 | `agent_workflow.py validate` | +| Integrator | 仅核对建议、既有候选和账本状态,创建账本记录提交 | Supervisor 已落盘的证据 | `git diff --check`、候选祖先关系、账本验证 | +| Verify-Reviewer | 干净 worktree 对固定候选做只读完整差异审查 | Integrator | 固定 SHA 审查与契约命令 | + +禁止修改范围:所有产品模块、API 基线、Gradle、发布配置、既有任务账本和候选提交。`MW-20260818-005` 账本文件仅由 Supervisor 落盘。 + +## 完整差异与影响 + +- 公共 API:受影响;验证证据:公开 `ScriptBridgeOriginPolicy.ExactHttpsHosts` 收紧输入与集合快照语义,`WebViewConfig.persistentSessionEnabled` 的跨平台默认值说明更新;需检查公开构造、`copy`、`component1`、相等性和 API 兼容性。 +- Android:受影响;验证证据:精确 HTTPS JS 桥只向顶层窗口注入,并有同源 `srcdoc` 子框架回归;既有 Android unit、androidTest Kotlin 编译和 release assemble 通过。 +- iOS:受影响;验证证据:空允许主机集合在可信 JS URL 入口显式拒绝;既有 iOS Simulator 测试 Kotlin 编译通过。 +- Desktop:受影响;验证证据:空允许主机集合在可信 JS URL 入口显式拒绝;既有 Desktop JVM 测试通过。 +- JS/Wasm:受影响;验证证据:`webview-extension-api` 是共享 KMP 公共代码,`ExactHttpsHosts` 构造与拒绝语义进入 JS/Wasm 消费端;既有示例 JS/Wasm 编译通过。 +- 安全默认值:仅收紧 JS 桥来源边界。精确 HTTPS 主机必须为非空精确 ASCII 主机且使用 443 默认端口;Android 门面只向顶层窗口暴露;iOS/Desktop 空白名单拒绝。未放宽 Cookie、文件访问、下载或外部导航权限。 +- 文档与治理:默认不生成 `handoffs/`,停止状态必须带原因和唯一下一步,默认角色链路固定为 Planner、单一 Implementer、Integrator、Verify-Reviewer。 + +## 验收与验证 + +- PR 正文与本契约的公共 API、Android、iOS、Desktop、JS/Wasm 均标记为 `受影响`,每项都有实际通过命令或既有验证证据。 +- `candidate.sha`、`state.json`、`review-report.md` 与 PR 正文使用同一 `3ba4d6d41c327650ace3555aebc832cce1e71275`;候选必须是 PR 头祖先。 +- Verify-Reviewer 必须运行: + + ```sh + PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tools.tests.test_agent_workflow -v + ./gradlew apiCheck :webview-android:assembleRelease :webview-android:compileDebugAndroidTestKotlin :webview-desktop:check :webview-ios:compileTestKotlinIosSimulatorArm64 :sample-compose:compileKotlinJs :sample-compose:compileKotlinWasmJs --no-parallel -Dorg.gradle.jvmargs=-Xmx2g + git diff --check 4320a062d641c84c9ed5ec67e1af09551f62d430 3ba4d6d41c327650ace3555aebc832cce1e71275 + git status --short + git rev-parse HEAD + ``` + +- iOS 的最低编译验证为 `:webview-ios:compileTestKotlinIosSimulatorArm64`,不等价于 iOS/WKWebView 运行时测试;运行时未验证必须保留为未验证范围,不能标为通过。 +- 账本验证必须在包含候选后 `MW-20260818-005` 记录的 PR HEAD worktree 执行: + + ```sh + git merge-base --is-ancestor 3ba4d6d41c327650ace3555aebc832cce1e71275 HEAD + python3 tools/agent_workflow.py validate .codex/workflow/runs/MW-20260818-005 + ``` + +- 未验证范围:Android System WebView instrumentation runtime、WKWebView runtime、JS/Wasm runtime 必须记录原因和替代编译证据。 +- P1 退回条件:五个平台标签不一致、任一 SHA 不同或不为 PR 头祖先、账本不是 `PR_READY/PASS`、`apiCheck` 或受影响平台构建失败、来源校验被放宽、缺少实际通过证据或 PR 正文仍引用旧账本。 +- P0:发现凭据泄露或可绕过来源限制向不可信文档暴露桥能力时立即阻断 PR。