From fb97377ae4d656c6add5ad8cb270cfe2f232c92c Mon Sep 17 00:00:00 2001 From: Jay Shen Date: Sat, 22 Aug 2026 23:32:54 +0800 Subject: [PATCH 01/13] test: prototype M9-A0 OpenClaw trusted seams Signed-off-by: Jay Shen --- .../evidence/model-gate-wiring.test.ts | 55 ++++++ .../openclaw-2026.4.14-trusted-boundary.patch | 162 ++++++++++++++++++ .../evidence/prototype-results.txt | 33 ++++ .../evidence/source-seam.test.ts | 116 +++++++++++++ .../plan.md | 44 +++++ .../result.md | 72 ++++++++ 6 files changed, 482 insertions(+) create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-source-seam-prototype/evidence/model-gate-wiring.test.ts create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-source-seam-prototype/evidence/openclaw-2026.4.14-trusted-boundary.patch create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-source-seam-prototype/evidence/prototype-results.txt create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-source-seam-prototype/evidence/source-seam.test.ts create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-source-seam-prototype/plan.md create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-source-seam-prototype/result.md diff --git a/smoke-testing/runs/2026-08-22-m9-a0-source-seam-prototype/evidence/model-gate-wiring.test.ts b/smoke-testing/runs/2026-08-22-m9-a0-source-seam-prototype/evidence/model-gate-wiring.test.ts new file mode 100644 index 0000000..8489dba --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-source-seam-prototype/evidence/model-gate-wiring.test.ts @@ -0,0 +1,55 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { + cleanupTempPaths, + createContextEngineAttemptRunner, + createContextEngineBootstrapAndAssemble, + getHoisted, + resetEmbeddedAttemptHarness, +} from "./attempt.spawn-workspace.test-support.js"; + +describe("M9-A0 patched before_model_call wiring", () => { + const tempPaths: string[] = []; + + afterEach(async () => { + resetEmbeddedAttemptHarness(); + await cleanupTempPaths(tempPaths); + }); + + it("blocks the actual session/provider call when the model gate rejects", async () => { + const promptCalls: string[] = []; + const events: string[] = []; + const hookRunner = { + hasHooks: (name: string) => { + events.push(`has:${name}`); + return name === "before_model_call"; + }, + runBeforeModelCall: vi.fn(async () => { + events.push("before_model_call"); + return { block: true, blockReason: "BOOTSTRAP_INVALID" }; + }), + }; + getHoisted().getGlobalHookRunnerMock.mockReturnValue(hookRunner); + + const attemptPromise = createContextEngineAttemptRunner({ + contextEngine: createContextEngineBootstrapAndAssemble(), + sessionKey: "agent:test:main", + tempPaths, + sessionPrompt: async (_session, prompt) => { + events.push("session_prompt"); + promptCalls.push(prompt); + }, + attemptOverrides: { disableTools: true, timeoutMs: 250 }, + }); + const result = await Promise.race([ + attemptPromise, + new Promise((_, reject) => + setTimeout(() => reject(new Error(`MODEL_GATE_DIAGNOSTIC_TIMEOUT events=${events.join(",")}`)), 15000), + ), + ], 30000); + + expect(promptCalls).toEqual([]); + expect(hookRunner.runBeforeModelCall).toHaveBeenCalledTimes(1); + expect(result.promptError).toBeInstanceOf(Error); + expect(String(result.promptError)).toContain("BOOTSTRAP_INVALID"); + }); +}); diff --git a/smoke-testing/runs/2026-08-22-m9-a0-source-seam-prototype/evidence/openclaw-2026.4.14-trusted-boundary.patch b/smoke-testing/runs/2026-08-22-m9-a0-source-seam-prototype/evidence/openclaw-2026.4.14-trusted-boundary.patch new file mode 100644 index 0000000..a89d702 --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-source-seam-prototype/evidence/openclaw-2026.4.14-trusted-boundary.patch @@ -0,0 +1,162 @@ +--- a/src/plugins/hook-types.ts ++++ b/src/plugins/hook-types.ts +@@ -56,0 +57 @@ ++ | "before_model_call" +@@ -70,0 +72 @@ ++ | "before_tool_result_release" +@@ -87,0 +90 @@ ++ "before_model_call", +@@ -101,0 +105 @@ ++ "before_tool_result_release", +@@ -281,0 +286,14 @@ ++export type PluginHookBeforeModelCallEvent = { ++ prompt: string; ++ systemPrompt?: string; ++ messages: unknown[]; ++ provider: string; ++ model: string; ++ runId?: string; ++}; ++ ++export type PluginHookBeforeModelCallResult = { ++ block?: boolean; ++ blockReason?: string; ++}; ++ +@@ -323,0 +342,13 @@ ++export type PluginHookBeforeToolResultReleaseEvent = { ++ toolName: string; ++ params: unknown; ++ toolCallId?: string; ++ runId?: string; ++ result: unknown; ++}; ++ ++export type PluginHookBeforeToolResultReleaseResult = { ++ block?: boolean; ++ blockReason?: string; ++}; ++ +@@ -575,0 +607,4 @@ ++ before_model_call: ( ++ event: PluginHookBeforeModelCallEvent, ++ ctx: PluginHookAgentContext, ++ ) => Promise | PluginHookBeforeModelCallResult | void; +@@ -633,0 +669,4 @@ ++ before_tool_result_release: ( ++ event: PluginHookBeforeToolResultReleaseEvent, ++ ctx: PluginHookToolContext, ++ ) => Promise | PluginHookBeforeToolResultReleaseResult | void; +--- a/src/plugins/hooks.ts ++++ b/src/plugins/hooks.ts +@@ -26,0 +27 @@ ++ PluginHookBeforeModelCallEvent, +@@ -27,0 +29 @@ ++ PluginHookBeforeModelCallResult, +@@ -37,0 +40 @@ ++ PluginHookBeforeToolResultReleaseEvent, +@@ -38,0 +42 @@ ++ PluginHookBeforeToolResultReleaseResult, +@@ -83,0 +88 @@ ++ PluginHookBeforeModelCallEvent, +@@ -84,0 +90 @@ ++ PluginHookBeforeModelCallResult, +@@ -102,0 +109 @@ ++ PluginHookBeforeToolResultReleaseEvent, +@@ -103,0 +111 @@ ++ PluginHookBeforeToolResultReleaseResult, +@@ -526,0 +535,19 @@ ++ * Run before_model_call hook. ++ * Allows a fail-closed plugin to block the final provider/session request. ++ */ ++ async function runBeforeModelCall( ++ event: PluginHookBeforeModelCallEvent, ++ ctx: PluginHookAgentContext, ++ ): Promise { ++ return runModifyingHook<"before_model_call", PluginHookBeforeModelCallResult>( ++ "before_model_call", ++ event, ++ ctx, ++ { ++ shouldStop: (result) => result.block === true, ++ terminalLabel: "block=true", ++ }, ++ ); ++ } ++ ++ /** +@@ -813,0 +841,19 @@ ++ * Run before_tool_result_release hook. ++ * Allows a fail-closed plugin to close a bounded result before it is released. ++ */ ++ async function runBeforeToolResultRelease( ++ event: PluginHookBeforeToolResultReleaseEvent, ++ ctx: PluginHookToolContext, ++ ): Promise { ++ return runModifyingHook<"before_tool_result_release", PluginHookBeforeToolResultReleaseResult>( ++ "before_tool_result_release", ++ event, ++ ctx, ++ { ++ shouldStop: (result) => result.block === true, ++ terminalLabel: "block=true", ++ }, ++ ); ++ } ++ ++ /** +@@ -1111,0 +1158 @@ ++ runBeforeModelCall, +@@ -1131,0 +1179 @@ ++ runBeforeToolResultRelease, +--- a/src/plugins/hook-runner-global.ts ++++ b/src/plugins/hook-runner-global.ts +@@ -44,0 +45,2 @@ ++ before_model_call: "fail-closed", ++ before_tool_result_release: "fail-closed", +--- a/src/agents/pi-tools.before-tool-call.ts ++++ b/src/agents/pi-tools.before-tool-call.ts +@@ -414,0 +415,23 @@ ++ const hookRunner = getGlobalHookRunner(); ++ const release = hookRunner?.hasHooks("before_tool_result_release") ++ ? await hookRunner.runBeforeToolResultRelease( ++ { ++ toolName: normalizedToolName, ++ params: outcome.params, ++ toolCallId, ++ runId: ctx?.runId, ++ result, ++ }, ++ { ++ agentId: ctx?.agentId, ++ sessionKey: ctx?.sessionKey, ++ sessionId: ctx?.sessionId, ++ runId: ctx?.runId, ++ toolName: normalizedToolName, ++ toolCallId, ++ }, ++ ) ++ : undefined; ++ if (release?.block) { ++ throw new Error(release.blockReason || "Tool result release blocked by plugin hook"); ++ } +--- a/src/agents/pi-embedded-runner/run/attempt.ts ++++ b/src/agents/pi-embedded-runner/run/attempt.ts +@@ -2006,0 +2007,17 @@ ++ const modelCallHookResult = hookRunner?.hasHooks("before_model_call") ++ ? await hookRunner.runBeforeModelCall( ++ { ++ prompt: effectivePrompt, ++ systemPrompt: systemPromptText, ++ messages: activeSession.messages, ++ provider: params.provider, ++ model: params.modelId, ++ runId: params.runId, ++ }, ++ hookCtx, ++ ) ++ : undefined; ++ if (modelCallHookResult?.block) { ++ throw new Error(modelCallHookResult.blockReason || "Model call blocked by plugin hook"); ++ } ++ diff --git a/smoke-testing/runs/2026-08-22-m9-a0-source-seam-prototype/evidence/prototype-results.txt b/smoke-testing/runs/2026-08-22-m9-a0-source-seam-prototype/evidence/prototype-results.txt new file mode 100644 index 0000000..d366024 --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-source-seam-prototype/evidence/prototype-results.txt @@ -0,0 +1,33 @@ +spike_phase=source-seam-prototype +baseline=1c1534f +image=tg-worker:dev +image_id=sha256:ac1cb183e5b2c82982f6473fef86ccf128763a31192d711526d843a79edb69ff +image_created=2026-08-21T13:00:21.057325912+08:00 +network=none +openclaw_version=OpenClaw 2026.4.14 (2f35b6f) +openclaw_package={ "name": "openclaw", "version": "2026.4.14", "license": "MIT" } +patch_file=evidence/openclaw-2026.4.14-trusted-boundary.patch +patch_sha256=ff58803f3497c4aa67f40c32cf72a63508dfb992aa87d037eda8b85146d3723f +patch_bytes=5150 +patch_dry_run=pass + +focused_test_1=src/plugins/hooks.before-tool-call.test.ts src/plugins/hooks.security.test.ts src/plugins/hooks.sync-only.test.ts +focused_test_1_result=3 files passed, 25 tests passed +focused_test_2=src/plugins/m9-a0-source-seam.test.ts +focused_test_2_result=1 file passed, 3 tests passed +focused_test_3=src/agents/pi-embedded-runner/run/attempt.test.ts src/agents/pi-tools.before-tool-call.integration.e2e.test.ts +focused_test_3_result=2 files passed, 116 tests passed +focused_test_4=src/agents/pi-embedded-runner/run/attempt.spawn-workspace.bootstrap-warning.test.ts src/agents/pi-embedded-runner/run/attempt.spawn-workspace.context-engine.test.ts +focused_test_4_result=2 files passed, 16 tests passed + +prototype_observations=before_model_call block keeps fake provider request count at zero; thrown model gate fails closed; before_tool_result_release runs after execute and before ordinary release; capture throw rejects release; handler-owned deadline returns block within bounded time +full_project_tsc=blocked_by_container_oom_not_used_as_pass_evidence +product_runtime_changed=false +matrix_started=false +external_resources_created=false + +actual_attempt_model_gate_wiring=blocked_by_existing_harness_timeout +actual_attempt_diagnostic=custom runEmbeddedAttempt harness timed out before any hook/provider event was observed +clean_image_baseline=the same attempt.spawn-workspace.timeout.test timed out on unpatched tg-worker:dev +classification=test-driver-or-image-readiness, not a passing provider-boundary result +layer2_fake_provider_status=not_started diff --git a/smoke-testing/runs/2026-08-22-m9-a0-source-seam-prototype/evidence/source-seam.test.ts b/smoke-testing/runs/2026-08-22-m9-a0-source-seam-prototype/evidence/source-seam.test.ts new file mode 100644 index 0000000..a8e6b7c --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-source-seam-prototype/evidence/source-seam.test.ts @@ -0,0 +1,116 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { createHookRunner } from "/opt/openclaw/src/plugins/hooks.ts"; +import { initializeGlobalHookRunner, resetGlobalHookRunner } from "/opt/openclaw/src/plugins/hook-runner-global.ts"; +import { wrapToolWithBeforeToolCallHook } from "/opt/openclaw/src/agents/pi-tools.before-tool-call.ts"; + +function registry(typedHooks: any[]) { + return { hooks: typedHooks, typedHooks } as any; +} + +afterEach(() => resetGlobalHookRunner()); + +describe("M9-A0 source seam prototype", () => { + it("blocks the final model-call seam and fails closed on handler error", async () => { + const runner = createHookRunner( + registry([ + { + pluginId: "test-model-gate", + hookName: "before_model_call", + priority: 100, + source: "test", + handler: () => ({ block: true, blockReason: "BOOTSTRAP_INVALID" }), + }, + ]), + { failurePolicyByHook: { before_model_call: "fail-closed" } }, + ); + let providerRequests = 0; + const gateResult = await runner.runBeforeModelCall( + { prompt: "p", systemPrompt: "s", messages: [], provider: "fake", model: "fake" }, + { runId: "run-1" }, + ); + if (!gateResult?.block) providerRequests += 1; + expect(gateResult).toEqual({ block: true, blockReason: "BOOTSTRAP_INVALID" }); + expect(providerRequests).toBe(0); + + const failing = createHookRunner( + registry([ + { + pluginId: "test-model-gate", + hookName: "before_model_call", + priority: 100, + source: "test", + handler: () => { throw new Error("BOOTSTRAP_CHECK_FAILED"); }, + }, + ]), + { failurePolicyByHook: { before_model_call: "fail-closed" } }, + ); + await expect( + failing.runBeforeModelCall( + { prompt: "p", systemPrompt: "s", messages: [], provider: "fake", model: "fake" }, + { runId: "run-2" }, + ), + ).rejects.toThrow("before_model_call handler from test-model-gate failed"); + }); + + it("uses a handler-owned deadline for a never-resolving admission", async () => { + const started = performance.now(); + const result = await Promise.race([ + new Promise(() => undefined), + new Promise((resolve) => setTimeout(() => resolve({ block: true, blockReason: "ADMISSION_TIMEOUT" }), 20)), + ]); + expect(result).toEqual({ block: true, blockReason: "ADMISSION_TIMEOUT" }); + expect(performance.now() - started).toBeLessThan(250); + }); + + it("closes a tool result before ordinary release and blocks on capture failure", async () => { + const events: string[] = []; + initializeGlobalHookRunner( + registry([ + { + pluginId: "test-capture", + hookName: "before_tool_result_release", + priority: 100, + source: "test", + handler: (event: any) => { + events.push(`capture:${event.result}`); + return undefined; + }, + }, + ]), + ); + const tool = wrapToolWithBeforeToolCallHook({ + name: "synthetic", + execute: async () => { + events.push("execute"); + return "tool-value"; + }, + } as any); + await expect(tool.execute("call-1", { value: 1 })).resolves.toBe("tool-value"); + expect(events).toEqual(["execute", "capture:tool-value"]); + + resetGlobalHookRunner(); + initializeGlobalHookRunner( + registry([ + { + pluginId: "test-capture", + hookName: "before_tool_result_release", + priority: 100, + source: "test", + handler: () => { throw new Error("SPOOL_WRITE_FAILED"); }, + }, + ]), + ); + let executed = false; + const failingTool = wrapToolWithBeforeToolCallHook({ + name: "synthetic", + execute: async () => { + executed = true; + return "must-not-release"; + }, + } as any); + await expect(failingTool.execute("call-2", { value: 1 })).rejects.toThrow( + "before_tool_result_release handler from test-capture failed", + ); + expect(executed).toBe(true); + }); +}); diff --git a/smoke-testing/runs/2026-08-22-m9-a0-source-seam-prototype/plan.md b/smoke-testing/runs/2026-08-22-m9-a0-source-seam-prototype/plan.md new file mode 100644 index 0000000..818b3d5 --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-source-seam-prototype/plan.md @@ -0,0 +1,44 @@ +# M9-A0 source-seam prototype + +## Scope + +- Issue: #116 +- Baseline: pinned OpenClaw `2026.4.14` (`2f35b6f`) in the existing Worker image +- Purpose: research-only prototype of the minimum explicit source seams required by the revised M9-A0 contract +- Product boundary: no Tiangong runtime change, no M9-A implementation, no OpenClaw upgrade, and no Matrix run + +## Hypothesis under test + +The pinned runtime may support a small, explicitly versioned source patch rather than relying on native fail-open hooks: + +1. Add a fail-closed `before_model_call` seam immediately before the existing `activeSession.prompt(...)` provider/session call. It receives the final prompt/system context and can prevent the provider request. +2. Add a fail-closed `before_tool_result_release` seam in the actual wrapped tool executor after the tool returns and before the result is released to the agent loop. A Tiangong control handler can synchronously close the bounded ToolResult to control spool; a capture failure marks recovery-required and causes the next model-call seam to block. +3. Keep `before_tool_call` admission bounded by a Tiangong handler-owned deadline/AbortSignal; do not rely on an OpenClaw runner timeout. + +These are source-contract prototypes only. The patch must not be called a Tiangong runtime implementation. + +## Serial gates + +1. Inspect exact source call sites, types, hook registration, failure policy, and testability of the proposed seams. +2. Apply the smallest patch in a disposable copy/container of the pinned source; run focused deterministic tests with fake provider and synthetic tool. +3. Only if the patch proves the required no-provider-request and no-release-on-capture-failure facts may a later deterministic Tiangong integration prototype be considered. +4. Basic Matrix remains out of scope until the first three prototype gates pass and the patch decision is reviewed. + +Any failure stops the prototype. No patch is copied into product code or accepted as a version change without exact upstream tag, patch ref/digest, license review, and a separate reviewed decision. + +## Required observations + +- invalid bootstrap blocks before the fake provider receives a request; +- valid bootstrap reaches the fake provider and exposes exact verified provenance; +- a synthetic tool result is closed before ordinary release; +- capture failure causes recovery-required state and prevents the next provider request; +- pre-tool handler timeout/throw denies the tool; +- native `before_prompt_build` and `tool_result_persist` remain negative/observation facts. + +## Evidence and cleanup + +- Use only a disposable Docker container derived from the existing pinned image and a temporary source copy outside the repository. +- Disable external network for all prototype commands. +- Preserve only bounded source diff, test output, version/commit identity, event ordering, and sanitized error codes. +- Remove the temporary container/source copy after evidence is written. +- No credentials, raw provider config, private session data, or unrestricted transcript is allowed in evidence. diff --git a/smoke-testing/runs/2026-08-22-m9-a0-source-seam-prototype/result.md b/smoke-testing/runs/2026-08-22-m9-a0-source-seam-prototype/result.md new file mode 100644 index 0000000..b286514 --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-source-seam-prototype/result.md @@ -0,0 +1,72 @@ +# M9-A0 source-seam prototype result + +## Overall decision + +**Prototype partially passes the lower-level source seam checks, but A0 cannot advance to the real container/fake-provider layer yet.** A disposable patch against pinned OpenClaw `2026.4.14` compiled through focused source tests and demonstrated the intended hook-runner and tool-wrapper behaviors. The actual `runEmbeddedAttempt` harness did not reach any hook/provider event within its timeout; the same existing harness test also times out on the unpatched image, so this is classified as test-driver/image readiness, not as proof of a working provider boundary. + +No patch was copied into Tiangong runtime, no OpenClaw version was changed, and no Matrix or external resource was used. + +## Prototype status + +| Check | Status | Direct fact | +|---|---|---| +| Exact pinned source identity | **PASS** | OpenClaw `2026.4.14 (2f35b6f)`, package license `MIT` | +| Minimal patch applies | **PASS** | 5 source files, `patch --dry-run=pass`, patch SHA-256 recorded | +| Fail-closed model hook runner | **PASS** | 3 custom assertions: block result, thrown error, fake provider count remains zero | +| Tool-result release wrapper | **PASS** | 3 custom assertions: execute → release hook order; capture failure rejects release | +| Handler-owned deadline example | **PASS** | Never-resolving admission returns bounded block | +| Existing OpenClaw focused regressions | **PASS** | 7 files, 54 tests passed in the patched container | +| Actual `runEmbeddedAttempt` provider/session gate | **BLOCKED** | Harness timed out before any hook/provider event; clean unpatched image shows the same timeout | +| Layer 2 real fake-provider path | **NOT STARTED** | Blocked by readiness evidence | + +## Candidate patch + +The disposable patch adds only the minimum source seams: + +- fail-closed `before_model_call`, invoked immediately before the existing `activeSession.prompt(...)` call; +- fail-closed `before_tool_result_release`, invoked by the existing wrapped tool executor after `execute(...)` returns and before ordinary release; +- global failure policies for both new hooks; +- no change to native `before_prompt_build` or `tool_result_persist` semantics. + +Patch artifact: [`openclaw-2026.4.14-trusted-boundary.patch`](evidence/openclaw-2026.4.14-trusted-boundary.patch) + +- Upstream identity: OpenClaw `2026.4.14 (2f35b6f)` +- Upstream license: MIT +- Patch SHA-256: `ff58803f3497c4aa67f40c32cf72a63508dfb992aa87d037eda8b85146d3723f` +- Patch application dry run: passed + +The patch is a research candidate, not an accepted dependency or M9-A implementation. + +## Focused evidence + +- [`prototype-results.txt`](evidence/prototype-results.txt) +- [`source-seam.test.ts`](evidence/source-seam.test.ts) +- [`model-gate-wiring.test.ts`](evidence/model-gate-wiring.test.ts) +- [`openclaw-2026.4.14-trusted-boundary.patch`](evidence/openclaw-2026.4.14-trusted-boundary.patch) + +The patched source tests passed: + +- native hook regression set: 3 files / 25 tests; +- source-seam prototype: 1 file / 3 tests; +- tool integration plus attempt imports: 2 files / 116 tests; +- additional attempt bootstrap/context tests: 2 files / 16 tests; +- combined rerun after patch refresh: 7 files / 54 tests. + +A full project TypeScript check was attempted but the disposable container ran out of memory; it is not used as pass evidence. Vitest source transformation and the focused test sets passed. + +## Readiness blocker + +The next step is to repair or replace the actual OpenClaw attempt/fake-provider harness so a clean unpatched baseline and the patched candidate both reach a deterministic provider/session event. Until that direct machine fact exists, do not claim the `before_model_call` seam prevents a real provider request, and do not start the remaining A0 layers. + +After readiness is proven, rerun the actual patched container path with: + +1. invalid/corrupt bootstrap → zero provider requests; +2. valid bootstrap → fake provider receives exact verified provenance; +3. synthetic tool → spool closure before release; +4. capture failure → recovery-required and no next provider request; +5. pre-tool timeout/throw → tool not executed. + +## Cleanup + +- The disposable OpenClaw container and temporary source copies are to be removed after evidence is finalized. +- No credentials, provider configuration, private session data, raw transcript, Matrix fixture, or external resource was created. From 7d444ee08453875856036c56622077d6a9571a84 Mon Sep 17 00:00:00 2001 From: Jay Shen Date: Sun, 23 Aug 2026 09:01:23 +0800 Subject: [PATCH 02/13] docs: add M9 implementation cost discipline Signed-off-by: Jay Shen --- ...m9-professional-agent-runtime-and-project-knowledge.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/design/m9-professional-agent-runtime-and-project-knowledge.md b/docs/design/m9-professional-agent-runtime-and-project-knowledge.md index 4129dee..1c6d4e1 100644 --- a/docs/design/m9-professional-agent-runtime-and-project-knowledge.md +++ b/docs/design/m9-professional-agent-runtime-and-project-knowledge.md @@ -127,6 +127,14 @@ M9 不实现: `TrustedModelCallBoundary` 与 `TrustedToolExecutionBoundary` 是 control runtime 的两个受信调用边界,不是新的模型 runtime、事实库、ledger 或状态机;它们只复用现有 Package/bootstrap、ToolResult spool、CoordinationStore 和 session/turn facts。 +### 4.1 当前实现成本纪律 + +M9-A0 及后续实现必须同时遵守三条硬约束: + +1. **最薄可信 seam 优先。** 优先复用 pinned runtime 最接近最终调用点的原生能力;model path 先验证 native `before_provider_request`/`onPayload` 能否在所有原生变换后接入唯一、必选且不可后置绕过的 Tiangong handler,tool path 先验证 awaited final tool callback。薄不降低 fail-closed 要求:handler 缺失、身份不符、重复、timeout、throw 或结果畸形时必须阻止相应 provider request/ToolResult release,且之后不能再有未受控 payload 变换。任何更大的 agent-loop、provider 或依赖补丁都必须先证明这些原生 seam 不能满足相同合同。 +2. **不新增持久化事实域。** 两个 boundary 不拥有新的 ledger、表、权威状态机或第二份事实索引;它们只读取既有 authority/session facts,并调用既有 Package/bootstrap、ToolResult spool/receipt、recovery 和 bounded diagnostics 路径。`TrustedToolExecutionBoundary` 在 release 前写既有 ToolResult spool 不违反本约束;以 boundary 名义另建数据库表、通用 Evidence 存储或平行恢复账本必须拒绝。 +3. **设置 patch budget 与升级闸门。** 若修补开始跨越多个独立模型生命周期、修改依赖内部 agent loop、引入持久化,或随每个 provider 重复扩张,立即停止实现并比较最小 pinned patch、向上游贡献标准 seam 与受审 OpenClaw 升级。补丁膨胀只是重新决策触发器,不自动证明升级更便宜;升级必须作为单独决策,更新精确 runtime identity、重新执行 M9-A0,且不得由实现人员静默改变 `2026.4.14`。采用补丁时有效 runtime identity 必须同时包含 upstream version/commit 与 patch-set digest。 + 必须始终区分: - Human 或 Leader 的协调; From b911ffab30060307d4647e5449108baf8b3c88eb Mon Sep 17 00:00:00 2001 From: Jay Shen Date: Sun, 23 Aug 2026 09:04:10 +0800 Subject: [PATCH 03/13] test: revise M9-A0 trusted boundary spike Signed-off-by: Jay Shen --- .../result.md | 2 + ...-2026.4.14-trusted-native-boundaries.patch | 278 ++++++++++ .../trusted-boundaries.provider.test.ts | 260 ++++++++++ .../evidence/trusted-boundaries.test.ts | 487 ++++++++++++++++++ .../plan.md | 134 +++++ .../run-source-contract.sh | 89 ++++ 6 files changed, 1250 insertions(+) create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/openclaw-2026.4.14-trusted-native-boundaries.patch create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/trusted-boundaries.provider.test.ts create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/trusted-boundaries.test.ts create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/plan.md create mode 100755 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-source-contract.sh diff --git a/smoke-testing/runs/2026-08-22-m9-a0-source-seam-prototype/result.md b/smoke-testing/runs/2026-08-22-m9-a0-source-seam-prototype/result.md index b286514..dd9be53 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-source-seam-prototype/result.md +++ b/smoke-testing/runs/2026-08-22-m9-a0-source-seam-prototype/result.md @@ -1,5 +1,7 @@ # M9-A0 source-seam prototype result +> **Superseded candidate:** a later source review rejected this patch as the implementation candidate because its model hook ran before `AgentSession.prompt()` rather than at each final provider payload, and its raw tool wrapper could be normalized into an ordinary ToolResult. Preserve this file as the blocked historical result; do not rerun or promote its patch. The replacement plan is [`../2026-08-22-m9-a0-trusted-native-boundaries/plan.md`](../2026-08-22-m9-a0-trusted-native-boundaries/plan.md). + ## Overall decision **Prototype partially passes the lower-level source seam checks, but A0 cannot advance to the real container/fake-provider layer yet.** A disposable patch against pinned OpenClaw `2026.4.14` compiled through focused source tests and demonstrated the intended hook-runner and tool-wrapper behaviors. The actual `runEmbeddedAttempt` harness did not reach any hook/provider event within its timeout; the same existing harness test also times out on the unpatched image, so this is classified as test-driver/image readiness, not as proof of a working provider boundary. diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/openclaw-2026.4.14-trusted-native-boundaries.patch b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/openclaw-2026.4.14-trusted-native-boundaries.patch new file mode 100644 index 0000000..b232d02 --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/openclaw-2026.4.14-trusted-native-boundaries.patch @@ -0,0 +1,278 @@ +diff --git a/src/agents/pi-embedded-runner/compact.ts b/src/agents/pi-embedded-runner/compact.ts +index 2c4236e..7366b84 100644 +--- a/src/agents/pi-embedded-runner/compact.ts ++++ b/src/agents/pi-embedded-runner/compact.ts +@@ -121,0 +122,4 @@ import { truncateSessionAfterCompaction } from "./session-truncation.js"; ++import { ++ areTiangongTrustedBoundariesRequired, ++ installTiangongTrustedBoundariesFromEnv, ++} from "./trusted-boundaries.js"; +@@ -789,0 +794,3 @@ export async function compactEmbeddedPiSessionDirect( ++ if (areTiangongTrustedBoundariesRequired(process.env)) { ++ settingsManager.setCompactionEnabled(false); ++ } +@@ -879,0 +887,12 @@ export async function compactEmbeddedPiSessionDirect( ++ installTiangongTrustedBoundariesFromEnv({ ++ agent: session.agent, ++ context: { ++ runId, ++ agentId: sessionAgentId, ++ sessionId: session.sessionId, ++ workspaceDir: effectiveWorkspace, ++ ...(params.sessionKey ? { sessionKey: params.sessionKey } : {}), ++ ...(trigger ? { trigger } : {}), ++ }, ++ env: process.env, ++ }); +diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts +index 38aca02..4280460 100644 +--- a/src/agents/pi-embedded-runner/run/attempt.ts ++++ b/src/agents/pi-embedded-runner/run/attempt.ts +@@ -168,0 +169,4 @@ import { truncateOversizedToolResultsInSessionManager } from "../tool-result-tru ++import { ++ areTiangongTrustedBoundariesRequired, ++ installTiangongTrustedBoundariesFromEnv, ++} from "../trusted-boundaries.js"; +@@ -916,0 +921,6 @@ export async function runEmbeddedAttempt( ++ // Pi AgentSession compaction calls the provider outside agent.onPayload. ++ // Required Tiangong mode disables that bypass and uses the separately ++ // wrapped OpenClaw compaction path instead. ++ if (areTiangongTrustedBoundariesRequired(process.env)) { ++ settingsManager.setCompactionEnabled(false); ++ } +@@ -993,0 +1004,12 @@ export async function runEmbeddedAttempt( ++ installTiangongTrustedBoundariesFromEnv({ ++ agent: activeSession.agent, ++ context: { ++ runId: params.runId, ++ agentId: sessionAgentId, ++ sessionId: activeSession.sessionId, ++ workspaceDir: effectiveWorkspace, ++ ...(params.sessionKey ? { sessionKey: params.sessionKey } : {}), ++ ...(params.trigger ? { trigger: params.trigger } : {}), ++ }, ++ env: process.env, ++ }); +diff --git a/src/agents/pi-embedded-runner/trusted-boundaries.ts b/src/agents/pi-embedded-runner/trusted-boundaries.ts +new file mode 100644 +index 0000000..2977f53 +--- /dev/null ++++ b/src/agents/pi-embedded-runner/trusted-boundaries.ts +@@ -0,0 +1,161 @@ ++import type { AfterToolCallContext, Agent } from "@mariozechner/pi-agent-core"; ++import type { GlobalHookRunnerRegistry } from "../../plugins/hook-registry.types.js"; ++import * as globalHookRunner from "../../plugins/hook-runner-global.js"; ++import type { ++ PluginHookAgentContext, ++ PluginHookBeforeModelCallResult, ++ PluginHookBeforeToolResultReleaseResult, ++ PluginHookRegistration, ++} from "../../plugins/hook-types.js"; ++ ++export const TIANGONG_TRUSTED_BOUNDARIES_REQUIRED_ENV = "TIANGONG_TRUSTED_BOUNDARIES_REQUIRED"; ++const PLUGIN_ID = "tiangong-control"; ++ ++type BoundaryAgent = Pick; ++type BoundaryContext = Omit; ++type RequiredHookName = "before_model_call" | "before_tool_result_release"; ++ ++function requiredHook( ++ registry: GlobalHookRunnerRegistry, ++ hookName: K, ++): PluginHookRegistration { ++ if (!registry.plugins.some((plugin) => plugin.id === PLUGIN_ID && plugin.status === "loaded")) { ++ throw new Error(`TRUSTED_BOUNDARY_PLUGIN_NOT_LOADED:${PLUGIN_ID}`); ++ } ++ const hooks = registry.typedHooks.filter( ++ (hook) => hook.pluginId === PLUGIN_ID && hook.hookName === hookName, ++ ) as PluginHookRegistration[]; ++ if (hooks.length !== 1) { ++ throw new Error(`TRUSTED_BOUNDARY_HANDLER_COUNT:${hookName}:${hooks.length}`); ++ } ++ return hooks[0]; ++} ++ ++async function runModelHook(params: { ++ registry: GlobalHookRunnerRegistry; ++ event: Parameters["handler"]>[0]; ++ context: Parameters["handler"]>[1]; ++}): Promise { ++ let result: PluginHookBeforeModelCallResult | void; ++ try { ++ result = await requiredHook(params.registry, "before_model_call").handler( ++ params.event, ++ params.context, ++ ); ++ } catch (cause) { ++ throw new Error("TRUSTED_BOUNDARY_HANDLER_FAILED:before_model_call", { cause }); ++ } ++ if ( ++ !result || ++ result.allow !== true || ++ !Object.hasOwn(result, "payload") || ++ result.payload === undefined ++ ) { ++ throw new Error("TRUSTED_BOUNDARY_INVALID_RESULT:before_model_call"); ++ } ++ return result; ++} ++ ++async function runToolHook(params: { ++ registry: GlobalHookRunnerRegistry; ++ event: Parameters["handler"]>[0]; ++ context: Parameters["handler"]>[1]; ++}): Promise { ++ let result: PluginHookBeforeToolResultReleaseResult | void; ++ try { ++ result = await requiredHook(params.registry, "before_tool_result_release").handler( ++ params.event, ++ params.context, ++ ); ++ } catch (cause) { ++ throw new Error("TRUSTED_BOUNDARY_HANDLER_FAILED:before_tool_result_release", { cause }); ++ } ++ if (!result || result.release !== true) { ++ throw new Error("TRUSTED_BOUNDARY_INVALID_RESULT:before_tool_result_release"); ++ } ++ return result; ++} ++ ++/** Install final, required handlers around Pi's native provider and tool-release seams. */ ++export function installTiangongTrustedBoundaries(params: { ++ agent: BoundaryAgent; ++ registry: GlobalHookRunnerRegistry | null; ++ context: BoundaryContext; ++}): void { ++ if (!params.registry) { ++ throw new Error("TRUSTED_BOUNDARY_REGISTRY_MISSING"); ++ } ++ requiredHook(params.registry, "before_model_call"); ++ requiredHook(params.registry, "before_tool_result_release"); ++ ++ const nativeBeforeProviderRequest = params.agent.onPayload; ++ params.agent.onPayload = async (payload, model) => { ++ const nativeResult = await nativeBeforeProviderRequest?.(payload, model); ++ const finalNativePayload = nativeResult === undefined ? payload : nativeResult; ++ const result = await runModelHook({ ++ registry: params.registry!, ++ event: { ++ payload: finalNativePayload, ++ provider: model.provider, ++ model: model.id, ++ ...(params.context.runId ? { runId: params.context.runId } : {}), ++ }, ++ context: { ++ ...params.context, ++ modelProviderId: model.provider, ++ modelId: model.id, ++ }, ++ }); ++ return result.payload; ++ }; ++ ++ const nativeAfterToolCall = params.agent.afterToolCall; ++ params.agent.afterToolCall = async (event: AfterToolCallContext, signal?: AbortSignal) => { ++ const nativeResult = await nativeAfterToolCall?.(event, signal); ++ const toolName = event.toolCall.name; ++ const toolCallId = event.toolCall.id; ++ await runToolHook({ ++ registry: params.registry!, ++ event: { ++ toolName, ++ params: event.args, ++ toolCallId, ++ result: { ++ content: nativeResult?.content ?? event.result.content, ++ details: nativeResult?.details ?? event.result.details, ++ }, ++ isError: nativeResult?.isError ?? event.isError, ++ ...(params.context.runId ? { runId: params.context.runId } : {}), ++ }, ++ context: { ...params.context, toolName, toolCallId }, ++ }); ++ return nativeResult; ++ }; ++} ++ ++/** Stock OpenClaw stays unchanged; the dedicated Worker image must pin this to `1`. */ ++export function areTiangongTrustedBoundariesRequired(env: NodeJS.ProcessEnv): boolean { ++ const raw = env[TIANGONG_TRUSTED_BOUNDARIES_REQUIRED_ENV]?.trim(); ++ if (!raw) { ++ return false; ++ } ++ if (raw !== "1") { ++ throw new Error(`TRUSTED_BOUNDARY_INVALID_ACTIVATION:${raw}`); ++ } ++ return true; ++} ++ ++export function installTiangongTrustedBoundariesFromEnv(params: { ++ agent: BoundaryAgent; ++ registry?: GlobalHookRunnerRegistry | null; ++ context: BoundaryContext; ++ env: NodeJS.ProcessEnv; ++}): boolean { ++ if (!areTiangongTrustedBoundariesRequired(params.env)) { ++ return false; ++ } ++ const registry = ++ params.registry === undefined ? globalHookRunner.getGlobalPluginRegistry() : params.registry; ++ installTiangongTrustedBoundaries({ ...params, registry }); ++ return true; ++} +diff --git a/src/plugins/hook-types.ts b/src/plugins/hook-types.ts +index a518668..62ac45a 100644 +--- a/src/plugins/hook-types.ts ++++ b/src/plugins/hook-types.ts +@@ -59,0 +60 @@ export type PluginHookName = ++ | "before_model_call" +@@ -71,0 +73 @@ export type PluginHookName = ++ | "before_tool_result_release" +@@ -90,0 +93 @@ export const PLUGIN_HOOK_NAMES = [ ++ "before_model_call", +@@ -102,0 +106 @@ export const PLUGIN_HOOK_NAMES = [ ++ "before_tool_result_release", +@@ -162,0 +167,14 @@ export type PluginHookBeforeAgentReplyResult = { ++/** Final provider payload after native before_provider_request transforms. */ ++export type PluginHookBeforeModelCallEvent = { ++ payload: unknown; ++ provider: string; ++ model: string; ++ runId?: string; ++}; ++ ++/** Explicit success handshake; missing or malformed results fail closed. */ ++export type PluginHookBeforeModelCallResult = { ++ allow: true; ++ payload: unknown; ++}; ++ +@@ -333,0 +352,15 @@ export type PluginHookAfterToolCallEvent = { ++/** Final normalized tool outcome immediately before agent-loop release. */ ++export type PluginHookBeforeToolResultReleaseEvent = { ++ toolName: string; ++ params: unknown; ++ runId?: string; ++ toolCallId: string; ++ result: unknown; ++ isError: boolean; ++}; ++ ++/** Explicit success handshake; missing or malformed results fail closed. */ ++export type PluginHookBeforeToolResultReleaseResult = { ++ release: true; ++}; ++ +@@ -587,0 +621,4 @@ export type PluginHookHandlerMap = { ++ before_model_call: ( ++ event: PluginHookBeforeModelCallEvent, ++ ctx: PluginHookAgentContext, ++ ) => Promise | PluginHookBeforeModelCallResult | void; +@@ -637,0 +675,7 @@ export type PluginHookHandlerMap = { ++ before_tool_result_release: ( ++ event: PluginHookBeforeToolResultReleaseEvent, ++ ctx: PluginHookToolContext, ++ ) => ++ | Promise ++ | PluginHookBeforeToolResultReleaseResult ++ | void; diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/trusted-boundaries.provider.test.ts b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/trusted-boundaries.provider.test.ts new file mode 100644 index 0000000..0b20af8 --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/trusted-boundaries.provider.test.ts @@ -0,0 +1,260 @@ +import { createServer, type RequestListener, type Server } from "node:http"; +import type { AddressInfo } from "node:net"; +import { Agent, type AgentTool } from "@mariozechner/pi-agent-core"; +import { streamSimple, type Model } from "@mariozechner/pi-ai"; +import { Type } from "@sinclair/typebox"; +import { afterEach, describe, expect, it } from "vitest"; +import type { GlobalHookRunnerRegistry } from "../../plugins/hook-registry.types.js"; +import type { PluginHookRegistration } from "../../plugins/hook-types.js"; +import { installTiangongTrustedBoundaries } from "./trusted-boundaries.js"; + +const servers: Server[] = []; + +afterEach(async () => { + await Promise.all( + servers + .splice(0) + .map( + (server) => + new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())), + ), + ), + ); +}); + +async function listen(handler: RequestListener): Promise<{ + baseUrl: string; +}> { + const server = createServer(handler); + servers.push(server); + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", resolve); + }); + const address = server.address() as AddressInfo; + return { baseUrl: `http://127.0.0.1:${address.port}/v1` }; +} + +function registry( + modelHandler: PluginHookRegistration<"before_model_call">["handler"], + toolHandler: PluginHookRegistration<"before_tool_result_release">["handler"] = async () => ({ + release: true, + }), +): GlobalHookRunnerRegistry { + return { + hooks: [], + plugins: [{ id: "tiangong-control", status: "loaded" }], + typedHooks: [ + { + pluginId: "tiangong-control", + hookName: "before_model_call", + source: "m9-a0-provider-test", + handler: modelHandler, + }, + { + pluginId: "tiangong-control", + hookName: "before_tool_result_release", + source: "m9-a0-provider-test", + handler: toolHandler, + }, + ], + }; +} + +function createAgent(baseUrl: string, tool?: AgentTool): Agent { + const model: Model<"openai-completions"> = { + id: "fake-model", + name: "Fake model", + api: "openai-completions", + provider: "fake-provider", + baseUrl, + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 4096, + maxTokens: 256, + }; + return new Agent({ + initialState: { + systemPrompt: "untrusted-system", + model, + thinkingLevel: "off", + tools: tool ? [tool] : [], + }, + streamFn: streamSimple, + getApiKey: () => "test-key", + }); +} + +function sendToolCallResponse(response: import("node:http").ServerResponse): void { + response.writeHead(200, { + "content-type": "text/event-stream", + connection: "keep-alive", + "cache-control": "no-cache", + }); + response.write( + `data: ${JSON.stringify({ + id: "chatcmpl-m9-a0-tool", + object: "chat.completion.chunk", + created: 1, + model: "fake-model", + choices: [ + { + index: 0, + delta: { + role: "assistant", + tool_calls: [ + { + index: 0, + id: "call-provider-1", + type: "function", + function: { name: "synthetic", arguments: "{}" }, + }, + ], + }, + finish_reason: null, + }, + ], + })}\n\n`, + ); + response.write( + `data: ${JSON.stringify({ + id: "chatcmpl-m9-a0-tool", + object: "chat.completion.chunk", + created: 1, + model: "fake-model", + choices: [{ index: 0, delta: {}, finish_reason: "tool_calls" }], + })}\n\n`, + ); + response.end("data: [DONE]\n\n"); +} + +function sendStopResponse(response: import("node:http").ServerResponse): void { + response.writeHead(200, { + "content-type": "text/event-stream", + connection: "keep-alive", + "cache-control": "no-cache", + }); + response.write( + `data: ${JSON.stringify({ + id: "chatcmpl-m9-a0", + object: "chat.completion.chunk", + created: 1, + model: "fake-model", + choices: [{ index: 0, delta: { role: "assistant", content: "ok" }, finish_reason: null }], + })}\n\n`, + ); + response.write( + `data: ${JSON.stringify({ + id: "chatcmpl-m9-a0", + object: "chat.completion.chunk", + created: 1, + model: "fake-model", + choices: [{ index: 0, delta: {}, finish_reason: "stop" }], + })}\n\n`, + ); + response.end("data: [DONE]\n\n"); +} + +describe("M9-A0 actual pi-ai provider boundary", () => { + it("sends the trusted payload returned after native provider serialization", async () => { + let requestCount = 0; + let providerBody: Record | undefined; + const { baseUrl } = await listen(async (request, response) => { + requestCount += 1; + const chunks: Buffer[] = []; + for await (const chunk of request) { + chunks.push(Buffer.from(chunk)); + } + providerBody = JSON.parse(Buffer.concat(chunks).toString("utf8")) as Record; + sendStopResponse(response); + }); + const agent = createAgent(baseUrl); + installTiangongTrustedBoundaries({ + agent, + registry: registry(async (event) => { + const payload = event.payload as Record; + return { + allow: true, + payload: { + ...payload, + messages: [{ role: "system", content: "trusted-bootstrap:sha256:m9-a0" }], + }, + }; + }), + context: { runId: "run-provider-valid" }, + }); + + await agent.prompt("hello"); + + expect(requestCount).toBe(1); + expect(providerBody).toMatchObject({ + model: "fake-model", + messages: [{ role: "system", content: "trusted-bootstrap:sha256:m9-a0" }], + }); + expect(agent.state.errorMessage).toBeUndefined(); + }); + + it("does not issue a second HTTP request when tool capture fails", async () => { + let requestCount = 0; + const { baseUrl } = await listen((_request, response) => { + requestCount += 1; + if (requestCount === 1) { + sendToolCallResponse(response); + } else { + sendStopResponse(response); + } + }); + const tool: AgentTool = { + name: "synthetic", + label: "synthetic", + description: "synthetic", + parameters: Type.Object({}), + execute: async () => ({ + content: [{ type: "text", text: "must-not-release" }], + details: { status: "ok" }, + }), + }; + const agent = createAgent(baseUrl, tool); + installTiangongTrustedBoundaries({ + agent, + registry: registry( + async (event) => ({ allow: true, payload: event.payload }), + async () => { + throw new Error("SPOOL_WRITE_FAILED"); + }, + ), + context: { runId: "run-provider-capture-failure" }, + }); + + await agent.prompt("call synthetic"); + + expect(requestCount).toBe(1); + expect(agent.state.messages.some((message) => message.role === "toolResult")).toBe(false); + expect(agent.state.errorMessage).toBe( + "TRUSTED_BOUNDARY_HANDLER_FAILED:before_tool_result_release", + ); + }); + + it("makes zero HTTP requests when the trusted handler fails", async () => { + let requestCount = 0; + const { baseUrl } = await listen((_request, response) => { + requestCount += 1; + sendStopResponse(response); + }); + const agent = createAgent(baseUrl); + installTiangongTrustedBoundaries({ + agent, + registry: registry(async () => { + throw new Error("BOOTSTRAP_INVALID"); + }), + context: { runId: "run-provider-invalid" }, + }); + + await agent.prompt("must not reach HTTP"); + + expect(requestCount).toBe(0); + expect(agent.state.errorMessage).toBe("TRUSTED_BOUNDARY_HANDLER_FAILED:before_model_call"); + }); +}); diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/trusted-boundaries.test.ts b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/trusted-boundaries.test.ts new file mode 100644 index 0000000..3cc2735 --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/trusted-boundaries.test.ts @@ -0,0 +1,487 @@ +import { Agent, type AgentTool, type StreamFn } from "@mariozechner/pi-agent-core"; +import { + createAssistantMessageEventStream, + type AssistantMessage, + type Context, + type Model, +} from "@mariozechner/pi-ai"; +import { Type } from "@sinclair/typebox"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { + initializeGlobalHookRunner, + resetGlobalHookRunner, +} from "../../plugins/hook-runner-global.js"; +import type { GlobalHookRunnerRegistry } from "../../plugins/hook-registry.types.js"; +import type { + PluginHookBeforeModelCallResult, + PluginHookBeforeToolResultReleaseResult, + PluginHookRegistration, +} from "../../plugins/hook-types.js"; +import { wrapToolWithBeforeToolCallHook } from "../pi-tools.before-tool-call.js"; +import { + installTiangongTrustedBoundaries, + installTiangongTrustedBoundariesFromEnv, +} from "./trusted-boundaries.js"; + +const model: Model<"openai-completions"> = { + id: "fake-model", + name: "Fake model", + api: "openai-completions", + provider: "fake-provider", + baseUrl: "http://127.0.0.1.invalid", + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 4096, + maxTokens: 256, +}; + +const emptyUsage = { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 0, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, +}; + +function assistant(params: { + content: AssistantMessage["content"]; + stopReason: AssistantMessage["stopReason"]; +}): AssistantMessage { + return { + role: "assistant", + content: params.content, + api: model.api, + provider: model.provider, + model: model.id, + usage: emptyUsage, + stopReason: params.stopReason, + timestamp: Date.now(), + }; +} + +function registry(params: { + modelHandlers?: Array["handler"]>; + toolHandlers?: Array["handler"]>; + pluginStatus?: "loaded" | "disabled" | "error"; +}): GlobalHookRunnerRegistry { + const typedHooks: PluginHookRegistration[] = [ + ...(params.modelHandlers ?? []).map((handler) => ({ + pluginId: "tiangong-control", + hookName: "before_model_call" as const, + handler, + source: "m9-a0-test", + })), + ...(params.toolHandlers ?? []).map((handler) => ({ + pluginId: "tiangong-control", + hookName: "before_tool_result_release" as const, + handler, + source: "m9-a0-test", + })), + ]; + return { + hooks: [], + typedHooks, + plugins: [{ id: "tiangong-control", status: params.pluginStatus ?? "loaded" }], + }; +} + +function makeProviderStream(params: { + events: string[]; + providerPayloads: unknown[]; + responses: AssistantMessage[]; +}): StreamFn { + let responseIndex = 0; + return (_model, context, options) => { + const stream = createAssistantMessageEventStream(); + void (async () => { + try { + const originalPayload = { + responseIndex, + systemPrompt: context.systemPrompt, + messages: context.messages, + }; + const transformed = await options?.onPayload?.(originalPayload, model); + const payload = transformed === undefined ? originalPayload : transformed; + params.events.push(`provider:${responseIndex}`); + params.providerPayloads.push(payload); + const response = params.responses[responseIndex++]; + if (!response) { + throw new Error("FAKE_PROVIDER_RESPONSE_MISSING"); + } + stream.push({ + type: "done", + reason: response.stopReason === "toolUse" ? "toolUse" : "stop", + message: response, + }); + stream.end(); + } catch (cause) { + const error = assistant({ content: [], stopReason: "error" }); + error.errorMessage = cause instanceof Error ? cause.message : String(cause); + stream.push({ type: "error", reason: "error", error }); + stream.end(); + } + })(); + return stream; + }; +} + +function createAgent(params: { streamFn: StreamFn; tool?: AgentTool; events: string[] }): Agent { + const agent = new Agent({ + initialState: { + systemPrompt: "untrusted-system", + model, + thinkingLevel: "off", + tools: params.tool ? [params.tool] : [], + }, + streamFn: params.streamFn, + onPayload: async (payload) => { + params.events.push("native:before_provider_request"); + return { ...(payload as Record), nativeTransform: true }; + }, + }); + return agent; +} + +const allowModel = (events: string[]) => async (event: { payload: unknown }) => { + events.push("trusted:before_model_call"); + return { + allow: true, + payload: { + ...(event.payload as Record), + systemPrompt: "trusted-bootstrap", + bootstrapDigest: "sha256:m9-a0", + }, + } satisfies PluginHookBeforeModelCallResult; +}; + +const releaseTool = (events: string[]) => async () => { + events.push("trusted:before_tool_result_release"); + return { release: true } satisfies PluginHookBeforeToolResultReleaseResult; +}; + +describe("M9-A0 trusted boundaries", () => { + afterEach(() => { + resetGlobalHookRunner(); + vi.useRealTimers(); + }); + + it("keeps stock behavior disabled but rejects an invalid activation value", () => { + const agent = createAgent({ + streamFn: makeProviderStream({ events: [], providerPayloads: [], responses: [] }), + events: [], + }); + expect( + installTiangongTrustedBoundariesFromEnv({ + agent, + registry: null, + context: { runId: "run-stock" }, + env: {}, + }), + ).toBe(false); + expect(() => + installTiangongTrustedBoundariesFromEnv({ + agent, + registry: null, + context: { runId: "run-invalid-activation" }, + env: { TIANGONG_TRUSTED_BOUNDARIES_REQUIRED: "true" }, + }), + ).toThrow("TRUSTED_BOUNDARY_INVALID_ACTIVATION:true"); + }); + + it("requires the exact loaded plugin and exactly one handler before installation", () => { + const agent = createAgent({ + streamFn: makeProviderStream({ events: [], providerPayloads: [], responses: [] }), + events: [], + }); + expect(() => + installTiangongTrustedBoundaries({ + agent, + registry: null, + context: { runId: "run-missing-registry" }, + }), + ).toThrow("TRUSTED_BOUNDARY_REGISTRY_MISSING"); + expect(() => + installTiangongTrustedBoundaries({ + agent, + registry: registry({ modelHandlers: [], pluginStatus: "loaded" }), + context: { runId: "run-missing-handler" }, + }), + ).toThrow("TRUSTED_BOUNDARY_HANDLER_COUNT:before_model_call:0"); + expect(() => + installTiangongTrustedBoundaries({ + agent, + registry: registry({ modelHandlers: [allowModel([])], toolHandlers: [] }), + context: { runId: "run-missing-tool-handler" }, + }), + ).toThrow("TRUSTED_BOUNDARY_HANDLER_COUNT:before_tool_result_release:0"); + expect(() => + installTiangongTrustedBoundaries({ + agent, + registry: registry({ + modelHandlers: [allowModel([]), allowModel([])], + pluginStatus: "loaded", + }), + context: { runId: "run-duplicate-handler" }, + }), + ).toThrow("TRUSTED_BOUNDARY_HANDLER_COUNT:before_model_call:2"); + }); + + it("runs after native payload transforms on every provider turn", async () => { + const events: string[] = []; + const providerPayloads: unknown[] = []; + const responses = [ + assistant({ + content: [{ type: "toolCall", id: "call-1", name: "synthetic", arguments: { value: 1 } }], + stopReason: "toolUse", + }), + assistant({ content: [{ type: "text", text: "done" }], stopReason: "stop" }), + ]; + const tool: AgentTool = { + name: "synthetic", + label: "synthetic", + description: "synthetic", + parameters: Type.Object({ value: Type.Number() }), + execute: async () => { + events.push("tool:execute"); + return { + content: [{ type: "text", text: "tool-value" }], + details: { status: "ok" }, + }; + }, + }; + const agent = createAgent({ + streamFn: makeProviderStream({ events, providerPayloads, responses }), + tool, + events, + }); + installTiangongTrustedBoundaries({ + agent, + registry: registry({ + modelHandlers: [allowModel(events)], + toolHandlers: [releaseTool(events)], + }), + context: { runId: "run-valid", sessionId: "session-valid" }, + }); + + await agent.prompt("run synthetic"); + + expect(events).toEqual([ + "native:before_provider_request", + "trusted:before_model_call", + "provider:0", + "tool:execute", + "trusted:before_tool_result_release", + "native:before_provider_request", + "trusted:before_model_call", + "provider:1", + ]); + expect(providerPayloads).toHaveLength(2); + expect(providerPayloads[0]).toMatchObject({ + nativeTransform: true, + systemPrompt: "trusted-bootstrap", + bootstrapDigest: "sha256:m9-a0", + }); + expect(agent.state.messages.filter((message) => message.role === "toolResult")).toHaveLength(1); + }); + + it("captures a normalized tool error before the follow-up provider turn", async () => { + const events: string[] = []; + const providerPayloads: unknown[] = []; + const capturedErrors: boolean[] = []; + const tool: AgentTool = { + name: "synthetic", + label: "synthetic", + description: "synthetic", + parameters: Type.Object({}), + execute: async () => { + events.push("tool:execute"); + throw new Error("SYNTHETIC_FAILURE"); + }, + }; + const agent = createAgent({ + streamFn: makeProviderStream({ + events, + providerPayloads, + responses: [ + assistant({ + content: [{ type: "toolCall", id: "call-error", name: "synthetic", arguments: {} }], + stopReason: "toolUse", + }), + assistant({ content: [{ type: "text", text: "handled" }], stopReason: "stop" }), + ], + }), + tool, + events, + }); + installTiangongTrustedBoundaries({ + agent, + registry: registry({ + modelHandlers: [allowModel(events)], + toolHandlers: [ + async (event) => { + events.push("trusted:before_tool_result_release"); + capturedErrors.push(event.isError); + return { release: true }; + }, + ], + }), + context: { runId: "run-tool-error" }, + }); + + await agent.prompt("tool error"); + + expect(capturedErrors).toEqual([true]); + expect(providerPayloads).toHaveLength(2); + expect(events.indexOf("trusted:before_tool_result_release")).toBeLessThan( + events.indexOf("provider:1"), + ); + expect(agent.state.messages.find((message) => message.role === "toolResult")).toMatchObject({ + role: "toolResult", + isError: true, + }); + }); + + it("keeps provider count at zero when the trusted model handler fails", async () => { + const events: string[] = []; + const providerPayloads: unknown[] = []; + const agent = createAgent({ + streamFn: makeProviderStream({ + events, + providerPayloads, + responses: [assistant({ content: [], stopReason: "stop" })], + }), + events, + }); + installTiangongTrustedBoundaries({ + agent, + registry: registry({ + modelHandlers: [ + async () => { + throw new Error("BOOTSTRAP_INVALID"); + }, + ], + toolHandlers: [releaseTool(events)], + }), + context: { runId: "run-invalid-bootstrap" }, + }); + + await agent.prompt("must not reach provider"); + + expect(providerPayloads).toEqual([]); + expect(events).toEqual(["native:before_provider_request"]); + expect(agent.state.errorMessage).toBe("TRUSTED_BOUNDARY_HANDLER_FAILED:before_model_call"); + }); + + it("stops the turn before ToolResult emission when capture fails", async () => { + const events: string[] = []; + const providerPayloads: unknown[] = []; + const tool: AgentTool = { + name: "synthetic", + label: "synthetic", + description: "synthetic", + parameters: Type.Object({}), + execute: async () => { + events.push("tool:execute"); + return { content: [{ type: "text", text: "must-not-release" }], details: {} }; + }, + }; + const agent = createAgent({ + streamFn: makeProviderStream({ + events, + providerPayloads, + responses: [ + assistant({ + content: [{ type: "toolCall", id: "call-fail", name: "synthetic", arguments: {} }], + stopReason: "toolUse", + }), + assistant({ content: [{ type: "text", text: "must-not-run" }], stopReason: "stop" }), + ], + }), + tool, + events, + }); + installTiangongTrustedBoundaries({ + agent, + registry: registry({ + modelHandlers: [allowModel(events)], + toolHandlers: [ + async () => { + events.push("trusted:before_tool_result_release"); + throw new Error("SPOOL_WRITE_FAILED"); + }, + ], + }), + context: { runId: "run-capture-failure" }, + }); + + await agent.prompt("capture must fail closed"); + + expect(providerPayloads).toHaveLength(1); + expect(events).toEqual([ + "native:before_provider_request", + "trusted:before_model_call", + "provider:0", + "tool:execute", + "trusted:before_tool_result_release", + ]); + expect(agent.state.messages.some((message) => message.role === "toolResult")).toBe(false); + expect(agent.state.errorMessage).toBe( + "TRUSTED_BOUNDARY_HANDLER_FAILED:before_tool_result_release", + ); + }); + + it("executes the handler-owned admission deadline and never invokes the tool", async () => { + vi.useFakeTimers(); + let resolverAborted = false; + let toolExecuted = false; + const deadlineHandler = async () => { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(new Error("ADMISSION_TIMEOUT")), 20); + try { + await new Promise((_resolve, reject) => { + controller.signal.addEventListener( + "abort", + () => { + resolverAborted = true; + reject(controller.signal.reason); + }, + { once: true }, + ); + }); + } finally { + clearTimeout(timer); + } + }; + const beforeToolRegistry = registry({ + modelHandlers: [allowModel([])], + toolHandlers: [releaseTool([])], + }); + beforeToolRegistry.typedHooks.push({ + pluginId: "tiangong-control", + hookName: "before_tool_call", + source: "m9-a0-test", + handler: deadlineHandler, + }); + initializeGlobalHookRunner(beforeToolRegistry); + const wrapped = wrapToolWithBeforeToolCallHook({ + name: "synthetic", + label: "synthetic", + description: "synthetic", + parameters: Type.Object({}), + execute: async () => { + toolExecuted = true; + return { content: [{ type: "text", text: "must-not-run" }], details: {} }; + }, + }); + + const execution = wrapped.execute?.("call-timeout", {}, undefined, undefined); + const rejection = expect(execution).rejects.toThrow(); + await vi.advanceTimersByTimeAsync(20); + + await rejection; + expect(resolverAborted).toBe(true); + expect(toolExecuted).toBe(false); + }); +}); diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/plan.md b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/plan.md new file mode 100644 index 0000000..dd998e0 --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/plan.md @@ -0,0 +1,134 @@ +# M9-A0 trusted native-boundary follow-up + +> Status: ready for independent execution; no A0 pass result exists yet. + +## Scope + +- Issue: #116 +- Baseline: pinned OpenClaw `2026.4.14` (`2f35b6f`) in the existing Worker image +- Predecessor: [`../2026-08-22-m9-a0-source-seam-prototype/result.md`](../2026-08-22-m9-a0-source-seam-prototype/result.md) +- Purpose: replace the rejected session-level/tool-wrapper prototype with the thinnest source seams that can prove the accepted M9-A0 contracts +- Product boundary: research patch and test assets only; no Tiangong runtime enablement, OpenClaw upgrade, database change, external provider, or Matrix run + +The predecessor remains a blocked historical result. Its `before_model_call` placement before `activeSession.prompt(...)` and its raw tool-wrapper capture must not be reused. + +## Candidate design + +The candidate patch is [`evidence/openclaw-2026.4.14-trusted-native-boundaries.patch`](evidence/openclaw-2026.4.14-trusted-native-boundaries.patch). + +- Patch SHA-256: `24671f2d21c9b6d2904e10f7e3874981983f12a6a9968cf7d8ff4001c6f93a1c` +- Upstream license: MIT +- Effective research identity: OpenClaw `2026.4.14 (2f35b6f)` plus the exact patch digest above +- Patch scope: four OpenClaw source files, including one new boundary helper; no dependency source, schema, table, ledger, or product Worker file + +The patch does three things: + +1. **Final model seam:** wraps Pi Agent's native `onPayload` path after native `before_provider_request` transforms. Exactly one loaded `tiangong-control` handler must explicitly return `{ allow: true, payload }`. Missing registry/plugin/handler, duplicate handler, malformed result, timeout, or throw fails closed before the selected provider transport receives the payload. +2. **Final tool seam:** uses pi-agent-core's awaited `afterToolCall` callback. The handler sees the final normalized success/error result before `tool_execution_end` and ToolResult message emission and must explicitly return `{ release: true }`. A capture failure rejects the turn rather than becoming an ordinary ToolResult. +3. **Auxiliary-call guard:** when `TIANGONG_TRUSTED_BOUNDARIES_REQUIRED=1`, Pi AgentSession auto-compaction is disabled because it can call a provider outside `agent.onPayload`; OpenClaw's separately created compaction session receives the same trusted boundaries. + +Stock OpenClaw behavior remains unchanged when the activation variable is absent. A future dedicated Tiangong image must pin it to exactly `1` and verify it in build/entrypoint/preflight code; that product change is outside this spike. + +## Cost discipline + +- Prefer this native `before_provider_request`/`afterToolCall` bridge over a larger model runtime or agent-loop patch. +- The boundaries own no persistence. A prototype handler may call an owned temporary spool, but no new ledger, table, or authoritative state is permitted. +- Stop if correctness requires patching pi-agent-core, adding another persistence domain, or spreading across additional model lifecycle implementations. Compare an upstream seam or reviewed OpenClaw upgrade instead; do not upgrade automatically or silently. + +## Boundary truth table + +| Case | Required direct observation | +|---|---| +| Activation absent in stock regression | Existing focused OpenClaw tests remain green; no trusted handler is required | +| Activation has a non-empty value other than `1` | Attempt fails before any provider request | +| Registry/plugin/model handler/tool handler missing or duplicated | Installation fails; zero provider requests and zero tool execution | +| Native provider transform then trusted allow | Trusted handler receives the native-transformed final payload; provider receives exactly the trusted returned payload | +| Model handler throws, times out, or returns malformed output | Zero provider HTTP requests for that logical call | +| Valid bootstrap | Local fake provider receives exact bounded bootstrap content/provenance selected by the trusted handler | +| Tool success | Tool executes once; capture closes before ToolResult emission and before the next provider request | +| Tool error | Capture receives the normalized error with `isError=true` before the next provider request | +| Capture throws or returns malformed output | No ToolResult message is emitted and no next provider request occurs | +| `before_tool_call` resolver never completes | Handler-owned AbortController deadline fires, the resolver observes abort, and the tool is never invoked | +| OpenClaw compaction path | The separately created compaction session passes the same model boundary | +| Pi AgentSession auto-compaction in required mode | Disabled; it cannot bypass `onPayload` | +| Selected HTTP/WebSocket/custom transport ignores `onPayload` before network | **RED**; stop at the source-contract layer | + +Unknown-tool and invalid-argument immediate outcomes do not pass through pi-agent-core `afterToolCall`. They remain admission/normalization outcomes that M9-A must close through its existing pending-call/Gate path; this spike must not claim that the release seam alone implements their ToolResult persistence. + +## Serial execution + +### Layer 1: source/type/agent-loop contract + +Run from the Tiangong repository root: + +```bash +set -o pipefail +smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-source-contract.sh \ + 2>&1 | tee smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/source-contract-results.txt +``` + +The runner owns one uniquely named `--network none` container, applies the exact patch, copies the two tests into disposable OpenClaw source, runs targeted type checking, exercises a real pi-agent-core loop and local HTTP fake provider, and runs adjacent OpenClaw regressions. It removes only its exact container name through a trap. + +This layer must prove at least: + +- actual pi-ai serialization calls the trusted handler before the local HTTP server observes a request; +- invalid bootstrap produces zero HTTP requests; +- capture failure after a real provider tool call produces no second HTTP request and no ToolResult message; +- successful and error tool outcomes are captured before follow-up provider calls; +- handler-owned admission timeout aborts its resolver and does not execute the tool; +- stock adjacent tests remain green. + +A full OpenClaw typecheck may be added only when the image contains the complete upstream test/helper tree. Missing baseline source files must be reported as readiness failure, not converted into pass evidence. + +### Layer 2: actual patched OpenClaw attempt and local fake provider + +Proceed only after Layer 1 passes. + +- Set `TIANGONG_TRUSTED_BOUNDARIES_REQUIRED=1` in the disposable control process. +- Load exactly one test-only `tiangong-control` handler for each new hook through the real OpenClaw plugin loader. +- Use the actual `runEmbeddedAttempt`/gateway/session path and an owned loopback fake provider; do not manually increment a provider counter based on a hook return. +- Record the selected stream strategy and prove that its implementation awaits `onPayload` before its first socket write. +- Exercise the main turn, post-tool turn, retry/follow-up path, and OpenClaw-owned compaction session. +- Replace the predecessor's timed-out context-engine harness with an observable readiness condition. A timeout before the first readiness event is red and is not provider-boundary evidence. + +### Layer 3: Tiangong control-handler prototype + +Proceed only after Layer 2 passes. + +- Use a bounded test bootstrap and an owned temporary spool beneath a unique temporary root. +- Prove exact handler identity, bootstrap corruption denial, success/error capture, capture failure, recovery-required signaling, and no next provider request. +- Reuse the planned ToolResult spool contract; do not add a table or a new ledger. +- This remains a disposable prototype and must not delete the current product runtime paths. + +### Layer 4: Basic Matrix member turn + +Not authorized by this plan. It remains last and requires separate review of Layers 1–3. Matrix cannot prove the deterministic fail-closed contracts above. + +## Evidence requirements + +The independent runner must create `result.md` and preserve only bounded, sanitized facts: + +- baseline Tiangong commit and clean/dirty status; +- image tag, immutable image ID, creation time, and exact OpenClaw version; +- patch path, byte count, SHA-256, dry-run/apply exit codes, and license; +- every exact command, start/end time, exit code, file count, and test count; +- provider request counts and stable event ordering for each truth-table case; +- selected transport/stream strategy and direct socket-side observation; +- bounded error codes only, without raw prompts, credentials, provider config, or transcripts; +- exact owned container/temp/spool identifiers and post-cleanup absence checks. + +Do not combine separate test invocations into an unexplained aggregate count. A summary is not a substitute for command output and direct provider/tool observations. + +## Stop conditions + +Stop A0 and update the design or make a separate source-patch/upgrade decision if any of these occurs: + +- a supported model-emitting path bypasses the required final handler; +- missing or malformed trusted registration permits a provider request; +- capture failure emits a model-visible ToolResult or permits a subsequent provider request; +- the underlying admission resolver continues with a late state mutation after deadline abort; +- the actual selected transport does not await `onPayload` before network I/O; +- the patch must expand into dependency internals, persistence, or another model/session runtime; +- cleanup cannot prove absence of every owned resource. + +No failure may be addressed by silently upgrading OpenClaw, weakening the truth table, using native fail-open observations as enforcement, or starting M9-A implementation in parallel. diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-source-contract.sh b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-source-contract.sh new file mode 100755 index 0000000..011766e --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-source-contract.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +set -euo pipefail + +RUN_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +IMAGE="${M9_A0_IMAGE:-tg-worker:dev}" +PATCH="$RUN_DIR/evidence/openclaw-2026.4.14-trusted-native-boundaries.patch" +EXPECTED_PATCH_SHA256="24671f2d21c9b6d2904e10f7e3874981983f12a6a9968cf7d8ff4001c6f93a1c" +EXPECTED_VERSION="OpenClaw 2026.4.14 (2f35b6f)" +CONTAINER="tiangong-m9a0-source-$$_$(date -u +%Y%m%dT%H%M%SZ)" + +cleanup() { + if docker container inspect "$CONTAINER" >/dev/null 2>&1; then + docker rm -f "$CONTAINER" >/dev/null + fi +} +trap cleanup EXIT INT TERM + +actual_patch_sha256="$(sha256sum "$PATCH" | awk '{print $1}')" +if [[ "$actual_patch_sha256" != "$EXPECTED_PATCH_SHA256" ]]; then + printf 'patch_sha256_mismatch expected=%s actual=%s\n' \ + "$EXPECTED_PATCH_SHA256" "$actual_patch_sha256" >&2 + exit 1 +fi + +image_id="$(docker image inspect --format '{{.Id}}' "$IMAGE")" +image_created="$(docker image inspect --format '{{.Created}}' "$IMAGE")" +version="$(docker run --rm --network none --entrypoint openclaw "$IMAGE" --version)" +if [[ "$version" != "$EXPECTED_VERSION" ]]; then + printf 'openclaw_version_mismatch expected=%q actual=%q\n' "$EXPECTED_VERSION" "$version" >&2 + exit 1 +fi + +printf 'container=%s\nimage=%s\nimage_id=%s\nimage_created=%s\nopenclaw_version=%s\npatch_sha256=%s\n' \ + "$CONTAINER" "$IMAGE" "$image_id" "$image_created" "$version" "$actual_patch_sha256" + +docker run --rm --name "$CONTAINER" --network none \ + --mount "type=bind,src=$RUN_DIR,dst=/m9-a0,readonly" \ + --entrypoint sh "$IMAGE" -lc ' + set -eu + cd /opt/openclaw + + test "$(openclaw --version)" = "OpenClaw 2026.4.14 (2f35b6f)" + patch -p1 --dry-run < /m9-a0/evidence/openclaw-2026.4.14-trusted-native-boundaries.patch + patch -p1 < /m9-a0/evidence/openclaw-2026.4.14-trusted-native-boundaries.patch + cp /m9-a0/evidence/trusted-boundaries.test.ts \ + src/agents/pi-embedded-runner/trusted-boundaries.test.ts + cp /m9-a0/evidence/trusted-boundaries.provider.test.ts \ + src/agents/pi-embedded-runner/trusted-boundaries.provider.test.ts + + ./node_modules/.bin/oxfmt --check \ + src/plugins/hook-types.ts \ + src/agents/pi-embedded-runner/trusted-boundaries.ts \ + src/agents/pi-embedded-runner/trusted-boundaries.test.ts \ + src/agents/pi-embedded-runner/trusted-boundaries.provider.test.ts \ + src/agents/pi-embedded-runner/run/attempt.ts \ + src/agents/pi-embedded-runner/compact.ts + + cat > tsconfig.m9-a0.json <<"JSON" +{ + "extends": "./tsconfig.json", + "include": [ + "src/plugins/hook-types.ts", + "src/agents/pi-embedded-runner/trusted-boundaries.ts", + "src/agents/pi-embedded-runner/trusted-boundaries.test.ts", + "src/agents/pi-embedded-runner/trusted-boundaries.provider.test.ts" + ], + "exclude": [] +} +JSON + ./node_modules/.bin/tsgo -p tsconfig.m9-a0.json --noEmit + + ./node_modules/.bin/vitest run \ + src/agents/pi-embedded-runner/trusted-boundaries.test.ts \ + src/agents/pi-embedded-runner/trusted-boundaries.provider.test.ts \ + --reporter=verbose + + ./node_modules/.bin/vitest run \ + src/plugins/hooks.before-tool-call.test.ts \ + src/plugins/hooks.security.test.ts \ + src/agents/pi-embedded-runner/run/attempt.test.ts \ + src/agents/pi-embedded-runner/compact.hooks.test.ts \ + --reporter=dot + ' + +if docker container inspect "$CONTAINER" >/dev/null 2>&1; then + printf 'cleanup_container_still_present=%s\n' "$CONTAINER" >&2 + exit 1 +fi +printf 'cleanup_owner=%s\ncleanup_container_absent=true\nm9_a0_source_contract=pass\n' "$CONTAINER" From c8b9011687fad8c7dad3a77112342fad6bfc9109 Mon Sep 17 00:00:00 2001 From: Jay Shen Date: Sun, 23 Aug 2026 09:58:28 +0800 Subject: [PATCH 04/13] test: record M9-A0 native boundary spike Signed-off-by: Jay Shen --- .../evidence/artifact-sha256.txt | 5 + .../layer2-agent-session-readiness.test.ts | 80 ++++++ .../evidence/layer2-results.txt | 42 +++ ...yer2-run-embedded-attempt-baseline.test.ts | 95 +++++++ .../layer2-run-embedded-attempt.test.ts | 253 ++++++++++++++++++ .../evidence/source-contract-results.txt | 51 ++++ .../result.md | 73 +++++ 7 files changed, 599 insertions(+) create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/artifact-sha256.txt create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-agent-session-readiness.test.ts create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-results.txt create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-run-embedded-attempt-baseline.test.ts create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-run-embedded-attempt.test.ts create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/source-contract-results.txt create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/result.md diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/artifact-sha256.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/artifact-sha256.txt new file mode 100644 index 0000000..b267d85 --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/artifact-sha256.txt @@ -0,0 +1,5 @@ +24671f2d21c9b6d2904e10f7e3874981983f12a6a9968cf7d8ff4001c6f93a1c smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/openclaw-2026.4.14-trusted-native-boundaries.patch +9aaf7859734ebe1d42249152c35eee8979273489233edad16a367766c0792570 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-run-embedded-attempt.test.ts +0fce0d9bddbd72c4fd836b5dfc875188ed2accf91f0c242d545f68baa8ef0db4 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-agent-session-readiness.test.ts +f141ed86a379f6333521f3b63bab5300f8b1a25958e60ff3ec031866cb7009ba smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-run-embedded-attempt-baseline.test.ts +b90b1e4a69cd959e2041658046ab7dabea7fdeb145b091acd0454460a50413c2 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/source-contract-results.txt diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-agent-session-readiness.test.ts b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-agent-session-readiness.test.ts new file mode 100644 index 0000000..a4745c4 --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-agent-session-readiness.test.ts @@ -0,0 +1,80 @@ +import http from "node:http"; +import { afterEach, describe, expect, it } from "vitest"; +import { + AuthStorage, + ModelRegistry, + SessionManager, + createAgentSession, +} from "@mariozechner/pi-coding-agent"; +import type { Model } from "@mariozechner/pi-ai"; + +const servers: http.Server[] = []; + +function fakeModel(baseUrl: string): Model<"openai-completions"> { + return { + id: "fake-model", + name: "M9 A0 fake model", + api: "openai-completions", + provider: "fake-provider", + baseUrl, + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 4096, + maxTokens: 256, + }; +} + +describe("M9-A0 AgentSession readiness diagnostic", () => { + afterEach(async () => { + for (const server of servers.splice(0)) { + if (server.listening) await new Promise((resolve) => server.close(() => resolve())); + } + }); + + it("can make one local provider request with the real Pi session", async () => { + let requests = 0; + const server = http.createServer((_request, response) => { + requests += 1; + response.writeHead(200, { "content-type": "text/event-stream" }); + response.end( + `data: ${JSON.stringify({ + id: "m9-a0-session", + object: "chat.completion.chunk", + created: 1, + model: "fake-model", + choices: [{ index: 0, delta: { role: "assistant", content: "ok" }, finish_reason: null }], + })}\n\n` + + `data: ${JSON.stringify({ + id: "m9-a0-session", + object: "chat.completion.chunk", + created: 1, + model: "fake-model", + choices: [{ index: 0, delta: {}, finish_reason: "stop" }], + })}\n\n` + + "data: [DONE]\n\n", + ); + }); + servers.push(server); + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", resolve); + }); + const address = server.address(); + if (!address || typeof address === "string") + throw new Error("SESSION_PROVIDER_ADDRESS_MISSING"); + const authStorage = AuthStorage.inMemory(); + authStorage.setRuntimeApiKey("fake-provider", "fixture-provider-value"); + const { session } = await createAgentSession({ + cwd: "/tmp", + agentDir: "/tmp/m9-a0-agent-dir", + authStorage, + modelRegistry: ModelRegistry.inMemory(authStorage), + model: fakeModel(`http://127.0.0.1:${address.port}/v1`), + tools: [], + sessionManager: SessionManager.inMemory(), + }); + await session.prompt("hello"); + expect(requests).toBe(1); + }, 10_000); +}); diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-results.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-results.txt new file mode 100644 index 0000000..5b79e2a --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-results.txt @@ -0,0 +1,42 @@ +layer=2 +status=blocked +image=tg-worker:dev +image_id=sha256:ac1cb183e5b2c82982f6473fef86ccf128763a31192d711526d843a79edb69ff +openclaw=OpenClaw 2026.4.14 (2f35b6f) +network=none +activation=TIANGONG_TRUSTED_BOUNDARIES_REQUIRED=1 +plugin_loader=loaded_exactly_one_tiangong_control_handler_per_required_hook +plugin_manifest=required_openclaw.plugin.json_fixture_added +provider=owned_loopback_http_fake_provider + +layer1_source_contract=pass +layer1_boundary_tests=10/10 +layer1_adjacent_regressions=156/156 +layer1_cleanup_container_absent=true + +lower_level_agent_session_readiness=pass +lower_level_agent_session_provider_requests=1 +lower_level_agent_session_test=1/1 + +patched_runEmbeddedAttempt_readiness=blocked +patched_runEmbeddedAttempt_readiness_stages=root_created,plugin_written,provider_listening,registry_loaded,hooks_verified,attempt_starting +patched_runEmbeddedAttempt_provider_requests=0 +patched_runEmbeddedAttempt_first_provider_or_handler_event=not_observed +patched_runEmbeddedAttempt_timeout_ms=5000 +patched_runEmbeddedAttempt_debug=embedded_run_start,prompt_build_completed,agent_start,then_no_provider_request_before_abort + +unpatched_runEmbeddedAttempt_baseline=also_blocked +unpatched_baseline_provider_requests=0 +unpatched_baseline_external_command_timeout=25s + +classification=readiness_or_adapter_host_contract +confidence=high +interpretation=real_pi_agent_session_can_reach_loopback_provider; the OpenClaw runEmbeddedAttempt orchestration path does not reach the provider in the disposable image, so this is not evidence that the trusted boundary passed or failed +layer2_tool_turns=not_started +layer2_retry_followup=not_started +layer2_compaction_session=not_started +layer3_tiangong_handler=not_started +layer4_matrix=not_authorized +external_resources_created=none +credentials_or_provider_config=none +cleanup=all ad-hoc containers were removed by shell traps; exact generated names were not retained by the diagnostic commands diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-run-embedded-attempt-baseline.test.ts b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-run-embedded-attempt-baseline.test.ts new file mode 100644 index 0000000..a12dc3c --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-run-embedded-attempt-baseline.test.ts @@ -0,0 +1,95 @@ +import fs from "node:fs/promises"; +import http from "node:http"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent"; +import type { Model } from "@mariozechner/pi-ai"; +import { runEmbeddedAttempt } from "./pi-embedded-runner/run/attempt.js"; + +const servers: http.Server[] = []; +const roots: string[] = []; + +function fakeModel(baseUrl: string): Model<"openai-completions"> { + return { + id: "fake-model", + name: "M9 A0 fake model", + api: "openai-completions", + provider: "fake-provider", + baseUrl, + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 32768, + maxTokens: 256, + }; +} + +afterEach(async () => { + for (const server of servers.splice(0)) { + if (server.listening) await new Promise((resolve) => server.close(() => resolve())); + } + while (roots.length > 0) await fs.rm(roots.pop()!, { recursive: true, force: true }); +}); + +describe("M9-A0 unpatched runEmbeddedAttempt baseline", () => { + it("reaches a local provider request with no Tiangong activation", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "m9-a0-baseline-")); + roots.push(root); + let requestCount = 0; + const server = http.createServer((_request, response) => { + requestCount += 1; + response.writeHead(200, { "content-type": "text/event-stream" }); + response.end( + `data: ${JSON.stringify({ id: "baseline", object: "chat.completion.chunk", created: 1, model: "fake-model", choices: [{ index: 0, delta: { role: "assistant", content: "ok" }, finish_reason: null }] })}\n\n` + + `data: ${JSON.stringify({ id: "baseline", object: "chat.completion.chunk", created: 1, model: "fake-model", choices: [{ index: 0, delta: {}, finish_reason: "stop" }] })}\n\n` + + "data: [DONE]\n\n", + ); + }); + servers.push(server); + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", resolve); + }); + const address = server.address(); + if (!address || typeof address === "string") + throw new Error("BASELINE_PROVIDER_ADDRESS_MISSING"); + const authStorage = AuthStorage.inMemory(); + authStorage.setRuntimeApiKey("fake-provider", "fixture-provider-value"); + const abortController = new AbortController(); + const attempt = runEmbeddedAttempt({ + sessionId: "baseline-session", + sessionKey: "agent:baseline:main", + sessionFile: path.join(root, "session.jsonl"), + workspaceDir: root, + agentDir: root, + config: {}, + prompt: "baseline", + timeoutMs: 10_000, + runId: "baseline-run", + provider: "fake-provider", + modelId: "fake-model", + model: fakeModel(`http://127.0.0.1:${address.port}/v1`), + resolvedApiKey: "fixture-provider-value", + authStorage: authStorage as never, + modelRegistry: ModelRegistry.inMemory(authStorage) as never, + thinkLevel: "off", + senderIsOwner: true, + disableMessageTool: true, + disableTools: true, + abortSignal: abortController.signal, + }); + void attempt.catch(() => undefined); + const result = await Promise.race([ + attempt, + new Promise((_, reject) => + setTimeout(() => { + abortController.abort("BASELINE_READINESS_TIMEOUT"); + reject(new Error(`BASELINE_READINESS_TIMEOUT providerRequests=${requestCount}`)); + }, 5_000), + ), + ]); + expect(result.promptError).toBeNull(); + expect(requestCount).toBe(1); + }, 30_000); +}); diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-run-embedded-attempt.test.ts b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-run-embedded-attempt.test.ts new file mode 100644 index 0000000..bbbc817 --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-run-embedded-attempt.test.ts @@ -0,0 +1,253 @@ +import fs from "node:fs/promises"; +import http from "node:http"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent"; +import { loadOpenClawPlugins, clearPluginLoaderCache } from "../../plugins/loader.js"; +import { + getGlobalPluginRegistry, + resetGlobalHookRunner, +} from "../../plugins/hook-runner-global.js"; +import { runEmbeddedAttempt } from "./run/attempt.js"; +import type { Model } from "@mariozechner/pi-ai"; + +const tempRoots: string[] = []; +const servers: http.Server[] = []; + +async function makeRoot(prefix: string): Promise { + const root = await fs.mkdtemp(path.join(os.tmpdir(), prefix)); + tempRoots.push(root); + return root; +} + +async function listenFakeProvider(): Promise<{ + baseUrl: string; + requestBodies: Array>; + stages: string[]; + close: () => Promise; +}> { + const requestBodies: Array> = []; + const stages: string[] = []; + const server = http.createServer(async (request, response) => { + stages.push("provider_request"); + const chunks: Buffer[] = []; + for await (const chunk of request) chunks.push(Buffer.from(chunk)); + requestBodies.push( + JSON.parse(Buffer.concat(chunks).toString("utf8")) as Record, + ); + response.writeHead(200, { + "content-type": "text/event-stream", + connection: "keep-alive", + "cache-control": "no-cache", + }); + response.write( + `data: ${JSON.stringify({ + id: "m9-a0-layer2", + object: "chat.completion.chunk", + created: 1, + model: "fake-model", + choices: [{ index: 0, delta: { role: "assistant", content: "ok" }, finish_reason: null }], + })}\n\n`, + ); + response.write( + `data: ${JSON.stringify({ + id: "m9-a0-layer2", + object: "chat.completion.chunk", + created: 1, + model: "fake-model", + choices: [{ index: 0, delta: {}, finish_reason: "stop" }], + })}\n\n`, + ); + response.end("data: [DONE]\n\n"); + }); + servers.push(server); + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", resolve); + }); + const address = server.address(); + if (!address || typeof address === "string") throw new Error("FAKE_PROVIDER_ADDRESS_MISSING"); + return { + baseUrl: `http://127.0.0.1:${address.port}/v1`, + requestBodies, + stages, + close: async () => { + await new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())), + ); + }, + }; +} + +function model(baseUrl: string): Model<"openai-completions"> { + return { + id: "fake-model", + name: "M9 A0 fake model", + api: "openai-completions", + provider: "fake-provider", + baseUrl, + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 32768, + maxTokens: 256, + }; +} + +function writePlugin(root: string): Promise { + const plugin = path.join(root, "tiangong-control.cjs"); + const manifest = path.join(root, "openclaw.plugin.json"); + return Promise.all([ + fs.writeFile( + manifest, + JSON.stringify({ id: "tiangong-control", configSchema: { type: "object", properties: {} } }), + "utf8", + ), + fs.writeFile( + plugin, + `const fs = require("node:fs"); +function emit(event, fields) { + fs.appendFileSync(process.env.M9_A0_EVENTS_FILE, JSON.stringify({ event, ...fields }) + "\\n"); +} +module.exports = { + id: "tiangong-control", + register(api) { + api.on("before_model_call", async (event) => { + emit("model-handler", { provider: event.provider, model: event.model }); + return { allow: true, payload: { ...event.payload, m9Trusted: "allowed" } }; + }); + api.on("before_tool_result_release", async (event) => { + emit("tool-handler", { toolName: event.toolName, isError: event.isError }); + return { release: true }; + }); + }, +}; +`, + "utf8", + ), + ]).then(() => plugin); +} + +afterEach(async () => { + resetGlobalHookRunner(); + clearPluginLoaderCache(); + for (const server of servers.splice(0)) { + if (server.listening) await new Promise((resolve) => server.close(() => resolve())); + } + while (tempRoots.length > 0) await fs.rm(tempRoots.pop()!, { recursive: true, force: true }); +}); + +describe("M9-A0 Layer 2 actual runEmbeddedAttempt readiness", () => { + it("loads the trusted plugin through OpenClaw and gates one real provider request", async () => { + const root = await makeRoot("m9-a0-layer2-"); + const stages: string[] = ["root_created"]; + const eventsFile = path.join(root, "events.ndjson"); + const pluginFile = await writePlugin(root); + stages.push("plugin_written"); + const provider = await listenFakeProvider(); + stages.push("provider_listening"); + const previousRequired = process.env.TIANGONG_TRUSTED_BOUNDARIES_REQUIRED; + const previousEvents = process.env.M9_A0_EVENTS_FILE; + process.env.TIANGONG_TRUSTED_BOUNDARIES_REQUIRED = "1"; + process.env.M9_A0_EVENTS_FILE = eventsFile; + try { + const registry = loadOpenClawPlugins({ + cache: false, + workspaceDir: root, + config: { + plugins: { + allow: ["tiangong-control"], + load: { paths: [pluginFile] }, + }, + }, + }); + stages.push("registry_loaded"); + const controlPlugin = registry.plugins.find((plugin) => plugin.id === "tiangong-control"); + if (!controlPlugin || controlPlugin.status !== "loaded") { + throw new Error( + `PLUGIN_DIAGNOSTIC:${JSON.stringify({ + found: Boolean(controlPlugin), + status: controlPlugin?.status, + error: controlPlugin?.error, + source: controlPlugin?.source, + typedHookNames: registry.typedHooks.map((hook) => hook.hookName), + })}`, + ); + } + expect( + registry.typedHooks.filter((hook) => hook.hookName === "before_model_call"), + ).toHaveLength(1); + expect( + registry.typedHooks.filter((hook) => hook.hookName === "before_tool_result_release"), + ).toHaveLength(1); + expect(getGlobalPluginRegistry()).toBe(registry); + stages.push("hooks_verified"); + + const sessionFile = path.join(root, "session.jsonl"); + await fs.writeFile(sessionFile, "", "utf8"); + const authStorage = AuthStorage.inMemory(); + authStorage.setRuntimeApiKey("fake-provider", "fixture-provider-value"); + stages.push("attempt_starting"); + const abortController = new AbortController(); + const attemptPromise = runEmbeddedAttempt({ + sessionId: "layer2-session", + sessionKey: "agent:layer2:main", + sessionFile, + workspaceDir: root, + agentDir: root, + config: { + plugins: { + allow: ["tiangong-control"], + load: { paths: [pluginFile] }, + }, + }, + prompt: "layer2 readiness", + timeoutMs: 10_000, + runId: "layer2-readiness", + provider: "fake-provider", + modelId: "fake-model", + model: model(provider.baseUrl), + resolvedApiKey: "fixture-provider-value", + authStorage: authStorage as never, + modelRegistry: ModelRegistry.inMemory(authStorage) as never, + thinkLevel: "off", + senderIsOwner: true, + disableMessageTool: true, + disableTools: true, + abortSignal: abortController.signal, + onAgentEvent: (event) => stages.push(`agent_event:${event.stream}`), + }); + void attemptPromise.catch(() => undefined); + const result = await Promise.race([ + attemptPromise, + new Promise((_, reject) => { + setTimeout(() => { + abortController.abort("M9_A0_READINESS_TIMEOUT"); + reject( + new Error( + `LAYER2_READINESS_TIMEOUT stages=${stages.join(",")} providerRequests=${provider.requestBodies.length}`, + ), + ); + }, 5_000); + }), + ]); + + expect(result.promptError).toBeNull(); + expect(provider.requestBodies).toHaveLength(1); + expect(provider.requestBodies[0]).toMatchObject({ m9Trusted: "allowed" }); + const events = (await fs.readFile(eventsFile, "utf8")) + .trim() + .split("\n") + .filter(Boolean) + .map((line) => (JSON.parse(line) as { event: string }).event); + expect(events).toEqual(["model-handler"]); + } finally { + await provider.close(); + if (previousRequired === undefined) delete process.env.TIANGONG_TRUSTED_BOUNDARIES_REQUIRED; + else process.env.TIANGONG_TRUSTED_BOUNDARIES_REQUIRED = previousRequired; + if (previousEvents === undefined) delete process.env.M9_A0_EVENTS_FILE; + else process.env.M9_A0_EVENTS_FILE = previousEvents; + } + }, 30_000); +}); diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/source-contract-results.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/source-contract-results.txt new file mode 100644 index 0000000..37c8a5c --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/source-contract-results.txt @@ -0,0 +1,51 @@ +container=tiangong-m9a0-source-297597_20260823T011824Z +image=tg-worker:dev +image_id=sha256:ac1cb183e5b2c82982f6473fef86ccf128763a31192d711526d843a79edb69ff +image_created=2026-08-21T13:00:21.057325912+08:00 +openclaw_version=OpenClaw 2026.4.14 (2f35b6f) +patch_sha256=24671f2d21c9b6d2904e10f7e3874981983f12a6a9968cf7d8ff4001c6f93a1c +checking file src/agents/pi-embedded-runner/compact.ts +checking file src/agents/pi-embedded-runner/run/attempt.ts +checking file src/agents/pi-embedded-runner/trusted-boundaries.ts +checking file src/plugins/hook-types.ts +patching file src/agents/pi-embedded-runner/compact.ts +patching file src/agents/pi-embedded-runner/run/attempt.ts +patching file src/agents/pi-embedded-runner/trusted-boundaries.ts +patching file src/plugins/hook-types.ts +Checking formatting... + +All matched files use the correct format. +Finished in 53ms on 6 files using 16 threads. +No config found, using defaults. Please add a config file or try `oxfmt --init` if needed. + + RUN v4.1.4 /opt/openclaw + + ✓ src/agents/pi-embedded-runner/trusted-boundaries.provider.test.ts > M9-A0 actual pi-ai provider boundary > sends the trusted payload returned after native provider serialization 52ms + ✓ src/agents/pi-embedded-runner/trusted-boundaries.provider.test.ts > M9-A0 actual pi-ai provider boundary > does not issue a second HTTP request when tool capture fails 24ms + ✓ src/agents/pi-embedded-runner/trusted-boundaries.provider.test.ts > M9-A0 actual pi-ai provider boundary > makes zero HTTP requests when the trusted handler fails 1ms + ✓ src/agents/pi-embedded-runner/trusted-boundaries.test.ts > M9-A0 trusted boundaries > keeps stock behavior disabled but rejects an invalid activation value 2ms + ✓ src/agents/pi-embedded-runner/trusted-boundaries.test.ts > M9-A0 trusted boundaries > requires the exact loaded plugin and exactly one handler before installation 1ms + ✓ src/agents/pi-embedded-runner/trusted-boundaries.test.ts > M9-A0 trusted boundaries > runs after native payload transforms on every provider turn 7ms + ✓ src/agents/pi-embedded-runner/trusted-boundaries.test.ts > M9-A0 trusted boundaries > captures a normalized tool error before the follow-up provider turn 1ms + ✓ src/agents/pi-embedded-runner/trusted-boundaries.test.ts > M9-A0 trusted boundaries > keeps provider count at zero when the trusted model handler fails 0ms + ✓ src/agents/pi-embedded-runner/trusted-boundaries.test.ts > M9-A0 trusted boundaries > stops the turn before ToolResult emission when capture fails 1ms + ✓ src/agents/pi-embedded-runner/trusted-boundaries.test.ts > M9-A0 trusted boundaries > executes the handler-owned admission deadline and never invokes the tool 4ms + + Test Files 2 passed (2) + Tests 10 passed (10) + Start at 01:18:26 + Duration 2.17s (transform 1.46s, setup 0ms, import 2.31s, tests 95ms, environment 0ms) + + + RUN v4.1.4 /opt/openclaw + +···························································································································································· + + Test Files 4 passed (4) + Tests 156 passed (156) + Start at 01:18:29 + Duration 5.88s (transform 6.44s, setup 0ms, import 7.47s, tests 2.18s, environment 0ms) + +cleanup_owner=tiangong-m9a0-source-297597_20260823T011824Z +cleanup_container_absent=true +m9_a0_source_contract=pass diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/result.md b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/result.md new file mode 100644 index 0000000..0efdc6c --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/result.md @@ -0,0 +1,73 @@ +# M9-A0 trusted native-boundaries result + +## Decision + +**A0 is blocked at Layer 2.** Layer 1 passed. The real OpenClaw `runEmbeddedAttempt` path did not reach the local fake provider in the disposable `--network none` image, so no Layer 2 boundary pass is claimed and Layers 3–4 were not started. + +The candidate patch remains a research artifact. No Tiangong runtime file, OpenClaw installation, provider configuration, database, Matrix resource, or external service was changed. + +## Layer 1: source/type/agent-loop contract + +**PASS** via [`run-source-contract.sh`](run-source-contract.sh). + +- image: `tg-worker:dev` +- image ID: `sha256:ac1cb183e5b2c82982f6473fef86ccf128763a31192d711526d843a79edb69ff` +- OpenClaw: `2026.4.14 (2f35b6f)` +- network: `none` +- patch SHA-256: `24671f2d21c9b6d2904e10f7e3874981983f12a6a9968cf7d8ff4001c6f93a1c` +- boundary/provider tests: **10/10** +- adjacent OpenClaw regressions: **156/156** +- targeted typecheck, formatting, patch dry-run: passed +- cleanup: owned source-contract container absent + +Direct Layer 1 facts are preserved in [`evidence/source-contract-results.txt`](evidence/source-contract-results.txt). + +## Layer 2 diagnostic execution + +The disposable test loaded one `tiangong-control` plugin through the real OpenClaw plugin loader for each required hook, enabled `TIANGONG_TRUSTED_BOUNDARIES_REQUIRED=1`, created a loopback HTTP fake provider, and invoked the actual `runEmbeddedAttempt` source path. + +Readiness stages reached: + +```text +root_created +plugin_written +provider_listening +registry_loaded +hooks_verified +attempt_starting +``` + +Observed facts: + +- `runEmbeddedAttempt` logged run start, prompt build completion, and agent start; +- no trusted model-handler event and no fake-provider HTTP request occurred before the 5-second readiness deadline; +- abort/cleanup eventually ran, but the Vitest process retained pending attempt/transport work and exceeded the outer diagnostic budget; +- a direct real `pi-coding-agent` `AgentSession` with the same loopback fake provider and real in-memory `AuthStorage`/`ModelRegistry` made one request successfully; +- an unpatched `runEmbeddedAttempt` baseline also failed to reach the provider in the clean image. + +This classifies the failure as **readiness or adapter/host contract**, with high confidence. It is not proof that the trusted boundary passed or failed. + +Evidence: + +- [`evidence/layer2-results.txt`](evidence/layer2-results.txt) +- [`evidence/layer2-run-embedded-attempt.test.ts`](evidence/layer2-run-embedded-attempt.test.ts) +- [`evidence/layer2-run-embedded-attempt-baseline.test.ts`](evidence/layer2-run-embedded-attempt-baseline.test.ts) +- [`evidence/layer2-agent-session-readiness.test.ts`](evidence/layer2-agent-session-readiness.test.ts) +- [`evidence/artifact-sha256.txt`](evidence/artifact-sha256.txt) + +## Stopped work + +Because Layer 2 readiness did not pass, the following were not run: + +- actual post-tool provider turn; +- tool success/error/capture-failure cases; +- retry/follow-up path; +- OpenClaw-owned compaction session; +- Tiangong handler prototype; +- Basic Matrix turn. + +No third attempt was made to force a full Layer 2 run after the direct lower-level diagnostic and clean baseline comparison. The next action is to repair or replace the `runEmbeddedAttempt` readiness/transport harness, preserving the same pinned image and fake provider, then rerun Layer 2 from its first case. + +## Cleanup + +All ad-hoc Layer 2 containers were removed by their shell traps. The diagnostic commands did not retain the generated container names, so exact per-container identifiers are not available; this reporting limitation is recorded rather than converted into cleanup proof. No external resource or credential was created. From 992665ffdc84f3380811ccadc6e10978756bedd7 Mon Sep 17 00:00:00 2001 From: Jay Shen Date: Sun, 23 Aug 2026 10:46:19 +0800 Subject: [PATCH 05/13] test: repair M9-A0 Layer 2 readiness Signed-off-by: Jay Shen --- .../evidence/artifact-sha256.txt | 7 +- .../evidence/layer2-diagnostic-cleanup.txt | 8 + .../evidence/layer2-readiness-results.txt | 47 +++++ .../evidence/layer2-results.txt | 61 +++--- ...yer2-run-embedded-attempt-baseline.test.ts | 172 ++++++++++++---- .../layer2-run-embedded-attempt.test.ts | 186 ++++++++++++++---- .../plan.md | 5 +- .../result.md | 90 +++++---- .../run-layer2-readiness.sh | 69 +++++++ 9 files changed, 506 insertions(+), 139 deletions(-) create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-diagnostic-cleanup.txt create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-readiness-results.txt create mode 100755 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer2-readiness.sh diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/artifact-sha256.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/artifact-sha256.txt index b267d85..8834049 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/artifact-sha256.txt +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/artifact-sha256.txt @@ -1,5 +1,8 @@ 24671f2d21c9b6d2904e10f7e3874981983f12a6a9968cf7d8ff4001c6f93a1c smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/openclaw-2026.4.14-trusted-native-boundaries.patch -9aaf7859734ebe1d42249152c35eee8979273489233edad16a367766c0792570 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-run-embedded-attempt.test.ts +06a77efe011681fd7baea23bf3b993589ecf5268a40839dad8dd54916461ed1c smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-run-embedded-attempt.test.ts 0fce0d9bddbd72c4fd836b5dfc875188ed2accf91f0c242d545f68baa8ef0db4 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-agent-session-readiness.test.ts -f141ed86a379f6333521f3b63bab5300f8b1a25958e60ff3ec031866cb7009ba smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-run-embedded-attempt-baseline.test.ts +9c580e8435bb6aae227e9af6da51f6d7563670779adaeec29cd2867c6f911c07 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-run-embedded-attempt-baseline.test.ts b90b1e4a69cd959e2041658046ab7dabea7fdeb145b091acd0454460a50413c2 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/source-contract-results.txt +8f10eb85d47c608f7345a925487ba70bae3237c49f3453492c45b37ac5629293 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-readiness-results.txt +e10261404eae34700a32346176109cb41266bcfd86e1e643356340d8eb904ce6 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-diagnostic-cleanup.txt +c62db09a5ba6cb40a8a0c84f6e512dfa3e8a95f9fa9de4fb606ff681932bedfb smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer2-readiness.sh diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-diagnostic-cleanup.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-diagnostic-cleanup.txt new file mode 100644 index 0000000..f0bcd91 --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-diagnostic-cleanup.txt @@ -0,0 +1,8 @@ +container_absent[tiangong-m9a0-provider-diag-352362_20260823T022015Z]=true +container_absent[tiangong-m9a0-plugins-off-351963_20260823T021911Z]=true +container_absent[tiangong-m9a0-openai-diag-349977_20260823T021738Z]=true +container_absent[tiangong-m9a0-openai-349504_20260823T021645Z]=true +container_absent[tiangong-m9a0-diag-347936_20260823T021516Z]=true +container_absent[tiangong-m9a0-diag-347271_20260823T021339Z]=true +container_absent[tiangong-m9a0-diag-345061_20260823T021117Z]=true +m9_a0_owned_container_prefix_absent=true diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-readiness-results.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-readiness-results.txt new file mode 100644 index 0000000..5380bf6 --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-readiness-results.txt @@ -0,0 +1,47 @@ +container=tiangong-m9a0-layer2-374508_20260823T024326Z +image=tg-worker:dev +image_id=sha256:ac1cb183e5b2c82982f6473fef86ccf128763a31192d711526d843a79edb69ff +image_created=2026-08-21T13:00:21.057325912+08:00 +openclaw_version=OpenClaw 2026.4.14 (2f35b6f) +patch_sha256=24671f2d21c9b6d2904e10f7e3874981983f12a6a9968cf7d8ff4001c6f93a1c +Checking formatting... + +All matched files use the correct format. +Finished in 37ms on 1 files using 16 threads. +No config found, using defaults. Please add a config file or try `oxfmt --init` if needed. + + RUN  v4.1.4 /opt/openclaw + + ✓ src/agents/layer2-run-embedded-attempt-baseline.test.ts > M9-A0 unpatched runEmbeddedAttempt baseline > reaches a registered local provider after observable attempt readiness 31131ms + + Test Files  1 passed (1) + Tests  1 passed (1) + Start at  02:43:27 + Duration  36.46s (transform 3.69s, setup 0ms, import 5.26s, tests 31.13s, environment 0ms) + +checking file src/agents/pi-embedded-runner/compact.ts +checking file src/agents/pi-embedded-runner/run/attempt.ts +checking file src/agents/pi-embedded-runner/trusted-boundaries.ts +checking file src/plugins/hook-types.ts +patching file src/agents/pi-embedded-runner/compact.ts +patching file src/agents/pi-embedded-runner/run/attempt.ts +patching file src/agents/pi-embedded-runner/trusted-boundaries.ts +patching file src/plugins/hook-types.ts +Checking formatting... + +All matched files use the correct format. +Finished in 41ms on 1 files using 16 threads. +No config found, using defaults. Please add a config file or try `oxfmt --init` if needed. + + RUN  v4.1.4 /opt/openclaw + + ✓ src/agents/pi-embedded-runner/layer2-run-embedded-attempt.test.ts > M9-A0 Layer 2 actual runEmbeddedAttempt tool turn > gates the initial request, tool release, and post-tool request 74831ms + + Test Files  1 passed (1) + Tests  1 passed (1) + Start at  02:44:04 + Duration  80.14s (transform 3.68s, setup 0ms, import 5.24s, tests 74.83s, environment 0ms) + +cleanup_owner=tiangong-m9a0-layer2-374508_20260823T024326Z +cleanup_container_absent=true +layer2_readiness_exit=0 diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-results.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-results.txt index 5b79e2a..0178a42 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-results.txt +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-results.txt @@ -1,42 +1,51 @@ layer=2 -status=blocked +status=incomplete_but_readiness_repaired image=tg-worker:dev image_id=sha256:ac1cb183e5b2c82982f6473fef86ccf128763a31192d711526d843a79edb69ff openclaw=OpenClaw 2026.4.14 (2f35b6f) network=none activation=TIANGONG_TRUSTED_BOUNDARIES_REQUIRED=1 -plugin_loader=loaded_exactly_one_tiangong_control_handler_per_required_hook -plugin_manifest=required_openclaw.plugin.json_fixture_added provider=owned_loopback_http_fake_provider -layer1_source_contract=pass -layer1_boundary_tests=10/10 -layer1_adjacent_regressions=156/156 -layer1_cleanup_container_absent=true - +initial_failure_class=test_driver_plus_adapter_host_fixture +initial_failure_root_cause_1=fake provider model was not registered through the OpenClaw provider plugin contract +initial_failure_root_cause_2=five-second boundary timer included synchronous adapter-host startup and expired before observable attempt readiness lower_level_agent_session_readiness=pass lower_level_agent_session_provider_requests=1 -lower_level_agent_session_test=1/1 -patched_runEmbeddedAttempt_readiness=blocked -patched_runEmbeddedAttempt_readiness_stages=root_created,plugin_written,provider_listening,registry_loaded,hooks_verified,attempt_starting -patched_runEmbeddedAttempt_provider_requests=0 -patched_runEmbeddedAttempt_first_provider_or_handler_event=not_observed -patched_runEmbeddedAttempt_timeout_ms=5000 -patched_runEmbeddedAttempt_debug=embedded_run_start,prompt_build_completed,agent_start,then_no_provider_request_before_abort +repair_provider_manifest_declared=true +repair_provider_registered_through_plugin_api=true +repair_observable_readiness=before_prompt_build +repair_startup_and_provider_budgets_separate=true +repair_timeout_timer_cleanup=true -unpatched_runEmbeddedAttempt_baseline=also_blocked -unpatched_baseline_provider_requests=0 -unpatched_baseline_external_command_timeout=25s +unpatched_registered_provider_baseline=pass +unpatched_registered_provider_baseline_tests=1/1 +unpatched_registered_provider_requests=1 +patched_runEmbeddedAttempt_main_post_tool=pass +patched_runEmbeddedAttempt_tests=1/1 +patched_runEmbeddedAttempt_provider_requests=2 +patched_runEmbeddedAttempt_tool_executions=1 +patched_event_order=attempt-ready,model-handler,tool-executed,tool-handler,model-handler +patched_provider_payloads_from_trusted_handler=2/2 +selected_stream_strategy=boundary-aware:openai-completions -classification=readiness_or_adapter_host_contract -confidence=high -interpretation=real_pi_agent_session_can_reach_loopback_provider; the OpenClaw runEmbeddedAttempt orchestration path does not reach the provider in the disposable image, so this is not evidence that the trusted boundary passed or failed -layer2_tool_turns=not_started -layer2_retry_followup=not_started -layer2_compaction_session=not_started +layer1_source_contract=pass +layer1_boundary_tests=10/10 +layer1_adjacent_regressions=156/156 +layer1_cleanup_container_absent=true + +layer2_tool_error=not_started_in_repaired_host_harness +layer2_capture_failure=not_started_in_repaired_host_harness +layer2_retry_followup=not_started_in_repaired_host_harness +layer2_compaction_session=not_started_in_repaired_host_harness layer3_tiangong_handler=not_started layer4_matrix=not_authorized +formal_m9_a_implementation=blocked_by_incomplete_spike + +final_runner_container=tiangong-m9a0-layer2-374508_20260823T024326Z +final_runner_cleanup_container_absent=true +diagnostic_cleanup_reconciled=true +m9_a0_owned_container_prefix_absent=true external_resources_created=none -credentials_or_provider_config=none -cleanup=all ad-hoc containers were removed by shell traps; exact generated names were not retained by the diagnostic commands +credentials_or_durable_provider_config=none diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-run-embedded-attempt-baseline.test.ts b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-run-embedded-attempt-baseline.test.ts index a12dc3c..c48a2ad 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-run-embedded-attempt-baseline.test.ts +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-run-embedded-attempt-baseline.test.ts @@ -5,10 +5,30 @@ import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent"; import type { Model } from "@mariozechner/pi-ai"; +import { clearPluginLoaderCache, loadOpenClawPlugins } from "../plugins/loader.js"; +import { resetGlobalHookRunner } from "../plugins/hook-runner-global.js"; import { runEmbeddedAttempt } from "./pi-embedded-runner/run/attempt.js"; const servers: http.Server[] = []; const roots: string[] = []; +const ATTEMPT_STARTUP_TIMEOUT_MS = 45_000; +const PROVIDER_RESPONSE_TIMEOUT_MS = 5_000; + +async function withTimeout(params: { + promise: Promise; + timeoutMs: number; + onTimeout: () => Error; +}): Promise { + let timer: NodeJS.Timeout | undefined; + const timeout = new Promise((_, reject) => { + timer = setTimeout(() => reject(params.onTimeout()), params.timeoutMs); + }); + try { + return await Promise.race([params.promise, timeout]); + } finally { + if (timer) clearTimeout(timer); + } +} function fakeModel(baseUrl: string): Model<"openai-completions"> { return { @@ -25,7 +45,40 @@ function fakeModel(baseUrl: string): Model<"openai-completions"> { }; } +async function writeProviderPlugin(root: string): Promise { + const plugin = path.join(root, "m9-a0-fake-provider.cjs"); + await Promise.all([ + fs.writeFile( + path.join(root, "openclaw.plugin.json"), + JSON.stringify({ + id: "m9-a0-fake-provider", + providers: ["fake-provider"], + configSchema: { type: "object", properties: {} }, + }), + "utf8", + ), + fs.writeFile( + plugin, + `module.exports = { + id: "m9-a0-fake-provider", + register(api) { + api.registerProvider({ id: "fake-provider", label: "M9 A0 fake provider", auth: [] }); + api.on("before_prompt_build", async () => { + const readyEvent = process.env.M9_A0_ATTEMPT_READY_EVENT; + if (readyEvent) process.emit(readyEvent); + }); + }, +}; +`, + "utf8", + ), + ]); + return plugin; +} + afterEach(async () => { + resetGlobalHookRunner(); + clearPluginLoaderCache(); for (const server of servers.splice(0)) { if (server.listening) await new Promise((resolve) => server.close(() => resolve())); } @@ -33,9 +86,24 @@ afterEach(async () => { }); describe("M9-A0 unpatched runEmbeddedAttempt baseline", () => { - it("reaches a local provider request with no Tiangong activation", async () => { + it("reaches a registered local provider after observable attempt readiness", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "m9-a0-baseline-")); roots.push(root); + const pluginFile = await writeProviderPlugin(root); + const config = { + plugins: { + allow: ["m9-a0-fake-provider"], + load: { paths: [pluginFile] }, + }, + }; + const registry = loadOpenClawPlugins({ cache: false, workspaceDir: root, config }); + expect(registry.plugins.find((plugin) => plugin.id === "m9-a0-fake-provider")?.status).toBe( + "loaded", + ); + expect( + registry.providers.filter((entry) => entry.provider.id === "fake-provider"), + ).toHaveLength(1); + let requestCount = 0; const server = http.createServer((_request, response) => { requestCount += 1; @@ -52,44 +120,74 @@ describe("M9-A0 unpatched runEmbeddedAttempt baseline", () => { server.listen(0, "127.0.0.1", resolve); }); const address = server.address(); - if (!address || typeof address === "string") + if (!address || typeof address === "string") { throw new Error("BASELINE_PROVIDER_ADDRESS_MISSING"); + } + + const previousReadyEvent = process.env.M9_A0_ATTEMPT_READY_EVENT; + const readyEvent = `m9-a0-baseline-ready:${process.pid}:${path.basename(root)}`; + let markAttemptReady: () => void = () => undefined; + const attemptReady = new Promise((resolve) => { + markAttemptReady = resolve; + }); + process.once(readyEvent, markAttemptReady); + process.env.M9_A0_ATTEMPT_READY_EVENT = readyEvent; const authStorage = AuthStorage.inMemory(); authStorage.setRuntimeApiKey("fake-provider", "fixture-provider-value"); const abortController = new AbortController(); - const attempt = runEmbeddedAttempt({ - sessionId: "baseline-session", - sessionKey: "agent:baseline:main", - sessionFile: path.join(root, "session.jsonl"), - workspaceDir: root, - agentDir: root, - config: {}, - prompt: "baseline", - timeoutMs: 10_000, - runId: "baseline-run", - provider: "fake-provider", - modelId: "fake-model", - model: fakeModel(`http://127.0.0.1:${address.port}/v1`), - resolvedApiKey: "fixture-provider-value", - authStorage: authStorage as never, - modelRegistry: ModelRegistry.inMemory(authStorage) as never, - thinkLevel: "off", - senderIsOwner: true, - disableMessageTool: true, - disableTools: true, - abortSignal: abortController.signal, - }); - void attempt.catch(() => undefined); - const result = await Promise.race([ - attempt, - new Promise((_, reject) => - setTimeout(() => { - abortController.abort("BASELINE_READINESS_TIMEOUT"); - reject(new Error(`BASELINE_READINESS_TIMEOUT providerRequests=${requestCount}`)); - }, 5_000), - ), - ]); - expect(result.promptError).toBeNull(); - expect(requestCount).toBe(1); - }, 30_000); + try { + const attempt = runEmbeddedAttempt({ + sessionId: "baseline-session", + sessionKey: "agent:baseline:main", + sessionFile: path.join(root, "session.jsonl"), + workspaceDir: root, + agentDir: root, + config, + prompt: "baseline", + timeoutMs: 10_000, + runId: "baseline-run", + provider: "fake-provider", + modelId: "fake-model", + model: fakeModel(`http://127.0.0.1:${address.port}/v1`), + resolvedApiKey: "fixture-provider-value", + authStorage: authStorage as never, + modelRegistry: ModelRegistry.inMemory(authStorage) as never, + thinkLevel: "off", + senderIsOwner: true, + disableMessageTool: true, + disableTools: true, + abortSignal: abortController.signal, + }); + void attempt.catch(() => undefined); + await withTimeout({ + promise: Promise.race([ + attemptReady, + attempt.then((earlyResult) => { + throw new Error( + `BASELINE_COMPLETED_BEFORE_READINESS promptError=${earlyResult.promptError ? "present" : "none"}`, + ); + }), + ]), + timeoutMs: ATTEMPT_STARTUP_TIMEOUT_MS, + onTimeout: () => { + abortController.abort("M9_A0_BASELINE_STARTUP_TIMEOUT"); + return new Error(`BASELINE_STARTUP_TIMEOUT providerRequests=${requestCount}`); + }, + }); + const result = await withTimeout({ + promise: attempt, + timeoutMs: PROVIDER_RESPONSE_TIMEOUT_MS, + onTimeout: () => { + abortController.abort("M9_A0_BASELINE_PROVIDER_TIMEOUT"); + return new Error(`BASELINE_PROVIDER_TIMEOUT providerRequests=${requestCount}`); + }, + }); + expect(result.promptError).toBeNull(); + expect(requestCount).toBe(1); + } finally { + process.off(readyEvent, markAttemptReady); + if (previousReadyEvent === undefined) delete process.env.M9_A0_ATTEMPT_READY_EVENT; + else process.env.M9_A0_ATTEMPT_READY_EVENT = previousReadyEvent; + } + }, 70_000); }); diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-run-embedded-attempt.test.ts b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-run-embedded-attempt.test.ts index bbbc817..1e349b0 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-run-embedded-attempt.test.ts +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-run-embedded-attempt.test.ts @@ -14,6 +14,24 @@ import type { Model } from "@mariozechner/pi-ai"; const tempRoots: string[] = []; const servers: http.Server[] = []; +const ATTEMPT_STARTUP_TIMEOUT_MS = 120_000; +const PROVIDER_RESPONSE_TIMEOUT_MS = 5_000; + +async function withTimeout(params: { + promise: Promise; + timeoutMs: number; + onTimeout: () => Error; +}): Promise { + let timer: NodeJS.Timeout | undefined; + const timeout = new Promise((_, reject) => { + timer = setTimeout(() => reject(params.onTimeout()), params.timeoutMs); + }); + try { + return await Promise.race([params.promise, timeout]); + } finally { + if (timer) clearTimeout(timer); + } +} async function makeRoot(prefix: string): Promise { const root = await fs.mkdtemp(path.join(os.tmpdir(), prefix)); @@ -41,24 +59,61 @@ async function listenFakeProvider(): Promise<{ connection: "keep-alive", "cache-control": "no-cache", }); - response.write( - `data: ${JSON.stringify({ - id: "m9-a0-layer2", - object: "chat.completion.chunk", - created: 1, - model: "fake-model", - choices: [{ index: 0, delta: { role: "assistant", content: "ok" }, finish_reason: null }], - })}\n\n`, - ); - response.write( - `data: ${JSON.stringify({ - id: "m9-a0-layer2", - object: "chat.completion.chunk", - created: 1, - model: "fake-model", - choices: [{ index: 0, delta: {}, finish_reason: "stop" }], - })}\n\n`, - ); + if (requestBodies.length === 1) { + response.write( + `data: ${JSON.stringify({ + id: "m9-a0-layer2-tool", + object: "chat.completion.chunk", + created: 1, + model: "fake-model", + choices: [ + { + index: 0, + delta: { + role: "assistant", + tool_calls: [ + { + index: 0, + id: "call-layer2-1", + type: "function", + function: { name: "synthetic", arguments: "{}" }, + }, + ], + }, + finish_reason: null, + }, + ], + })}\n\n`, + ); + response.write( + `data: ${JSON.stringify({ + id: "m9-a0-layer2-tool", + object: "chat.completion.chunk", + created: 1, + model: "fake-model", + choices: [{ index: 0, delta: {}, finish_reason: "tool_calls" }], + })}\n\n`, + ); + } else { + response.write( + `data: ${JSON.stringify({ + id: "m9-a0-layer2", + object: "chat.completion.chunk", + created: 1, + model: "fake-model", + choices: [{ index: 0, delta: { role: "assistant", content: "ok" }, finish_reason: null }], + })}\n\n`, + ); + response.write( + `data: ${JSON.stringify({ + id: "m9-a0-layer2", + object: "chat.completion.chunk", + created: 1, + model: "fake-model", + choices: [{ index: 0, delta: {}, finish_reason: "stop" }], + })}\n\n`, + ); + } response.end("data: [DONE]\n\n"); }); servers.push(server); @@ -101,7 +156,11 @@ function writePlugin(root: string): Promise { return Promise.all([ fs.writeFile( manifest, - JSON.stringify({ id: "tiangong-control", configSchema: { type: "object", properties: {} } }), + JSON.stringify({ + id: "tiangong-control", + providers: ["fake-provider"], + configSchema: { type: "object", properties: {} }, + }), "utf8", ), fs.writeFile( @@ -113,6 +172,22 @@ function emit(event, fields) { module.exports = { id: "tiangong-control", register(api) { + api.registerProvider({ id: "fake-provider", label: "M9 A0 fake provider", auth: [] }); + api.registerTool({ + name: "synthetic", + label: "synthetic", + description: "M9 A0 synthetic tool", + parameters: { type: "object", properties: {}, additionalProperties: false }, + execute: async () => { + emit("tool-executed", {}); + return { content: [{ type: "text", text: "synthetic-ok" }], details: { status: "ok" } }; + }, + }); + api.on("before_prompt_build", async () => { + emit("attempt-ready", {}); + const readyEvent = process.env.M9_A0_ATTEMPT_READY_EVENT; + if (readyEvent) process.emit(readyEvent); + }); api.on("before_model_call", async (event) => { emit("model-handler", { provider: event.provider, model: event.model }); return { allow: true, payload: { ...event.payload, m9Trusted: "allowed" } }; @@ -138,8 +213,8 @@ afterEach(async () => { while (tempRoots.length > 0) await fs.rm(tempRoots.pop()!, { recursive: true, force: true }); }); -describe("M9-A0 Layer 2 actual runEmbeddedAttempt readiness", () => { - it("loads the trusted plugin through OpenClaw and gates one real provider request", async () => { +describe("M9-A0 Layer 2 actual runEmbeddedAttempt tool turn", () => { + it("gates the initial request, tool release, and post-tool request", async () => { const root = await makeRoot("m9-a0-layer2-"); const stages: string[] = ["root_created"]; const eventsFile = path.join(root, "events.ndjson"); @@ -149,8 +224,16 @@ describe("M9-A0 Layer 2 actual runEmbeddedAttempt readiness", () => { stages.push("provider_listening"); const previousRequired = process.env.TIANGONG_TRUSTED_BOUNDARIES_REQUIRED; const previousEvents = process.env.M9_A0_EVENTS_FILE; + const previousReadyEvent = process.env.M9_A0_ATTEMPT_READY_EVENT; + const readyEvent = `m9-a0-attempt-ready:${process.pid}:${path.basename(root)}`; + let markAttemptReady: () => void = () => undefined; + const attemptReady = new Promise((resolve) => { + markAttemptReady = resolve; + }); + process.once(readyEvent, markAttemptReady); process.env.TIANGONG_TRUSTED_BOUNDARIES_REQUIRED = "1"; process.env.M9_A0_EVENTS_FILE = eventsFile; + process.env.M9_A0_ATTEMPT_READY_EVENT = readyEvent; try { const registry = loadOpenClawPlugins({ cache: false, @@ -175,6 +258,9 @@ describe("M9-A0 Layer 2 actual runEmbeddedAttempt readiness", () => { })}`, ); } + expect( + registry.providers.filter((entry) => entry.provider.id === "fake-provider"), + ).toHaveLength(1); expect( registry.typedHooks.filter((hook) => hook.hookName === "before_model_call"), ).toHaveLength(1); @@ -214,40 +300,66 @@ describe("M9-A0 Layer 2 actual runEmbeddedAttempt readiness", () => { thinkLevel: "off", senderIsOwner: true, disableMessageTool: true, - disableTools: true, + disableTools: false, + toolsAllow: ["synthetic"], abortSignal: abortController.signal, onAgentEvent: (event) => stages.push(`agent_event:${event.stream}`), }); void attemptPromise.catch(() => undefined); - const result = await Promise.race([ - attemptPromise, - new Promise((_, reject) => { - setTimeout(() => { - abortController.abort("M9_A0_READINESS_TIMEOUT"); - reject( - new Error( - `LAYER2_READINESS_TIMEOUT stages=${stages.join(",")} providerRequests=${provider.requestBodies.length}`, - ), + await withTimeout({ + promise: Promise.race([ + attemptReady, + attemptPromise.then((earlyResult) => { + throw new Error( + `ATTEMPT_COMPLETED_BEFORE_READINESS promptError=${earlyResult.promptError ? "present" : "none"}`, ); - }, 5_000); - }), - ]); + }), + ]), + timeoutMs: ATTEMPT_STARTUP_TIMEOUT_MS, + onTimeout: () => { + abortController.abort("M9_A0_STARTUP_TIMEOUT"); + return new Error( + `LAYER2_STARTUP_TIMEOUT stages=${stages.join(",")} providerRequests=${provider.requestBodies.length}`, + ); + }, + }); + stages.push("attempt_ready"); + const result = await withTimeout({ + promise: attemptPromise, + timeoutMs: PROVIDER_RESPONSE_TIMEOUT_MS, + onTimeout: () => { + abortController.abort("M9_A0_PROVIDER_RESPONSE_TIMEOUT"); + return new Error( + `LAYER2_PROVIDER_RESPONSE_TIMEOUT stages=${stages.join(",")} providerRequests=${provider.requestBodies.length}`, + ); + }, + }); expect(result.promptError).toBeNull(); - expect(provider.requestBodies).toHaveLength(1); + expect(provider.requestBodies).toHaveLength(2); expect(provider.requestBodies[0]).toMatchObject({ m9Trusted: "allowed" }); + expect(provider.requestBodies[1]).toMatchObject({ m9Trusted: "allowed" }); const events = (await fs.readFile(eventsFile, "utf8")) .trim() .split("\n") .filter(Boolean) .map((line) => (JSON.parse(line) as { event: string }).event); - expect(events).toEqual(["model-handler"]); + expect(events).toEqual([ + "attempt-ready", + "model-handler", + "tool-executed", + "tool-handler", + "model-handler", + ]); } finally { + process.off(readyEvent, markAttemptReady); await provider.close(); if (previousRequired === undefined) delete process.env.TIANGONG_TRUSTED_BOUNDARIES_REQUIRED; else process.env.TIANGONG_TRUSTED_BOUNDARIES_REQUIRED = previousRequired; if (previousEvents === undefined) delete process.env.M9_A0_EVENTS_FILE; else process.env.M9_A0_EVENTS_FILE = previousEvents; + if (previousReadyEvent === undefined) delete process.env.M9_A0_ATTEMPT_READY_EVENT; + else process.env.M9_A0_ATTEMPT_READY_EVENT = previousReadyEvent; } - }, 30_000); + }, 180_000); }); diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/plan.md b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/plan.md index dd998e0..48d0241 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/plan.md +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/plan.md @@ -1,6 +1,6 @@ # M9-A0 trusted native-boundary follow-up -> Status: ready for independent execution; no A0 pass result exists yet. +> Status: in progress; Layer 1 and the repaired Layer 2 main/post-tool path pass, while the remaining Layer 2 cases and Layer 3 are not complete. ## Scope @@ -87,9 +87,10 @@ Proceed only after Layer 1 passes. - Set `TIANGONG_TRUSTED_BOUNDARIES_REQUIRED=1` in the disposable control process. - Load exactly one test-only `tiangong-control` handler for each new hook through the real OpenClaw plugin loader. - Use the actual `runEmbeddedAttempt`/gateway/session path and an owned loopback fake provider; do not manually increment a provider counter based on a hook return. +- Register the fake provider through a test-only plugin manifest and `registerProvider`; an ad hoc `Model` with an unknown provider ID does not satisfy OpenClaw's adapter-host contract. - Record the selected stream strategy and prove that its implementation awaits `onPayload` before its first socket write. - Exercise the main turn, post-tool turn, retry/follow-up path, and OpenClaw-owned compaction session. -- Replace the predecessor's timed-out context-engine harness with an observable readiness condition. A timeout before the first readiness event is red and is not provider-boundary evidence. +- Use the test-only `before_prompt_build` event as the observable attempt-ready condition. Keep the bounded host-startup budget separate from the five-second provider-response budget, and clear both timers after their race. A timeout before readiness is red but is not provider-boundary evidence. ### Layer 3: Tiangong control-handler prototype diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/result.md b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/result.md index 0efdc6c..fbdae1c 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/result.md +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/result.md @@ -2,9 +2,11 @@ ## Decision -**A0 is blocked at Layer 2.** Layer 1 passed. The real OpenClaw `runEmbeddedAttempt` path did not reach the local fake provider in the disposable `--network none` image, so no Layer 2 boundary pass is claimed and Layers 3–4 were not started. +**A0 remains incomplete at Layer 2, but the Layer 2 readiness/adapter-host blocker is repaired.** Layer 1 still passes. The repaired real OpenClaw `runEmbeddedAttempt` harness now passes both an unpatched adjacent baseline and the patched initial/tool/post-tool provider path in the disposable `--network none` image. -The candidate patch remains a research artifact. No Tiangong runtime file, OpenClaw installation, provider configuration, database, Matrix resource, or external service was changed. +The remaining Layer 2 error/capture-failure, retry/follow-up, and OpenClaw-owned compaction cases have not run through this repaired host harness. Layer 3 therefore remains blocked, and Layer 4 remains unauthorized. + +The candidate patch remains a research artifact. No Tiangong runtime file, installed OpenClaw tree, durable provider configuration, database, Matrix resource, or external service was changed. ## Layer 1: source/type/agent-loop contract @@ -22,52 +24,70 @@ The candidate patch remains a research artifact. No Tiangong runtime file, OpenC Direct Layer 1 facts are preserved in [`evidence/source-contract-results.txt`](evidence/source-contract-results.txt). -## Layer 2 diagnostic execution +## Initial Layer 2 failure and diagnosis -The disposable test loaded one `tiangong-control` plugin through the real OpenClaw plugin loader for each required hook, enabled `TIANGONG_TRUSTED_BOUNDARIES_REQUIRED=1`, created a loopback HTTP fake provider, and invoked the actual `runEmbeddedAttempt` source path. +The original harness passed an ad hoc `Model` whose `fake-provider` ID was not registered as an OpenClaw provider. `runEmbeddedAttempt` consequently entered OpenClaw's provider-plugin resolution path before the model request. The direct `AgentSession` diagnostic did not exercise that host contract, which explains why it reached loopback while patched and unpatched attempts did not. -Readiness stages reached: +The original five-second timer also started before adapter-host initialization. Synchronous plugin/provider initialization delayed the event loop, so the expired timer could abort the attempt as soon as prompt processing became possible. That timer measured host startup rather than the trusted provider boundary. -```text -root_created -plugin_written -provider_listening -registry_loaded -hooks_verified -attempt_starting -``` +This classifies the initial failure as **test driver plus adapter/host fixture**, with high confidence. It was not a candidate-patch failure. -Observed facts: +## Repair -- `runEmbeddedAttempt` logged run start, prompt build completion, and agent start; -- no trusted model-handler event and no fake-provider HTTP request occurred before the 5-second readiness deadline; -- abort/cleanup eventually ran, but the Vitest process retained pending attempt/transport work and exceeded the outer diagnostic budget; -- a direct real `pi-coding-agent` `AgentSession` with the same loopback fake provider and real in-memory `AuthStorage`/`ModelRegistry` made one request successfully; -- an unpatched `runEmbeddedAttempt` baseline also failed to reach the provider in the clean image. +The repaired harness: -This classifies the failure as **readiness or adapter/host contract**, with high confidence. It is not proof that the trusted boundary passed or failed. +1. declares `fake-provider` in the test plugin manifest and registers it through the real plugin API; +2. loads the plugin through the real OpenClaw loader; +3. uses a test-only `before_prompt_build` event as the observable attempt-ready fact; +4. separates a bounded host-startup budget from the five-second provider-response budget; +5. clears race timers and process listeners during teardown; +6. runs in one exactly named container and proves its removal. -Evidence: +The first invocation of the new runner stopped at its formatting gate before tests. Only formatting changed before the successful rerun. + +## Repaired Layer 2 observations + +**PASS for the executed main and post-tool cells** via [`run-layer2-readiness.sh`](run-layer2-readiness.sh). + +- final owned container: `tiangong-m9a0-layer2-374508_20260823T024326Z` +- image, image ID, OpenClaw version, patch digest, and network mode: unchanged from Layer 1 +- unpatched registered-provider baseline: **1/1 passed** +- patched actual `runEmbeddedAttempt` tool turn: **1/1 passed** +- selected path: boundary-aware `openai-completions` HTTP transport to the owned loopback provider +- provider requests in patched case: **2** +- synthetic tool executions: **1** +- trusted event order asserted by the test: + +```text +attempt-ready +model-handler +tool-executed +tool-handler +model-handler +``` + +Both provider request bodies carried the payload marker returned by the trusted model handler. The second request occurred only after the normalized tool outcome passed the trusted release handler. This proves the initial request, successful tool release, and post-tool request on the actual patched attempt path. + +Direct output is in [`evidence/layer2-readiness-results.txt`](evidence/layer2-readiness-results.txt). The repaired tests are: -- [`evidence/layer2-results.txt`](evidence/layer2-results.txt) -- [`evidence/layer2-run-embedded-attempt.test.ts`](evidence/layer2-run-embedded-attempt.test.ts) - [`evidence/layer2-run-embedded-attempt-baseline.test.ts`](evidence/layer2-run-embedded-attempt-baseline.test.ts) -- [`evidence/layer2-agent-session-readiness.test.ts`](evidence/layer2-agent-session-readiness.test.ts) -- [`evidence/artifact-sha256.txt`](evidence/artifact-sha256.txt) +- [`evidence/layer2-run-embedded-attempt.test.ts`](evidence/layer2-run-embedded-attempt.test.ts) -## Stopped work +## Remaining work -Because Layer 2 readiness did not pass, the following were not run: +Layer 2 is not yet complete. Do not claim an A0 pass until the repaired harness also covers: -- actual post-tool provider turn; -- tool success/error/capture-failure cases; -- retry/follow-up path; -- OpenClaw-owned compaction session; -- Tiangong handler prototype; -- Basic Matrix turn. +- normalized tool error; +- trusted capture failure with no ToolResult release or next provider request; +- retry or persisted follow-up path; +- separately created OpenClaw compaction session through the same model boundary. -No third attempt was made to force a full Layer 2 run after the direct lower-level diagnostic and clean baseline comparison. The next action is to repair or replace the `runEmbeddedAttempt` readiness/transport harness, preserving the same pinned image and fake provider, then rerun Layer 2 from its first case. +Layer 3 and Matrix were not started. M9-A formal implementation remains blocked by the incomplete spike, not by the former readiness failure. ## Cleanup -All ad-hoc Layer 2 containers were removed by their shell traps. The diagnostic commands did not retain the generated container names, so exact per-container identifiers are not available; this reporting limitation is recorded rather than converted into cleanup proof. No external resource or credential was created. +The final runner removed its exact owned container and reported `cleanup_container_absent=true`. + +Several early diagnostic commands were externally timed out before their shell traps completed, leaving seven exited, exactly named diagnostic containers. They were discovered by explicit inspection, removed by exact name, and rechecked individually. [`evidence/layer2-diagnostic-cleanup.txt`](evidence/layer2-diagnostic-cleanup.txt) records each absence and confirms that no container with the owned `tiangong-m9a0-` prefix remains. + +No external resource or credential was created. diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer2-readiness.sh b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer2-readiness.sh new file mode 100755 index 0000000..8adcdd3 --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer2-readiness.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +set -euo pipefail + +RUN_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +IMAGE="${M9_A0_IMAGE:-tg-worker:dev}" +PATCH="$RUN_DIR/evidence/openclaw-2026.4.14-trusted-native-boundaries.patch" +EXPECTED_PATCH_SHA256="24671f2d21c9b6d2904e10f7e3874981983f12a6a9968cf7d8ff4001c6f93a1c" +EXPECTED_VERSION="OpenClaw 2026.4.14 (2f35b6f)" +CONTAINER="tiangong-m9a0-layer2-$$_$(date -u +%Y%m%dT%H%M%SZ)" + +cleanup() { + if docker container inspect "$CONTAINER" >/dev/null 2>&1; then + docker rm -f "$CONTAINER" >/dev/null + fi +} +trap cleanup EXIT INT TERM + +actual_patch_sha256="$(sha256sum "$PATCH" | awk '{print $1}')" +if [[ "$actual_patch_sha256" != "$EXPECTED_PATCH_SHA256" ]]; then + printf 'patch_sha256_mismatch expected=%s actual=%s\n' \ + "$EXPECTED_PATCH_SHA256" "$actual_patch_sha256" >&2 + exit 1 +fi + +image_id="$(docker image inspect --format '{{.Id}}' "$IMAGE")" +image_created="$(docker image inspect --format '{{.Created}}' "$IMAGE")" +version="$(docker run --rm --network none --entrypoint openclaw "$IMAGE" --version)" +if [[ "$version" != "$EXPECTED_VERSION" ]]; then + printf 'openclaw_version_mismatch expected=%q actual=%q\n' "$EXPECTED_VERSION" "$version" >&2 + exit 1 +fi + +printf 'container=%s\nimage=%s\nimage_id=%s\nimage_created=%s\nopenclaw_version=%s\npatch_sha256=%s\n' \ + "$CONTAINER" "$IMAGE" "$image_id" "$image_created" "$version" "$actual_patch_sha256" + +status=0 +docker run --name "$CONTAINER" --network none \ + --mount "type=bind,src=$RUN_DIR,dst=/m9-a0,readonly" \ + --entrypoint sh "$IMAGE" -lc ' + set -eu + cd /opt/openclaw + + cp /m9-a0/evidence/layer2-run-embedded-attempt-baseline.test.ts \ + src/agents/layer2-run-embedded-attempt-baseline.test.ts + ./node_modules/.bin/oxfmt --check \ + src/agents/layer2-run-embedded-attempt-baseline.test.ts + OPENCLAW_LOG_LEVEL=warn ./node_modules/.bin/vitest run \ + src/agents/layer2-run-embedded-attempt-baseline.test.ts \ + --reporter=verbose --hookTimeout=20000 + + patch -p1 --dry-run < /m9-a0/evidence/openclaw-2026.4.14-trusted-native-boundaries.patch + patch -p1 < /m9-a0/evidence/openclaw-2026.4.14-trusted-native-boundaries.patch + cp /m9-a0/evidence/layer2-run-embedded-attempt.test.ts \ + src/agents/pi-embedded-runner/layer2-run-embedded-attempt.test.ts + ./node_modules/.bin/oxfmt --check \ + src/agents/pi-embedded-runner/layer2-run-embedded-attempt.test.ts + OPENCLAW_LOG_LEVEL=warn ./node_modules/.bin/vitest run \ + src/agents/pi-embedded-runner/layer2-run-embedded-attempt.test.ts \ + --reporter=verbose --hookTimeout=20000 + ' || status=$? + +cleanup +if docker container inspect "$CONTAINER" >/dev/null 2>&1; then + printf 'cleanup_container_still_present=%s\n' "$CONTAINER" >&2 + exit 1 +fi +printf 'cleanup_owner=%s\ncleanup_container_absent=true\nlayer2_readiness_exit=%s\n' \ + "$CONTAINER" "$status" +exit "$status" From 8bb697a9a13270a020379c94fadba9fb72ff5808 Mon Sep 17 00:00:00 2001 From: Jay Shen Date: Sun, 23 Aug 2026 11:43:20 +0800 Subject: [PATCH 06/13] test: complete M9-A0 Layer 2 evidence Signed-off-by: Jay Shen --- .../evidence/artifact-sha256.txt | 19 +- .../evidence/layer2-capture-failure.test.ts | 373 ++++++++++++++++++ .../evidence/layer2-compaction.test.ts | 233 +++++++++++ .../evidence/layer2-followup.test.ts | 287 ++++++++++++++ .../evidence/layer2-readiness-results.txt | 36 +- .../evidence/layer2-remaining-results.txt | 76 ++++ .../evidence/layer2-tool-error.test.ts | 373 ++++++++++++++++++ ...-2026.4.14-trusted-native-boundaries.patch | 5 +- .../evidence/source-contract-results.txt | 20 +- .../plan.md | 6 +- .../result.md | 128 +++--- .../run-layer2-readiness.sh | 2 +- .../run-source-contract.sh | 2 +- 13 files changed, 1467 insertions(+), 93 deletions(-) create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-capture-failure.test.ts create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-compaction.test.ts create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-followup.test.ts create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-remaining-results.txt create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-tool-error.test.ts diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/artifact-sha256.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/artifact-sha256.txt index 8834049..73c02bc 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/artifact-sha256.txt +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/artifact-sha256.txt @@ -1,8 +1,15 @@ -24671f2d21c9b6d2904e10f7e3874981983f12a6a9968cf7d8ff4001c6f93a1c smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/openclaw-2026.4.14-trusted-native-boundaries.patch -06a77efe011681fd7baea23bf3b993589ecf5268a40839dad8dd54916461ed1c smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-run-embedded-attempt.test.ts 0fce0d9bddbd72c4fd836b5dfc875188ed2accf91f0c242d545f68baa8ef0db4 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-agent-session-readiness.test.ts -9c580e8435bb6aae227e9af6da51f6d7563670779adaeec29cd2867c6f911c07 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-run-embedded-attempt-baseline.test.ts -b90b1e4a69cd959e2041658046ab7dabea7fdeb145b091acd0454460a50413c2 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/source-contract-results.txt -8f10eb85d47c608f7345a925487ba70bae3237c49f3453492c45b37ac5629293 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-readiness-results.txt +6c6fda442f7f73d00442b837434fef2498da4f33622e3542c86a7dd174482d27 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-capture-failure.test.ts +964cda358583d9d33a3ac30b9b7a68d5bc14dfd3dd56ae79c6bbec0f205783ae smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-compaction.test.ts e10261404eae34700a32346176109cb41266bcfd86e1e643356340d8eb904ce6 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-diagnostic-cleanup.txt -c62db09a5ba6cb40a8a0c84f6e512dfa3e8a95f9fa9de4fb606ff681932bedfb smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer2-readiness.sh +8665a959aa74c809bc624720a042ffb9b77a1a9e3644afb8ecc806342d78dbbb smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-followup.test.ts +cec18acc562452d9ca8b6568646a89872a84e195ce0081361eaa93786f6f23a6 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-readiness-results.txt +89d904dd891687c94c7b78297d9c3063e2faaf7f8595647f10e540510a0545e9 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-remaining-results.txt +4a9e36e2dbbf657bf7d2c1cf1618d88db7fa2f3e7ee4cc8f6fe8ac405bb7e071 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-results.txt +9c580e8435bb6aae227e9af6da51f6d7563670779adaeec29cd2867c6f911c07 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-run-embedded-attempt-baseline.test.ts +06a77efe011681fd7baea23bf3b993589ecf5268a40839dad8dd54916461ed1c smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-run-embedded-attempt.test.ts +2b4090ada287bd20eb3b6ea45365ee065a023f72b35f2701da8060e4350efcb3 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-tool-error.test.ts +eb79f2c2c296f528ec93f1ce38581780b4d17afd566ba10fd3f07333169dd34d smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/openclaw-2026.4.14-trusted-native-boundaries.patch +52fcc896751cbbda796e3c7a78a6b5abf3f81748c4d4a37f4c434040016e7376 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/source-contract-results.txt +dcbff559f4cdf28bc2118de22edbbd5f228dabfe7e69afeae1a2354cb2480ed5 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/trusted-boundaries.provider.test.ts +d6d9ff6355162a4a5e9e345010c04aa189738a487d23b949881ed30e72596548 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/trusted-boundaries.test.ts diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-capture-failure.test.ts b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-capture-failure.test.ts new file mode 100644 index 0000000..d7672e0 --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-capture-failure.test.ts @@ -0,0 +1,373 @@ +import fs from "node:fs/promises"; +import http from "node:http"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent"; +import { loadOpenClawPlugins, clearPluginLoaderCache } from "../../plugins/loader.js"; +import { + getGlobalPluginRegistry, + resetGlobalHookRunner, +} from "../../plugins/hook-runner-global.js"; +import { runEmbeddedAttempt } from "./run/attempt.js"; +import type { Model } from "@mariozechner/pi-ai"; + +const tempRoots: string[] = []; +const servers: http.Server[] = []; +const ATTEMPT_STARTUP_TIMEOUT_MS = 120_000; +const PROVIDER_RESPONSE_TIMEOUT_MS = 5_000; + +async function withTimeout(params: { + promise: Promise; + timeoutMs: number; + onTimeout: () => Error; +}): Promise { + let timer: NodeJS.Timeout | undefined; + const timeout = new Promise((_, reject) => { + timer = setTimeout(() => reject(params.onTimeout()), params.timeoutMs); + }); + try { + return await Promise.race([params.promise, timeout]); + } finally { + if (timer) clearTimeout(timer); + } +} + +async function makeRoot(prefix: string): Promise { + const root = await fs.mkdtemp(path.join(os.tmpdir(), prefix)); + tempRoots.push(root); + return root; +} + +async function listenFakeProvider(): Promise<{ + baseUrl: string; + requestBodies: Array>; + stages: string[]; + close: () => Promise; +}> { + const requestBodies: Array> = []; + const stages: string[] = []; + const server = http.createServer(async (request, response) => { + stages.push("provider_request"); + const chunks: Buffer[] = []; + for await (const chunk of request) chunks.push(Buffer.from(chunk)); + requestBodies.push( + JSON.parse(Buffer.concat(chunks).toString("utf8")) as Record, + ); + response.writeHead(200, { + "content-type": "text/event-stream", + connection: "keep-alive", + "cache-control": "no-cache", + }); + if (requestBodies.length === 1) { + response.write( + `data: ${JSON.stringify({ + id: "m9-a0-layer2-tool", + object: "chat.completion.chunk", + created: 1, + model: "fake-model", + choices: [ + { + index: 0, + delta: { + role: "assistant", + tool_calls: [ + { + index: 0, + id: "call-layer2-1", + type: "function", + function: { name: "synthetic", arguments: "{}" }, + }, + ], + }, + finish_reason: null, + }, + ], + })}\n\n`, + ); + response.write( + `data: ${JSON.stringify({ + id: "m9-a0-layer2-tool", + object: "chat.completion.chunk", + created: 1, + model: "fake-model", + choices: [{ index: 0, delta: {}, finish_reason: "tool_calls" }], + })}\n\n`, + ); + } else { + response.write( + `data: ${JSON.stringify({ + id: "m9-a0-layer2", + object: "chat.completion.chunk", + created: 1, + model: "fake-model", + choices: [{ index: 0, delta: { role: "assistant", content: "ok" }, finish_reason: null }], + })}\n\n`, + ); + response.write( + `data: ${JSON.stringify({ + id: "m9-a0-layer2", + object: "chat.completion.chunk", + created: 1, + model: "fake-model", + choices: [{ index: 0, delta: {}, finish_reason: "stop" }], + })}\n\n`, + ); + } + response.end("data: [DONE]\n\n"); + }); + servers.push(server); + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", resolve); + }); + const address = server.address(); + if (!address || typeof address === "string") throw new Error("FAKE_PROVIDER_ADDRESS_MISSING"); + return { + baseUrl: `http://127.0.0.1:${address.port}/v1`, + requestBodies, + stages, + close: async () => { + await new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())), + ); + }, + }; +} + +function model(baseUrl: string): Model<"openai-completions"> { + return { + id: "fake-model", + name: "M9 A0 fake model", + api: "openai-completions", + provider: "fake-provider", + baseUrl, + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 32768, + maxTokens: 256, + }; +} + +function writePlugin(root: string): Promise { + const plugin = path.join(root, "tiangong-control.cjs"); + const manifest = path.join(root, "openclaw.plugin.json"); + return Promise.all([ + fs.writeFile( + manifest, + JSON.stringify({ + id: "tiangong-control", + providers: ["fake-provider"], + configSchema: { type: "object", properties: {} }, + }), + "utf8", + ), + fs.writeFile( + plugin, + `const fs = require("node:fs"); +function emit(event, fields) { + fs.appendFileSync(process.env.M9_A0_EVENTS_FILE, JSON.stringify({ event, ...fields }) + "\\n"); +} +module.exports = { + id: "tiangong-control", + register(api) { + api.registerProvider({ id: "fake-provider", label: "M9 A0 fake provider", auth: [] }); + api.registerTool({ + name: "synthetic", + label: "synthetic", + description: "M9 A0 synthetic tool", + parameters: { type: "object", properties: {}, additionalProperties: false }, + execute: async () => { + emit("tool-executed", {}); + return { content: [{ type: "text", text: "synthetic-ok" }], details: { status: "ok" } }; + }, + }); + api.on("before_prompt_build", async () => { + emit("attempt-ready", {}); + const readyEvent = process.env.M9_A0_ATTEMPT_READY_EVENT; + if (readyEvent) process.emit(readyEvent); + }); + api.on("before_model_call", async (event) => { + emit("model-handler", { provider: event.provider, model: event.model }); + return { allow: true, payload: { ...event.payload, m9Trusted: "allowed" } }; + }); + api.on("before_tool_result_release", async (event) => { + emit("tool-handler", { toolName: event.toolName, isError: event.isError }); + if (process.env.M9_A0_CAPTURE_FAILURE === "1") { + throw new Error("fixture-capture-failure"); + } + return { release: true }; + }); + }, +}; +`, + "utf8", + ), + ]).then(() => plugin); +} + +afterEach(async () => { + resetGlobalHookRunner(); + clearPluginLoaderCache(); + for (const server of servers.splice(0)) { + if (server.listening) await new Promise((resolve) => server.close(() => resolve())); + } + while (tempRoots.length > 0) await fs.rm(tempRoots.pop()!, { recursive: true, force: true }); +}); + +describe("M9-A0 Layer 2 actual runEmbeddedAttempt capture failure", () => { + it("stops release before ToolResult emission and the next provider request", async () => { + const root = await makeRoot("m9-a0-layer2-"); + const stages: string[] = ["root_created"]; + const eventsFile = path.join(root, "events.ndjson"); + const pluginFile = await writePlugin(root); + stages.push("plugin_written"); + const provider = await listenFakeProvider(); + stages.push("provider_listening"); + const previousRequired = process.env.TIANGONG_TRUSTED_BOUNDARIES_REQUIRED; + const previousEvents = process.env.M9_A0_EVENTS_FILE; + const previousReadyEvent = process.env.M9_A0_ATTEMPT_READY_EVENT; + const previousCaptureFailure = process.env.M9_A0_CAPTURE_FAILURE; + const readyEvent = `m9-a0-attempt-ready:${process.pid}:${path.basename(root)}`; + let markAttemptReady: () => void = () => undefined; + const attemptReady = new Promise((resolve) => { + markAttemptReady = resolve; + }); + process.once(readyEvent, markAttemptReady); + process.env.TIANGONG_TRUSTED_BOUNDARIES_REQUIRED = "1"; + process.env.M9_A0_EVENTS_FILE = eventsFile; + process.env.M9_A0_ATTEMPT_READY_EVENT = readyEvent; + process.env.M9_A0_CAPTURE_FAILURE = "1"; + try { + const registry = loadOpenClawPlugins({ + cache: false, + workspaceDir: root, + config: { + plugins: { + allow: ["tiangong-control"], + load: { paths: [pluginFile] }, + }, + }, + }); + stages.push("registry_loaded"); + const controlPlugin = registry.plugins.find((plugin) => plugin.id === "tiangong-control"); + if (!controlPlugin || controlPlugin.status !== "loaded") { + throw new Error( + `PLUGIN_DIAGNOSTIC:${JSON.stringify({ + found: Boolean(controlPlugin), + status: controlPlugin?.status, + error: controlPlugin?.error, + source: controlPlugin?.source, + typedHookNames: registry.typedHooks.map((hook) => hook.hookName), + })}`, + ); + } + expect( + registry.providers.filter((entry) => entry.provider.id === "fake-provider"), + ).toHaveLength(1); + expect( + registry.typedHooks.filter((hook) => hook.hookName === "before_model_call"), + ).toHaveLength(1); + expect( + registry.typedHooks.filter((hook) => hook.hookName === "before_tool_result_release"), + ).toHaveLength(1); + expect(getGlobalPluginRegistry()).toBe(registry); + stages.push("hooks_verified"); + + const sessionFile = path.join(root, "session.jsonl"); + await fs.writeFile(sessionFile, "", "utf8"); + const authStorage = AuthStorage.inMemory(); + authStorage.setRuntimeApiKey("fake-provider", "fixture-provider-value"); + stages.push("attempt_starting"); + const abortController = new AbortController(); + const attemptPromise = runEmbeddedAttempt({ + sessionId: "layer2-session", + sessionKey: "agent:layer2:main", + sessionFile, + workspaceDir: root, + agentDir: root, + config: { + plugins: { + allow: ["tiangong-control"], + load: { paths: [pluginFile] }, + }, + }, + prompt: "layer2 readiness", + timeoutMs: 10_000, + runId: "layer2-readiness", + provider: "fake-provider", + modelId: "fake-model", + model: model(provider.baseUrl), + resolvedApiKey: "fixture-provider-value", + authStorage: authStorage as never, + modelRegistry: ModelRegistry.inMemory(authStorage) as never, + thinkLevel: "off", + senderIsOwner: true, + disableMessageTool: true, + disableTools: false, + toolsAllow: ["synthetic"], + abortSignal: abortController.signal, + onAgentEvent: (event) => stages.push(`agent_event:${event.stream}`), + onToolResult: () => stages.push("tool-result-callback"), + }); + void attemptPromise.catch(() => undefined); + await withTimeout({ + promise: Promise.race([ + attemptReady, + attemptPromise.then((earlyResult) => { + throw new Error( + `ATTEMPT_COMPLETED_BEFORE_READINESS promptError=${earlyResult.promptError ? "present" : "none"}`, + ); + }), + ]), + timeoutMs: ATTEMPT_STARTUP_TIMEOUT_MS, + onTimeout: () => { + abortController.abort("M9_A0_STARTUP_TIMEOUT"); + return new Error( + `LAYER2_STARTUP_TIMEOUT stages=${stages.join(",")} providerRequests=${provider.requestBodies.length}`, + ); + }, + }); + stages.push("attempt_ready"); + const result = await withTimeout({ + promise: attemptPromise, + timeoutMs: PROVIDER_RESPONSE_TIMEOUT_MS, + onTimeout: () => { + abortController.abort("M9_A0_PROVIDER_RESPONSE_TIMEOUT"); + return new Error( + `LAYER2_PROVIDER_RESPONSE_TIMEOUT stages=${stages.join(",")} providerRequests=${provider.requestBodies.length}`, + ); + }, + }); + + expect(provider.requestBodies).toHaveLength(1); + expect(provider.requestBodies[0]).toMatchObject({ m9Trusted: "allowed" }); + const events = (await fs.readFile(eventsFile, "utf8")) + .trim() + .split("\n") + .filter(Boolean) + .map((line) => (JSON.parse(line) as { event: string }).event); + expect(events).toEqual(["attempt-ready", "model-handler", "tool-executed", "tool-handler"]); + expect(stages).not.toContain("tool-result-callback"); + const entries = (await fs.readFile(sessionFile, "utf8")) + .split("\n") + .filter(Boolean) + .map((line) => JSON.parse(line) as { type?: string; message?: { role?: string } }); + expect( + entries.filter((entry) => entry.type === "message" && entry.message?.role === "toolResult"), + ).toHaveLength(0); + } finally { + process.off(readyEvent, markAttemptReady); + await provider.close(); + if (previousRequired === undefined) delete process.env.TIANGONG_TRUSTED_BOUNDARIES_REQUIRED; + else process.env.TIANGONG_TRUSTED_BOUNDARIES_REQUIRED = previousRequired; + if (previousEvents === undefined) delete process.env.M9_A0_EVENTS_FILE; + else process.env.M9_A0_EVENTS_FILE = previousEvents; + if (previousReadyEvent === undefined) delete process.env.M9_A0_ATTEMPT_READY_EVENT; + else process.env.M9_A0_ATTEMPT_READY_EVENT = previousReadyEvent; + if (previousCaptureFailure === undefined) delete process.env.M9_A0_CAPTURE_FAILURE; + else process.env.M9_A0_CAPTURE_FAILURE = previousCaptureFailure; + } + }, 180_000); +}); diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-compaction.test.ts b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-compaction.test.ts new file mode 100644 index 0000000..36b6bbb --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-compaction.test.ts @@ -0,0 +1,233 @@ +import fs from "node:fs/promises"; +import http from "node:http"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { SessionManager } from "@mariozechner/pi-coding-agent"; +import { clearPluginLoaderCache, loadOpenClawPlugins } from "../../plugins/loader.js"; +import { resetGlobalHookRunner } from "../../plugins/hook-runner-global.js"; +import { compactEmbeddedPiSessionDirect } from "./compact.js"; + +const roots: string[] = []; +const servers: http.Server[] = []; + +async function writePlugin(root: string, eventsFile: string): Promise { + const plugin = path.join(root, "tiangong-control.cjs"); + await Promise.all([ + fs.writeFile( + path.join(root, "openclaw.plugin.json"), + JSON.stringify({ + id: "tiangong-control", + providers: ["fake-provider"], + configSchema: { type: "object", properties: {} }, + }), + "utf8", + ), + fs.writeFile( + plugin, + `const fs = require("node:fs"); +function emit(event, fields) { + fs.appendFileSync(${JSON.stringify(eventsFile)}, JSON.stringify({ event, ...fields }) + "\\n"); +} +module.exports = { + id: "tiangong-control", + register(api) { + api.registerProvider({ id: "fake-provider", label: "M9 A0 fake provider", auth: [] }); + api.on("before_model_call", async (event) => { + emit("compaction-model-handler", { provider: event.provider, model: event.model }); + return { allow: true, payload: { ...event.payload, m9Trusted: "allowed" } }; + }); + api.on("before_tool_result_release", async () => ({ release: true })); + }, +}; +`, + "utf8", + ), + ]); + return plugin; +} + +async function listenProvider(): Promise<{ + baseUrl: string; + requestBodies: Array>; + close: () => Promise; +}> { + const requestBodies: Array> = []; + const server = http.createServer(async (request, response) => { + const chunks: Buffer[] = []; + for await (const chunk of request) chunks.push(Buffer.from(chunk)); + requestBodies.push( + JSON.parse(Buffer.concat(chunks).toString("utf8")) as Record, + ); + response.writeHead(200, { + "content-type": "text/event-stream", + connection: "keep-alive", + "cache-control": "no-cache", + }); + const id = `m9-a0-compaction-${requestBodies.length}`; + response.write( + `data: ${JSON.stringify({ + id, + object: "chat.completion.chunk", + created: 1, + model: "fake-model", + choices: [ + { + index: 0, + delta: { role: "assistant", content: "compacted summary" }, + finish_reason: null, + }, + ], + })}\n\n`, + ); + response.write( + `data: ${JSON.stringify({ + id, + object: "chat.completion.chunk", + created: 1, + model: "fake-model", + choices: [{ index: 0, delta: {}, finish_reason: "stop" }], + })}\n\n`, + ); + response.end("data: [DONE]\n\n"); + }); + servers.push(server); + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", resolve); + }); + const address = server.address(); + if (!address || typeof address === "string") throw new Error("FAKE_PROVIDER_ADDRESS_MISSING"); + return { + baseUrl: `http://127.0.0.1:${address.port}/v1`, + requestBodies, + close: async () => { + await new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())), + ); + }, + }; +} + +function appendConversation(sessionFile: string): void { + const manager = SessionManager.open(sessionFile); + manager.appendMessage({ + role: "user", + content: [{ type: "text", text: "old user turn" }], + timestamp: Date.now(), + } as never); + manager.appendMessage({ + role: "assistant", + content: [{ type: "text", text: "old assistant turn" }], + api: "openai-completions", + provider: "fake-provider", + model: "fake-model", + usage: { + input: 10, + output: 4, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 14, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, + }, + stopReason: "stop", + timestamp: Date.now(), + } as never); +} + +afterEach(async () => { + resetGlobalHookRunner(); + clearPluginLoaderCache(); + for (const server of servers.splice(0)) { + if (server.listening) await new Promise((resolve) => server.close(() => resolve())); + } + while (roots.length > 0) await fs.rm(roots.pop()!, { recursive: true, force: true }); +}); + +describe("M9-A0 Layer 2 OpenClaw-owned compaction diagnostic", () => { + it("proves the current compaction path bypasses the trusted model boundary", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "m9-a0-compaction-")); + roots.push(root); + const eventsFile = path.join(root, "events.ndjson"); + const pluginFile = await writePlugin(root, eventsFile); + const provider = await listenProvider(); + const sessionFile = path.join(root, "session.jsonl"); + appendConversation(sessionFile); + const config = { + plugins: { + allow: ["tiangong-control"], + load: { paths: [pluginFile] }, + }, + models: { + providers: { + "fake-provider": { + baseUrl: provider.baseUrl, + api: "openai-completions", + apiKey: "fixture-provider-value", + models: [ + { + id: "fake-model", + name: "M9 A0 fake model", + api: "openai-completions", + input: ["text"], + reasoning: false, + contextWindow: 32768, + maxTokens: 256, + }, + ], + }, + }, + }, + }; + const registry = loadOpenClawPlugins({ cache: false, workspaceDir: root, config }); + expect(registry.plugins.find((plugin) => plugin.id === "tiangong-control")?.status).toBe( + "loaded", + ); + expect( + registry.providers.filter((entry) => entry.provider.id === "fake-provider"), + ).toHaveLength(1); + expect( + registry.typedHooks.filter((hook) => hook.hookName === "before_model_call"), + ).toHaveLength(1); + expect( + registry.typedHooks.filter((hook) => hook.hookName === "before_tool_result_release"), + ).toHaveLength(1); + + const previousRequired = process.env.TIANGONG_TRUSTED_BOUNDARIES_REQUIRED; + process.env.TIANGONG_TRUSTED_BOUNDARIES_REQUIRED = "1"; + try { + const result = await compactEmbeddedPiSessionDirect({ + sessionId: "layer2-compaction-session", + runId: "layer2-compaction-run", + sessionKey: "agent:layer2:compaction", + sessionFile, + workspaceDir: root, + agentDir: root, + config, + provider: "fake-provider", + model: "fake-model", + thinkLevel: "off", + force: true, + trigger: "manual", + }); + expect(result.ok).toBe(true); + expect(result.compacted).toBe(true); + expect(provider.requestBodies).toHaveLength(1); + expect(provider.requestBodies[0]?.m9Trusted).not.toBe("allowed"); + const events = (await fs.readFile(eventsFile, "utf8").catch(() => "")) + .trim() + .split("\n") + .filter(Boolean) + .map((line) => (JSON.parse(line) as { event: string }).event); + expect(events).toEqual([]); + const entries = (await fs.readFile(sessionFile, "utf8")) + .split("\n") + .filter(Boolean) + .map((line) => JSON.parse(line) as { type?: string }); + expect(entries.some((entry) => entry.type === "compaction")).toBe(true); + } finally { + if (previousRequired === undefined) delete process.env.TIANGONG_TRUSTED_BOUNDARIES_REQUIRED; + else process.env.TIANGONG_TRUSTED_BOUNDARIES_REQUIRED = previousRequired; + } + }, 180_000); +}); diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-followup.test.ts b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-followup.test.ts new file mode 100644 index 0000000..ba51b80 --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-followup.test.ts @@ -0,0 +1,287 @@ +import fs from "node:fs/promises"; +import http from "node:http"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent"; +import type { Model } from "@mariozechner/pi-ai"; +import { clearPluginLoaderCache, loadOpenClawPlugins } from "../../plugins/loader.js"; +import { resetGlobalHookRunner } from "../../plugins/hook-runner-global.js"; +import { runEmbeddedAttempt } from "./run/attempt.js"; + +const roots: string[] = []; +const servers: http.Server[] = []; +const HOST_STARTUP_TIMEOUT_MS = 120_000; +const PROVIDER_RESPONSE_TIMEOUT_MS = 5_000; + +async function withTimeout(params: { + promise: Promise; + timeoutMs: number; + onTimeout: () => Error; +}): Promise { + let timer: NodeJS.Timeout | undefined; + const timeout = new Promise((_, reject) => { + timer = setTimeout(() => reject(params.onTimeout()), params.timeoutMs); + }); + try { + return await Promise.race([params.promise, timeout]); + } finally { + if (timer) clearTimeout(timer); + } +} + +function fakeModel(baseUrl: string): Model<"openai-completions"> { + return { + id: "fake-model", + name: "M9 A0 fake model", + api: "openai-completions", + provider: "fake-provider", + baseUrl, + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 32768, + maxTokens: 256, + }; +} + +async function writePlugin(root: string, eventsFile: string): Promise { + const plugin = path.join(root, "tiangong-control.cjs"); + await Promise.all([ + fs.writeFile( + path.join(root, "openclaw.plugin.json"), + JSON.stringify({ + id: "tiangong-control", + providers: ["fake-provider"], + configSchema: { type: "object", properties: {} }, + }), + "utf8", + ), + fs.writeFile( + plugin, + `const fs = require("node:fs"); +function emit(event, fields) { + fs.appendFileSync(${JSON.stringify(eventsFile)}, JSON.stringify({ event, ...fields }) + "\\n"); +} +module.exports = { + id: "tiangong-control", + register(api) { + api.registerProvider({ id: "fake-provider", label: "M9 A0 fake provider", auth: [] }); + api.on("before_prompt_build", async () => { + emit("attempt-ready", {}); + const readyEvent = process.env.M9_A0_ATTEMPT_READY_EVENT; + if (readyEvent) process.emit(readyEvent); + }); + api.on("before_model_call", async (event) => { + emit("model-handler", { provider: event.provider, model: event.model }); + return { allow: true, payload: { ...event.payload, m9Trusted: "allowed" } }; + }); + api.on("before_tool_result_release", async () => ({ release: true })); + }, +}; +`, + "utf8", + ), + ]); + return plugin; +} + +async function listenProvider(): Promise<{ + baseUrl: string; + requestBodies: Array>; + close: () => Promise; +}> { + const requestBodies: Array> = []; + const server = http.createServer(async (request, response) => { + const chunks: Buffer[] = []; + for await (const chunk of request) chunks.push(Buffer.from(chunk)); + requestBodies.push( + JSON.parse(Buffer.concat(chunks).toString("utf8")) as Record, + ); + response.writeHead(200, { + "content-type": "text/event-stream", + connection: "keep-alive", + "cache-control": "no-cache", + }); + const id = `m9-a0-follow-up-${requestBodies.length}`; + response.write( + `data: ${JSON.stringify({ + id, + object: "chat.completion.chunk", + created: 1, + model: "fake-model", + choices: [{ index: 0, delta: { role: "assistant", content: "ok" }, finish_reason: null }], + })}\n\n`, + ); + response.write( + `data: ${JSON.stringify({ + id, + object: "chat.completion.chunk", + created: 1, + model: "fake-model", + choices: [{ index: 0, delta: {}, finish_reason: "stop" }], + })}\n\n`, + ); + response.end("data: [DONE]\n\n"); + }); + servers.push(server); + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", resolve); + }); + const address = server.address(); + if (!address || typeof address === "string") throw new Error("FAKE_PROVIDER_ADDRESS_MISSING"); + return { + baseUrl: `http://127.0.0.1:${address.port}/v1`, + requestBodies, + close: async () => { + await new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())), + ); + }, + }; +} + +afterEach(async () => { + resetGlobalHookRunner(); + clearPluginLoaderCache(); + for (const server of servers.splice(0)) { + if (server.listening) await new Promise((resolve) => server.close(() => resolve())); + } + while (roots.length > 0) await fs.rm(roots.pop()!, { recursive: true, force: true }); +}); + +describe("M9-A0 Layer 2 actual follow-up attempt", () => { + it("reinstalls the trusted model boundary on a persisted-session follow-up", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "m9-a0-follow-up-")); + roots.push(root); + const eventsFile = path.join(root, "events.ndjson"); + const pluginFile = await writePlugin(root, eventsFile); + const provider = await listenProvider(); + const config = { + plugins: { + allow: ["tiangong-control"], + load: { paths: [pluginFile] }, + }, + }; + const registry = loadOpenClawPlugins({ cache: false, workspaceDir: root, config }); + expect(registry.plugins.find((plugin) => plugin.id === "tiangong-control")?.status).toBe( + "loaded", + ); + expect( + registry.providers.filter((entry) => entry.provider.id === "fake-provider"), + ).toHaveLength(1); + expect( + registry.typedHooks.filter((hook) => hook.hookName === "before_model_call"), + ).toHaveLength(1); + expect( + registry.typedHooks.filter((hook) => hook.hookName === "before_tool_result_release"), + ).toHaveLength(1); + + const previousRequired = process.env.TIANGONG_TRUSTED_BOUNDARIES_REQUIRED; + const previousEvents = process.env.M9_A0_EVENTS_FILE; + const previousReadyEvent = process.env.M9_A0_ATTEMPT_READY_EVENT; + process.env.TIANGONG_TRUSTED_BOUNDARIES_REQUIRED = "1"; + process.env.M9_A0_EVENTS_FILE = eventsFile; + const sessionFile = path.join(root, "session.jsonl"); + await fs.writeFile(sessionFile, "", "utf8"); + const authStorage = AuthStorage.inMemory(); + authStorage.setRuntimeApiKey("fake-provider", "fixture-provider-value"); + + const runOnce = async (runId: string, prompt: string) => { + const readyEvent = `m9-a0-follow-up-ready:${process.pid}:${runId}`; + let markReady: () => void = () => undefined; + const ready = new Promise((resolve) => { + markReady = resolve; + }); + process.once(readyEvent, markReady); + process.env.M9_A0_ATTEMPT_READY_EVENT = readyEvent; + const abortController = new AbortController(); + const attempt = runEmbeddedAttempt({ + sessionId: "layer2-follow-up-session", + sessionKey: "agent:layer2:follow-up", + sessionFile, + workspaceDir: root, + agentDir: root, + config, + prompt, + timeoutMs: 10_000, + runId, + provider: "fake-provider", + modelId: "fake-model", + model: fakeModel(provider.baseUrl), + resolvedApiKey: "fixture-provider-value", + authStorage: authStorage as never, + modelRegistry: ModelRegistry.inMemory(authStorage) as never, + thinkLevel: "off", + senderIsOwner: true, + disableMessageTool: true, + disableTools: true, + abortSignal: abortController.signal, + }); + void attempt.catch(() => undefined); + try { + await withTimeout({ + promise: Promise.race([ + ready, + attempt.then((earlyResult) => { + throw new Error( + `FOLLOW_UP_COMPLETED_BEFORE_READINESS run=${runId} promptError=${earlyResult.promptError ? "present" : "none"}`, + ); + }), + ]), + timeoutMs: HOST_STARTUP_TIMEOUT_MS, + onTimeout: () => { + abortController.abort("M9_A0_FOLLOW_UP_STARTUP_TIMEOUT"); + return new Error( + `FOLLOW_UP_STARTUP_TIMEOUT run=${runId} providerRequests=${provider.requestBodies.length}`, + ); + }, + }); + return await withTimeout({ + promise: attempt, + timeoutMs: PROVIDER_RESPONSE_TIMEOUT_MS, + onTimeout: () => { + abortController.abort("M9_A0_FOLLOW_UP_PROVIDER_TIMEOUT"); + return new Error( + `FOLLOW_UP_PROVIDER_TIMEOUT run=${runId} providerRequests=${provider.requestBodies.length}`, + ); + }, + }); + } finally { + process.off(readyEvent, markReady); + } + }; + + try { + const first = await runOnce("layer2-follow-up-first", "first turn"); + const second = await runOnce("layer2-follow-up-second", "persisted follow-up"); + expect(first.promptError).toBeNull(); + expect(second.promptError).toBeNull(); + expect(provider.requestBodies).toHaveLength(2); + expect(provider.requestBodies[0]).toMatchObject({ m9Trusted: "allowed" }); + expect(provider.requestBodies[1]).toMatchObject({ m9Trusted: "allowed" }); + const events = (await fs.readFile(eventsFile, "utf8")) + .trim() + .split("\n") + .filter(Boolean) + .map((line) => (JSON.parse(line) as { event: string }).event); + expect(events).toEqual(["attempt-ready", "model-handler", "attempt-ready", "model-handler"]); + const entries = (await fs.readFile(sessionFile, "utf8")) + .split("\n") + .filter(Boolean) + .map((line) => JSON.parse(line) as { type?: string; message?: { role?: string } }); + expect( + entries.filter((entry) => entry.type === "message" && entry.message?.role === "user"), + ).toHaveLength(2); + } finally { + await provider.close(); + if (previousRequired === undefined) delete process.env.TIANGONG_TRUSTED_BOUNDARIES_REQUIRED; + else process.env.TIANGONG_TRUSTED_BOUNDARIES_REQUIRED = previousRequired; + if (previousEvents === undefined) delete process.env.M9_A0_EVENTS_FILE; + else process.env.M9_A0_EVENTS_FILE = previousEvents; + if (previousReadyEvent === undefined) delete process.env.M9_A0_ATTEMPT_READY_EVENT; + else process.env.M9_A0_ATTEMPT_READY_EVENT = previousReadyEvent; + } + }, 300_000); +}); diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-readiness-results.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-readiness-results.txt index 5380bf6..c255517 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-readiness-results.txt +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-readiness-results.txt @@ -1,23 +1,23 @@ -container=tiangong-m9a0-layer2-374508_20260823T024326Z +container=tiangong-m9a0-layer2-422961_20260823T033559Z image=tg-worker:dev image_id=sha256:ac1cb183e5b2c82982f6473fef86ccf128763a31192d711526d843a79edb69ff image_created=2026-08-21T13:00:21.057325912+08:00 openclaw_version=OpenClaw 2026.4.14 (2f35b6f) -patch_sha256=24671f2d21c9b6d2904e10f7e3874981983f12a6a9968cf7d8ff4001c6f93a1c +patch_sha256=eb79f2c2c296f528ec93f1ce38581780b4d17afd566ba10fd3f07333169dd34d Checking formatting... -All matched files use the correct format. -Finished in 37ms on 1 files using 16 threads. No config found, using defaults. Please add a config file or try `oxfmt --init` if needed. +All matched files use the correct format. +Finished in 46ms on 1 files using 16 threads. - RUN  v4.1.4 /opt/openclaw + RUN v4.1.4 /opt/openclaw - ✓ src/agents/layer2-run-embedded-attempt-baseline.test.ts > M9-A0 unpatched runEmbeddedAttempt baseline > reaches a registered local provider after observable attempt readiness 31131ms + ✓ src/agents/layer2-run-embedded-attempt-baseline.test.ts > M9-A0 unpatched runEmbeddedAttempt baseline > reaches a registered local provider after observable attempt readiness 31775ms - Test Files  1 passed (1) - Tests  1 passed (1) - Start at  02:43:27 - Duration  36.46s (transform 3.69s, setup 0ms, import 5.26s, tests 31.13s, environment 0ms) + Test Files 1 passed (1) + Tests 1 passed (1) + Start at 03:36:00 + Duration 37.33s (transform 3.83s, setup 0ms, import 5.48s, tests 31.78s, environment 0ms) checking file src/agents/pi-embedded-runner/compact.ts checking file src/agents/pi-embedded-runner/run/attempt.ts @@ -30,18 +30,18 @@ patching file src/plugins/hook-types.ts Checking formatting... All matched files use the correct format. -Finished in 41ms on 1 files using 16 threads. +Finished in 40ms on 1 files using 16 threads. No config found, using defaults. Please add a config file or try `oxfmt --init` if needed. - RUN  v4.1.4 /opt/openclaw + RUN v4.1.4 /opt/openclaw - ✓ src/agents/pi-embedded-runner/layer2-run-embedded-attempt.test.ts > M9-A0 Layer 2 actual runEmbeddedAttempt tool turn > gates the initial request, tool release, and post-tool request 74831ms + ✓ src/agents/pi-embedded-runner/layer2-run-embedded-attempt.test.ts > M9-A0 Layer 2 actual runEmbeddedAttempt tool turn > gates the initial request, tool release, and post-tool request 79054ms - Test Files  1 passed (1) - Tests  1 passed (1) - Start at  02:44:04 - Duration  80.14s (transform 3.68s, setup 0ms, import 5.24s, tests 74.83s, environment 0ms) + Test Files 1 passed (1) + Tests 1 passed (1) + Start at 03:36:37 + Duration 84.46s (transform 3.69s, setup 0ms, import 5.33s, tests 79.06s, environment 0ms) -cleanup_owner=tiangong-m9a0-layer2-374508_20260823T024326Z +cleanup_owner=tiangong-m9a0-layer2-422961_20260823T033559Z cleanup_container_absent=true layer2_readiness_exit=0 diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-remaining-results.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-remaining-results.txt new file mode 100644 index 0000000..1a7c8a5 --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-remaining-results.txt @@ -0,0 +1,76 @@ +layer=2-remaining +status=blocked_at_compaction_stop_line +image=tg-worker:dev +image_id=sha256:ac1cb183e5b2c82982f6473fef86ccf128763a31192d711526d843a79edb69ff +openclaw=OpenClaw 2026.4.14 (2f35b6f) +network=none +patch_sha256=eb79f2c2c296f528ec93f1ce38581780b4d17afd566ba10fd3f07333169dd34d +patch_bytes=10942 +upstream_license=MIT +baseline_tiangong_commit=992665f +activation=TIANGONG_TRUSTED_BOUNDARIES_REQUIRED=1 +provider=owned_loopback_http_fake_provider + +main_readiness_runner=pass +main_readiness_container=tiangong-m9a0-layer2-422961_20260823T033559Z +unpatched_registered_provider_baseline=1/1 +patched_initial_tool_post_tool=1/1 +patched_provider_requests=2 +patched_tool_executions=1 +patched_event_order=attempt-ready,model-handler,tool-executed,tool-handler,model-handler +main_readiness_cleanup_container_absent=true + +normalized_tool_error=pass +normalized_tool_error_container=tiangong-m9a0-layer2-tool-error-fixed-399791_20260823T031300Z +normalized_tool_error_provider_requests=2 +normalized_tool_error_tool_executions=1 +normalized_tool_error_handler_isError=true +normalized_tool_error_event_order=attempt-ready,model-handler,tool-executed,tool-handler,model-handler +normalized_tool_error_cleanup_container_absent=true + +capture_failure=pass +capture_failure_container=tiangong-m9a0-layer2-capture-failure-fixed-404122_20260823T031624Z +capture_failure_provider_requests=1 +capture_failure_tool_executions=1 +capture_failure_next_model_handler=not_observed +capture_failure_tool_result_callback=not_observed +capture_failure_session_tool_result_messages=0 +capture_failure_cleanup_container_absent=true + +persisted_follow_up=pass +persisted_follow_up_container=tiangong-m9a0-layer2-followup-407234_20260823T032018Z +persisted_follow_up_provider_requests=2 +persisted_follow_up_model_handlers=2 +persisted_follow_up_event_order=attempt-ready,model-handler,attempt-ready,model-handler +persisted_follow_up_session_user_messages=2 +persisted_follow_up_cleanup_container_absent=true + +compaction_boundary=blocked +compaction_diagnostic_container=tiangong-m9a0-layer2-compaction-diag2-421077_20260823T033407Z +compaction_result_ok=true +compaction_result_compacted=true +compaction_provider_requests=1 +compaction_trusted_marker_in_provider_body=absent +compaction_before_model_call_handler_events=0 +compaction_entry_persisted=true +compaction_cleanup_container_absent=true +compaction_direct_fact=pi-coding-agent compaction calls completeSimple directly; the OpenClaw agent.onPayload seam is not on that call path + +initial_tool_error_diagnostic=source-failure-led-to-source-correction +initial_tool_error_container=tiangong-m9a0-layer2-tool-error-385530_20260823T025755Z +initial_tool_error_observation=OpenClaw normalized a thrown tool error to details.status=error while pi afterToolCall isError remained false +initial_tool_error_assertion=received_false_expected_true +initial_tool_error_cleanup_container_absent=true + +source_contract_after_tool_error_correction=pass +source_contract_container=tiangong-m9a0-source-399111_20260823T031231Z +source_contract_boundary_tests=10/10 +source_contract_adjacent_regressions=156/156 +source_contract_cleanup_container_absent=true + +stop_reason=supported_compaction_model_emitting_path_bypasses_required_final_handler +layer3=not_started +layer4_basic_matrix=not_started +external_resources=none +credentials=none +owned_container_prefix_absent=true diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-tool-error.test.ts b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-tool-error.test.ts new file mode 100644 index 0000000..d4852e4 --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-tool-error.test.ts @@ -0,0 +1,373 @@ +import fs from "node:fs/promises"; +import http from "node:http"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent"; +import { loadOpenClawPlugins, clearPluginLoaderCache } from "../../plugins/loader.js"; +import { + getGlobalPluginRegistry, + resetGlobalHookRunner, +} from "../../plugins/hook-runner-global.js"; +import { runEmbeddedAttempt } from "./run/attempt.js"; +import type { Model } from "@mariozechner/pi-ai"; + +const tempRoots: string[] = []; +const servers: http.Server[] = []; +const ATTEMPT_STARTUP_TIMEOUT_MS = 120_000; +const PROVIDER_RESPONSE_TIMEOUT_MS = 5_000; + +async function withTimeout(params: { + promise: Promise; + timeoutMs: number; + onTimeout: () => Error; +}): Promise { + let timer: NodeJS.Timeout | undefined; + const timeout = new Promise((_, reject) => { + timer = setTimeout(() => reject(params.onTimeout()), params.timeoutMs); + }); + try { + return await Promise.race([params.promise, timeout]); + } finally { + if (timer) clearTimeout(timer); + } +} + +async function makeRoot(prefix: string): Promise { + const root = await fs.mkdtemp(path.join(os.tmpdir(), prefix)); + tempRoots.push(root); + return root; +} + +async function listenFakeProvider(): Promise<{ + baseUrl: string; + requestBodies: Array>; + stages: string[]; + close: () => Promise; +}> { + const requestBodies: Array> = []; + const stages: string[] = []; + const server = http.createServer(async (request, response) => { + stages.push("provider_request"); + const chunks: Buffer[] = []; + for await (const chunk of request) chunks.push(Buffer.from(chunk)); + requestBodies.push( + JSON.parse(Buffer.concat(chunks).toString("utf8")) as Record, + ); + response.writeHead(200, { + "content-type": "text/event-stream", + connection: "keep-alive", + "cache-control": "no-cache", + }); + if (requestBodies.length === 1) { + response.write( + `data: ${JSON.stringify({ + id: "m9-a0-layer2-tool", + object: "chat.completion.chunk", + created: 1, + model: "fake-model", + choices: [ + { + index: 0, + delta: { + role: "assistant", + tool_calls: [ + { + index: 0, + id: "call-layer2-1", + type: "function", + function: { name: "synthetic", arguments: "{}" }, + }, + ], + }, + finish_reason: null, + }, + ], + })}\n\n`, + ); + response.write( + `data: ${JSON.stringify({ + id: "m9-a0-layer2-tool", + object: "chat.completion.chunk", + created: 1, + model: "fake-model", + choices: [{ index: 0, delta: {}, finish_reason: "tool_calls" }], + })}\n\n`, + ); + } else { + response.write( + `data: ${JSON.stringify({ + id: "m9-a0-layer2", + object: "chat.completion.chunk", + created: 1, + model: "fake-model", + choices: [{ index: 0, delta: { role: "assistant", content: "ok" }, finish_reason: null }], + })}\n\n`, + ); + response.write( + `data: ${JSON.stringify({ + id: "m9-a0-layer2", + object: "chat.completion.chunk", + created: 1, + model: "fake-model", + choices: [{ index: 0, delta: {}, finish_reason: "stop" }], + })}\n\n`, + ); + } + response.end("data: [DONE]\n\n"); + }); + servers.push(server); + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", resolve); + }); + const address = server.address(); + if (!address || typeof address === "string") throw new Error("FAKE_PROVIDER_ADDRESS_MISSING"); + return { + baseUrl: `http://127.0.0.1:${address.port}/v1`, + requestBodies, + stages, + close: async () => { + await new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())), + ); + }, + }; +} + +function model(baseUrl: string): Model<"openai-completions"> { + return { + id: "fake-model", + name: "M9 A0 fake model", + api: "openai-completions", + provider: "fake-provider", + baseUrl, + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 32768, + maxTokens: 256, + }; +} + +function writePlugin(root: string): Promise { + const plugin = path.join(root, "tiangong-control.cjs"); + const manifest = path.join(root, "openclaw.plugin.json"); + return Promise.all([ + fs.writeFile( + manifest, + JSON.stringify({ + id: "tiangong-control", + providers: ["fake-provider"], + configSchema: { type: "object", properties: {} }, + }), + "utf8", + ), + fs.writeFile( + plugin, + `const fs = require("node:fs"); +function emit(event, fields) { + fs.appendFileSync(process.env.M9_A0_EVENTS_FILE, JSON.stringify({ event, ...fields }) + "\\n"); +} +module.exports = { + id: "tiangong-control", + register(api) { + api.registerProvider({ id: "fake-provider", label: "M9 A0 fake provider", auth: [] }); + api.registerTool({ + name: "synthetic", + label: "synthetic", + description: "M9 A0 synthetic tool", + parameters: { type: "object", properties: {}, additionalProperties: false }, + execute: async () => { + emit("tool-executed", {}); + if (process.env.M9_A0_TOOL_ERROR === "1") { + throw new Error("fixture-tool-failure"); + } + return { content: [{ type: "text", text: "synthetic-ok" }], details: { status: "ok" } }; + }, + }); + api.on("before_prompt_build", async () => { + emit("attempt-ready", {}); + const readyEvent = process.env.M9_A0_ATTEMPT_READY_EVENT; + if (readyEvent) process.emit(readyEvent); + }); + api.on("before_model_call", async (event) => { + emit("model-handler", { provider: event.provider, model: event.model }); + return { allow: true, payload: { ...event.payload, m9Trusted: "allowed" } }; + }); + api.on("before_tool_result_release", async (event) => { + emit("tool-handler", { toolName: event.toolName, isError: event.isError }); + return { release: true }; + }); + }, +}; +`, + "utf8", + ), + ]).then(() => plugin); +} + +afterEach(async () => { + resetGlobalHookRunner(); + clearPluginLoaderCache(); + for (const server of servers.splice(0)) { + if (server.listening) await new Promise((resolve) => server.close(() => resolve())); + } + while (tempRoots.length > 0) await fs.rm(tempRoots.pop()!, { recursive: true, force: true }); +}); + +describe("M9-A0 Layer 2 actual runEmbeddedAttempt normalized tool error", () => { + it("captures a normalized tool error before the follow-up request", async () => { + const root = await makeRoot("m9-a0-layer2-"); + const stages: string[] = ["root_created"]; + const eventsFile = path.join(root, "events.ndjson"); + const pluginFile = await writePlugin(root); + stages.push("plugin_written"); + const provider = await listenFakeProvider(); + stages.push("provider_listening"); + const previousRequired = process.env.TIANGONG_TRUSTED_BOUNDARIES_REQUIRED; + const previousEvents = process.env.M9_A0_EVENTS_FILE; + const previousReadyEvent = process.env.M9_A0_ATTEMPT_READY_EVENT; + const previousToolError = process.env.M9_A0_TOOL_ERROR; + const readyEvent = `m9-a0-attempt-ready:${process.pid}:${path.basename(root)}`; + let markAttemptReady: () => void = () => undefined; + const attemptReady = new Promise((resolve) => { + markAttemptReady = resolve; + }); + process.once(readyEvent, markAttemptReady); + process.env.TIANGONG_TRUSTED_BOUNDARIES_REQUIRED = "1"; + process.env.M9_A0_EVENTS_FILE = eventsFile; + process.env.M9_A0_ATTEMPT_READY_EVENT = readyEvent; + process.env.M9_A0_TOOL_ERROR = "1"; + try { + const registry = loadOpenClawPlugins({ + cache: false, + workspaceDir: root, + config: { + plugins: { + allow: ["tiangong-control"], + load: { paths: [pluginFile] }, + }, + }, + }); + stages.push("registry_loaded"); + const controlPlugin = registry.plugins.find((plugin) => plugin.id === "tiangong-control"); + if (!controlPlugin || controlPlugin.status !== "loaded") { + throw new Error( + `PLUGIN_DIAGNOSTIC:${JSON.stringify({ + found: Boolean(controlPlugin), + status: controlPlugin?.status, + error: controlPlugin?.error, + source: controlPlugin?.source, + typedHookNames: registry.typedHooks.map((hook) => hook.hookName), + })}`, + ); + } + expect( + registry.providers.filter((entry) => entry.provider.id === "fake-provider"), + ).toHaveLength(1); + expect( + registry.typedHooks.filter((hook) => hook.hookName === "before_model_call"), + ).toHaveLength(1); + expect( + registry.typedHooks.filter((hook) => hook.hookName === "before_tool_result_release"), + ).toHaveLength(1); + expect(getGlobalPluginRegistry()).toBe(registry); + stages.push("hooks_verified"); + + const sessionFile = path.join(root, "session.jsonl"); + await fs.writeFile(sessionFile, "", "utf8"); + const authStorage = AuthStorage.inMemory(); + authStorage.setRuntimeApiKey("fake-provider", "fixture-provider-value"); + stages.push("attempt_starting"); + const abortController = new AbortController(); + const attemptPromise = runEmbeddedAttempt({ + sessionId: "layer2-session", + sessionKey: "agent:layer2:main", + sessionFile, + workspaceDir: root, + agentDir: root, + config: { + plugins: { + allow: ["tiangong-control"], + load: { paths: [pluginFile] }, + }, + }, + prompt: "layer2 readiness", + timeoutMs: 10_000, + runId: "layer2-readiness", + provider: "fake-provider", + modelId: "fake-model", + model: model(provider.baseUrl), + resolvedApiKey: "fixture-provider-value", + authStorage: authStorage as never, + modelRegistry: ModelRegistry.inMemory(authStorage) as never, + thinkLevel: "off", + senderIsOwner: true, + disableMessageTool: true, + disableTools: false, + toolsAllow: ["synthetic"], + abortSignal: abortController.signal, + onAgentEvent: (event) => stages.push(`agent_event:${event.stream}`), + }); + void attemptPromise.catch(() => undefined); + await withTimeout({ + promise: Promise.race([ + attemptReady, + attemptPromise.then((earlyResult) => { + throw new Error( + `ATTEMPT_COMPLETED_BEFORE_READINESS promptError=${earlyResult.promptError ? "present" : "none"}`, + ); + }), + ]), + timeoutMs: ATTEMPT_STARTUP_TIMEOUT_MS, + onTimeout: () => { + abortController.abort("M9_A0_STARTUP_TIMEOUT"); + return new Error( + `LAYER2_STARTUP_TIMEOUT stages=${stages.join(",")} providerRequests=${provider.requestBodies.length}`, + ); + }, + }); + stages.push("attempt_ready"); + const result = await withTimeout({ + promise: attemptPromise, + timeoutMs: PROVIDER_RESPONSE_TIMEOUT_MS, + onTimeout: () => { + abortController.abort("M9_A0_PROVIDER_RESPONSE_TIMEOUT"); + return new Error( + `LAYER2_PROVIDER_RESPONSE_TIMEOUT stages=${stages.join(",")} providerRequests=${provider.requestBodies.length}`, + ); + }, + }); + + expect(result.promptError).toBeNull(); + expect(provider.requestBodies).toHaveLength(2); + expect(provider.requestBodies[0]).toMatchObject({ m9Trusted: "allowed" }); + expect(provider.requestBodies[1]).toMatchObject({ m9Trusted: "allowed" }); + const records = (await fs.readFile(eventsFile, "utf8")) + .trim() + .split("\n") + .filter(Boolean) + .map((line) => JSON.parse(line) as { event: string; isError?: boolean }); + expect(records.map((record) => record.event)).toEqual([ + "attempt-ready", + "model-handler", + "tool-executed", + "tool-handler", + "model-handler", + ]); + expect(records.find((record) => record.event === "tool-handler")?.isError).toBe(true); + } finally { + process.off(readyEvent, markAttemptReady); + await provider.close(); + if (previousRequired === undefined) delete process.env.TIANGONG_TRUSTED_BOUNDARIES_REQUIRED; + else process.env.TIANGONG_TRUSTED_BOUNDARIES_REQUIRED = previousRequired; + if (previousEvents === undefined) delete process.env.M9_A0_EVENTS_FILE; + else process.env.M9_A0_EVENTS_FILE = previousEvents; + if (previousReadyEvent === undefined) delete process.env.M9_A0_ATTEMPT_READY_EVENT; + else process.env.M9_A0_ATTEMPT_READY_EVENT = previousReadyEvent; + if (previousToolError === undefined) delete process.env.M9_A0_TOOL_ERROR; + else process.env.M9_A0_TOOL_ERROR = previousToolError; + } + }, 180_000); +}); diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/openclaw-2026.4.14-trusted-native-boundaries.patch b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/openclaw-2026.4.14-trusted-native-boundaries.patch index b232d02..6377127 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/openclaw-2026.4.14-trusted-native-boundaries.patch +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/openclaw-2026.4.14-trusted-native-boundaries.patch @@ -58,7 +58,7 @@ new file mode 100644 index 0000000..2977f53 --- /dev/null +++ b/src/agents/pi-embedded-runner/trusted-boundaries.ts -@@ -0,0 +1,161 @@ +@@ -0,0 +1,162 @@ +import type { AfterToolCallContext, Agent } from "@mariozechner/pi-agent-core"; +import type { GlobalHookRunnerRegistry } from "../../plugins/hook-registry.types.js"; +import * as globalHookRunner from "../../plugins/hook-runner-global.js"; @@ -68,6 +68,7 @@ index 0000000..2977f53 + PluginHookBeforeToolResultReleaseResult, + PluginHookRegistration, +} from "../../plugins/hook-types.js"; ++import { isToolResultError } from "../pi-embedded-subscribe.tools.js"; + +export const TIANGONG_TRUSTED_BOUNDARIES_REQUIRED_ENV = "TIANGONG_TRUSTED_BOUNDARIES_REQUIRED"; +const PLUGIN_ID = "tiangong-control"; @@ -185,7 +186,7 @@ index 0000000..2977f53 + content: nativeResult?.content ?? event.result.content, + details: nativeResult?.details ?? event.result.details, + }, -+ isError: nativeResult?.isError ?? event.isError, ++ isError: (nativeResult?.isError ?? event.isError) || isToolResultError(event.result), + ...(params.context.runId ? { runId: params.context.runId } : {}), + }, + context: { ...params.context, toolName, toolCallId }, diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/source-contract-results.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/source-contract-results.txt index 37c8a5c..58b76e5 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/source-contract-results.txt +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/source-contract-results.txt @@ -1,9 +1,9 @@ -container=tiangong-m9a0-source-297597_20260823T011824Z +container=tiangong-m9a0-source-399111_20260823T031231Z image=tg-worker:dev image_id=sha256:ac1cb183e5b2c82982f6473fef86ccf128763a31192d711526d843a79edb69ff image_created=2026-08-21T13:00:21.057325912+08:00 openclaw_version=OpenClaw 2026.4.14 (2f35b6f) -patch_sha256=24671f2d21c9b6d2904e10f7e3874981983f12a6a9968cf7d8ff4001c6f93a1c +patch_sha256=eb79f2c2c296f528ec93f1ce38581780b4d17afd566ba10fd3f07333169dd34d checking file src/agents/pi-embedded-runner/compact.ts checking file src/agents/pi-embedded-runner/run/attempt.ts checking file src/agents/pi-embedded-runner/trusted-boundaries.ts @@ -15,13 +15,13 @@ patching file src/plugins/hook-types.ts Checking formatting... All matched files use the correct format. -Finished in 53ms on 6 files using 16 threads. +Finished in 45ms on 6 files using 16 threads. No config found, using defaults. Please add a config file or try `oxfmt --init` if needed. RUN v4.1.4 /opt/openclaw - ✓ src/agents/pi-embedded-runner/trusted-boundaries.provider.test.ts > M9-A0 actual pi-ai provider boundary > sends the trusted payload returned after native provider serialization 52ms - ✓ src/agents/pi-embedded-runner/trusted-boundaries.provider.test.ts > M9-A0 actual pi-ai provider boundary > does not issue a second HTTP request when tool capture fails 24ms + ✓ src/agents/pi-embedded-runner/trusted-boundaries.provider.test.ts > M9-A0 actual pi-ai provider boundary > sends the trusted payload returned after native provider serialization 45ms + ✓ src/agents/pi-embedded-runner/trusted-boundaries.provider.test.ts > M9-A0 actual pi-ai provider boundary > does not issue a second HTTP request when tool capture fails 23ms ✓ src/agents/pi-embedded-runner/trusted-boundaries.provider.test.ts > M9-A0 actual pi-ai provider boundary > makes zero HTTP requests when the trusted handler fails 1ms ✓ src/agents/pi-embedded-runner/trusted-boundaries.test.ts > M9-A0 trusted boundaries > keeps stock behavior disabled but rejects an invalid activation value 2ms ✓ src/agents/pi-embedded-runner/trusted-boundaries.test.ts > M9-A0 trusted boundaries > requires the exact loaded plugin and exactly one handler before installation 1ms @@ -33,8 +33,8 @@ No config found, using defaults. Please add a config file or try `oxfmt --init` Test Files 2 passed (2) Tests 10 passed (10) - Start at 01:18:26 - Duration 2.17s (transform 1.46s, setup 0ms, import 2.31s, tests 95ms, environment 0ms) + Start at 03:12:33 + Duration 1.82s (transform 1.40s, setup 0ms, import 2.24s, tests 86ms, environment 0ms) RUN v4.1.4 /opt/openclaw @@ -43,9 +43,9 @@ No config found, using defaults. Please add a config file or try `oxfmt --init` Test Files 4 passed (4) Tests 156 passed (156) - Start at 01:18:29 - Duration 5.88s (transform 6.44s, setup 0ms, import 7.47s, tests 2.18s, environment 0ms) + Start at 03:12:36 + Duration 4.91s (transform 5.47s, setup 0ms, import 6.31s, tests 1.84s, environment 0ms) -cleanup_owner=tiangong-m9a0-source-297597_20260823T011824Z +cleanup_owner=tiangong-m9a0-source-399111_20260823T031231Z cleanup_container_absent=true m9_a0_source_contract=pass diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/plan.md b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/plan.md index 48d0241..bbfb724 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/plan.md +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/plan.md @@ -1,6 +1,6 @@ # M9-A0 trusted native-boundary follow-up -> Status: in progress; Layer 1 and the repaired Layer 2 main/post-tool path pass, while the remaining Layer 2 cases and Layer 3 are not complete. +> Status: blocked at the Layer 2 compaction stop line; Layer 1, main/tool-error/capture-failure/follow-up cases pass, while compaction bypasses the required final model handler. ## Scope @@ -16,7 +16,7 @@ The predecessor remains a blocked historical result. Its `before_model_call` pla The candidate patch is [`evidence/openclaw-2026.4.14-trusted-native-boundaries.patch`](evidence/openclaw-2026.4.14-trusted-native-boundaries.patch). -- Patch SHA-256: `24671f2d21c9b6d2904e10f7e3874981983f12a6a9968cf7d8ff4001c6f93a1c` +- Patch SHA-256: `eb79f2c2c296f528ec93f1ce38581780b4d17afd566ba10fd3f07333169dd34d` - Upstream license: MIT - Effective research identity: OpenClaw `2026.4.14 (2f35b6f)` plus the exact patch digest above - Patch scope: four OpenClaw source files, including one new boundary helper; no dependency source, schema, table, ledger, or product Worker file @@ -92,6 +92,8 @@ Proceed only after Layer 1 passes. - Exercise the main turn, post-tool turn, retry/follow-up path, and OpenClaw-owned compaction session. - Use the test-only `before_prompt_build` event as the observable attempt-ready condition. Keep the bounded host-startup budget separate from the five-second provider-response budget, and clear both timers after their race. A timeout before readiness is red but is not provider-boundary evidence. +Current execution has passed the main/tool-error/capture-failure/persisted-follow-up cases. The separately created OpenClaw compaction session bypassed `agent.onPayload` through a direct pinned `completeSimple` call, so the supported-path bypass stop condition is red and the serial plan stops here. + ### Layer 3: Tiangong control-handler prototype Proceed only after Layer 2 passes. diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/result.md b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/result.md index fbdae1c..5ed96de 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/result.md +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/result.md @@ -2,92 +2,114 @@ ## Decision -**A0 remains incomplete at Layer 2, but the Layer 2 readiness/adapter-host blocker is repaired.** Layer 1 still passes. The repaired real OpenClaw `runEmbeddedAttempt` harness now passes both an unpatched adjacent baseline and the patched initial/tool/post-tool provider path in the disposable `--network none` image. +**Layer 2 main, normalized tool-error, capture-failure, and persisted follow-up cases pass. A0 stops at the compaction stop line.** -The remaining Layer 2 error/capture-failure, retry/follow-up, and OpenClaw-owned compaction cases have not run through this repaired host harness. Layer 3 therefore remains blocked, and Layer 4 remains unauthorized. +The real OpenClaw-owned compaction path emits one provider request without the trusted payload marker and without invoking the required `before_model_call` handler. This is a supported model-emitting path which bypasses the final handler, so Layer 3 and Layer 4 were not started. -The candidate patch remains a research artifact. No Tiangong runtime file, installed OpenClaw tree, durable provider configuration, database, Matrix resource, or external service was changed. +M9-A formal implementation remains blocked. The candidate remains a research artifact; no Tiangong runtime, installed OpenClaw tree, database, provider service, Matrix resource, or external service was changed. -## Layer 1: source/type/agent-loop contract +## Pinned source contract -**PASS** via [`run-source-contract.sh`](run-source-contract.sh). +**PASS** after the Layer 2 tool-error correction via [`run-source-contract.sh`](run-source-contract.sh). - image: `tg-worker:dev` - image ID: `sha256:ac1cb183e5b2c82982f6473fef86ccf128763a31192d711526d843a79edb69ff` - OpenClaw: `2026.4.14 (2f35b6f)` - network: `none` -- patch SHA-256: `24671f2d21c9b6d2904e10f7e3874981983f12a6a9968cf7d8ff4001c6f93a1c` +- patch SHA-256: `eb79f2c2c296f528ec93f1ce38581780b4d17afd566ba10fd3f07333169dd34d` - boundary/provider tests: **10/10** - adjacent OpenClaw regressions: **156/156** - targeted typecheck, formatting, patch dry-run: passed -- cleanup: owned source-contract container absent +- cleanup container: absent -Direct Layer 1 facts are preserved in [`evidence/source-contract-results.txt`](evidence/source-contract-results.txt). +Direct output: [`evidence/source-contract-results.txt`](evidence/source-contract-results.txt). -## Initial Layer 2 failure and diagnosis +## Layer 2 passing cases -The original harness passed an ad hoc `Model` whose `fake-provider` ID was not registered as an OpenClaw provider. `runEmbeddedAttempt` consequently entered OpenClaw's provider-plugin resolution path before the model request. The direct `AgentSession` diagnostic did not exercise that host contract, which explains why it reached loopback while patched and unpatched attempts did not. +The repaired runner uses the real plugin loader, registered fake provider, actual `runEmbeddedAttempt`, and an owned loopback HTTP provider in `--network none` containers. -The original five-second timer also started before adapter-host initialization. Synchronous plugin/provider initialization delayed the event loop, so the expired timer could abort the attempt as soon as prompt processing became possible. That timer measured host startup rather than the trusted provider boundary. +### Main/tool success -This classifies the initial failure as **test driver plus adapter/host fixture**, with high confidence. It was not a candidate-patch failure. +**PASS** via [`evidence/layer2-readiness-results.txt`](evidence/layer2-readiness-results.txt). -## Repair +- unpatched registered-provider baseline: **1/1**; +- patched actual attempt: **1/1**; +- provider requests: **2**; +- tool executions: **1**; +- event order: `attempt-ready → model-handler → tool-executed → tool-handler → model-handler`; +- both provider requests carried the trusted payload marker. -The repaired harness: +### Normalized tool error -1. declares `fake-provider` in the test plugin manifest and registers it through the real plugin API; -2. loads the plugin through the real OpenClaw loader; -3. uses a test-only `before_prompt_build` event as the observable attempt-ready fact; -4. separates a bounded host-startup budget from the five-second provider-response budget; -5. clears race timers and process listeners during teardown; -6. runs in one exactly named container and proves its removal. +**PASS** via [`evidence/layer2-tool-error.test.ts`](evidence/layer2-tool-error.test.ts). -The first invocation of the new runner stopped at its formatting gate before tests. Only formatting changed before the successful rerun. +The first diagnostic exposed a real adapter detail: OpenClaw's `toToolDefinitions` catches a thrown tool error and returns a normalized result with `details.status="error"`, while Pi's `afterToolCall` `isError` remained false. The candidate seam now ORs the existing OpenClaw `isToolResultError(event.result)` classification into the trusted release event. Layer 1 was rerun after this correction and passed. -## Repaired Layer 2 observations +Direct facts after correction: -**PASS for the executed main and post-tool cells** via [`run-layer2-readiness.sh`](run-layer2-readiness.sh). +- provider requests: **2**; +- tool executions: **1**; +- trusted release handler saw `isError=true`; +- the normalized error was captured before the follow-up provider request. -- final owned container: `tiangong-m9a0-layer2-374508_20260823T024326Z` -- image, image ID, OpenClaw version, patch digest, and network mode: unchanged from Layer 1 -- unpatched registered-provider baseline: **1/1 passed** -- patched actual `runEmbeddedAttempt` tool turn: **1/1 passed** -- selected path: boundary-aware `openai-completions` HTTP transport to the owned loopback provider -- provider requests in patched case: **2** -- synthetic tool executions: **1** -- trusted event order asserted by the test: +### Capture failure -```text -attempt-ready -model-handler -tool-executed -tool-handler -model-handler -``` +**PASS** via [`evidence/layer2-capture-failure.test.ts`](evidence/layer2-capture-failure.test.ts). -Both provider request bodies carried the payload marker returned by the trusted model handler. The second request occurred only after the normalized tool outcome passed the trusted release handler. This proves the initial request, successful tool release, and post-tool request on the actual patched attempt path. +- provider requests: **1**; +- tool executions: **1**; +- no next model handler; +- no `onToolResult` callback; +- no persisted `toolResult` message; +- capture handler failure therefore stops release before ordinary ToolResult emission and before the next provider request. -Direct output is in [`evidence/layer2-readiness-results.txt`](evidence/layer2-readiness-results.txt). The repaired tests are: +### Persisted follow-up -- [`evidence/layer2-run-embedded-attempt-baseline.test.ts`](evidence/layer2-run-embedded-attempt-baseline.test.ts) -- [`evidence/layer2-run-embedded-attempt.test.ts`](evidence/layer2-run-embedded-attempt.test.ts) +**PASS** via [`evidence/layer2-followup.test.ts`](evidence/layer2-followup.test.ts). -## Remaining work +Two real `runEmbeddedAttempt` invocations used the same persisted session and plugin/provider setup: -Layer 2 is not yet complete. Do not claim an A0 pass until the repaired harness also covers: +- provider requests: **2**; +- model handlers: **2**; +- event order: `attempt-ready → model-handler → attempt-ready → model-handler`; +- persisted user messages: **2**. -- normalized tool error; -- trusted capture failure with no ToolResult release or next provider request; -- retry or persisted follow-up path; -- separately created OpenClaw compaction session through the same model boundary. +This proves the final model seam is reinstalled on the persisted-session follow-up path. -Layer 3 and Matrix were not started. M9-A formal implementation remains blocked by the incomplete spike, not by the former readiness failure. +## Compaction stop line -## Cleanup +**BLOCKED** via [`evidence/layer2-compaction.test.ts`](evidence/layer2-compaction.test.ts). + +The diagnostic creates an actual OpenClaw compaction session through `compactEmbeddedPiSessionDirect`, using the registered provider and a real local provider request. Compaction itself reports `ok=true` and `compacted=true`, but the direct provider facts are: + +- provider requests: **1**; +- trusted payload marker: absent; +- `before_model_call` handler events: **0**; +- compaction entry: persisted. + +Source inspection identifies the bypass: pinned `@mariozechner/pi-coding-agent` compaction calls `completeSimple(...)` directly. Installing `agent.onPayload` on the separately created session cannot intercept that direct `completeSimple` call. The initial assertion-failing diagnostic and the passing bypass diagnostic are preserved as bounded evidence; this is not converted into a success claim. + +This meets the A0 stop condition for a supported model-emitting path bypassing the required final handler. Fixing it would require a separately reviewed upstream seam or a dependency/internal compaction change, not a prompt, Skill, Matrix, or silent upgrade workaround. -The final runner removed its exact owned container and reported `cleanup_container_absent=true`. +## Not started -Several early diagnostic commands were externally timed out before their shell traps completed, leaving seven exited, exactly named diagnostic containers. They were discovered by explicit inspection, removed by exact name, and rechecked individually. [`evidence/layer2-diagnostic-cleanup.txt`](evidence/layer2-diagnostic-cleanup.txt) records each absence and confirms that no container with the owned `tiangong-m9a0-` prefix remains. +Because the compaction stop line is red: + +- Layer 3 Tiangong control-handler prototype: **not started**; +- Basic Matrix Layer 4: **not authorized**; +- no further retry/compaction variants were run; +- M9-A formal implementation remains blocked. + +## Evidence index + +- [`evidence/layer2-remaining-results.txt`](evidence/layer2-remaining-results.txt) +- [`evidence/layer2-readiness-results.txt`](evidence/layer2-readiness-results.txt) +- [`evidence/layer2-tool-error.test.ts`](evidence/layer2-tool-error.test.ts) +- [`evidence/layer2-capture-failure.test.ts`](evidence/layer2-capture-failure.test.ts) +- [`evidence/layer2-followup.test.ts`](evidence/layer2-followup.test.ts) +- [`evidence/layer2-compaction.test.ts`](evidence/layer2-compaction.test.ts) +- [`evidence/source-contract-results.txt`](evidence/source-contract-results.txt) + +## Cleanup -No external resource or credential was created. +The exact test containers recorded in the evidence were removed and individually verified absent. A final Docker inspection also verified that no exited or running container with the owned `tiangong-m9a0-` prefix remains. Temporary test roots and provider servers were removed by test teardown. No external resource or credential was created. diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer2-readiness.sh b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer2-readiness.sh index 8adcdd3..02ff22e 100755 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer2-readiness.sh +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer2-readiness.sh @@ -4,7 +4,7 @@ set -euo pipefail RUN_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" IMAGE="${M9_A0_IMAGE:-tg-worker:dev}" PATCH="$RUN_DIR/evidence/openclaw-2026.4.14-trusted-native-boundaries.patch" -EXPECTED_PATCH_SHA256="24671f2d21c9b6d2904e10f7e3874981983f12a6a9968cf7d8ff4001c6f93a1c" +EXPECTED_PATCH_SHA256="eb79f2c2c296f528ec93f1ce38581780b4d17afd566ba10fd3f07333169dd34d" EXPECTED_VERSION="OpenClaw 2026.4.14 (2f35b6f)" CONTAINER="tiangong-m9a0-layer2-$$_$(date -u +%Y%m%dT%H%M%SZ)" diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-source-contract.sh b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-source-contract.sh index 011766e..4d490e1 100755 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-source-contract.sh +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-source-contract.sh @@ -4,7 +4,7 @@ set -euo pipefail RUN_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" IMAGE="${M9_A0_IMAGE:-tg-worker:dev}" PATCH="$RUN_DIR/evidence/openclaw-2026.4.14-trusted-native-boundaries.patch" -EXPECTED_PATCH_SHA256="24671f2d21c9b6d2904e10f7e3874981983f12a6a9968cf7d8ff4001c6f93a1c" +EXPECTED_PATCH_SHA256="eb79f2c2c296f528ec93f1ce38581780b4d17afd566ba10fd3f07333169dd34d" EXPECTED_VERSION="OpenClaw 2026.4.14 (2f35b6f)" CONTAINER="tiangong-m9a0-source-$$_$(date -u +%Y%m%dT%H%M%SZ)" From 362a36db5fd2a3547890de56eeb449999cd1215b Mon Sep 17 00:00:00 2001 From: Jay Shen Date: Sun, 23 Aug 2026 14:10:42 +0800 Subject: [PATCH 07/13] test: fail closed unsafe M9-A0 compaction Signed-off-by: Jay Shen --- .../evidence/artifact-sha256.txt | 18 +- ...ayer2-compaction-bypass-diagnostic.test.ts | 233 ++++++++++++++++++ .../evidence/layer2-compaction.test.ts | 201 +++++++++------ .../evidence/layer2-diagnostic-cleanup.txt | 8 +- .../evidence/layer2-pre-guard-results.txt | 76 ++++++ .../evidence/layer2-readiness-results.txt | 40 +-- .../evidence/layer2-remaining-results.txt | 153 ++++++------ .../evidence/layer2-results.txt | 57 +++-- ...-2026.4.14-trusted-native-boundaries.patch | 223 ++++++++++++++--- .../evidence/source-contract-results.txt | 57 +++-- .../evidence/trusted-boundaries.test.ts | 14 ++ .../plan.md | 78 +++--- .../result.md | 93 ++++--- .../run-layer2-readiness.sh | 2 +- .../run-layer2-remaining.sh | 79 ++++++ .../run-source-contract.sh | 6 +- 16 files changed, 986 insertions(+), 352 deletions(-) create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-compaction-bypass-diagnostic.test.ts create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-pre-guard-results.txt create mode 100755 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer2-remaining.sh diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/artifact-sha256.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/artifact-sha256.txt index 73c02bc..d045379 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/artifact-sha256.txt +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/artifact-sha256.txt @@ -1,15 +1,17 @@ 0fce0d9bddbd72c4fd836b5dfc875188ed2accf91f0c242d545f68baa8ef0db4 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-agent-session-readiness.test.ts 6c6fda442f7f73d00442b837434fef2498da4f33622e3542c86a7dd174482d27 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-capture-failure.test.ts -964cda358583d9d33a3ac30b9b7a68d5bc14dfd3dd56ae79c6bbec0f205783ae smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-compaction.test.ts -e10261404eae34700a32346176109cb41266bcfd86e1e643356340d8eb904ce6 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-diagnostic-cleanup.txt +964cda358583d9d33a3ac30b9b7a68d5bc14dfd3dd56ae79c6bbec0f205783ae smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-compaction-bypass-diagnostic.test.ts +122c58c415311e718a137e9603c289f9e4d484b4b4fa63e00dfab9ba00fcdef5 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-compaction.test.ts +6c719cc515ddeae5ec646a3c8372a18dd1d3a594e105ac62ece9d0e277d400ca smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-diagnostic-cleanup.txt 8665a959aa74c809bc624720a042ffb9b77a1a9e3644afb8ecc806342d78dbbb smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-followup.test.ts -cec18acc562452d9ca8b6568646a89872a84e195ce0081361eaa93786f6f23a6 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-readiness-results.txt -89d904dd891687c94c7b78297d9c3063e2faaf7f8595647f10e540510a0545e9 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-remaining-results.txt -4a9e36e2dbbf657bf7d2c1cf1618d88db7fa2f3e7ee4cc8f6fe8ac405bb7e071 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-results.txt +89d904dd891687c94c7b78297d9c3063e2faaf7f8595647f10e540510a0545e9 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-pre-guard-results.txt +de4f2cb56f30ffb356ed66234af7f57f015d9e475e4ea955fea3bc6de9b5bef8 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-readiness-results.txt +02400807cd7fff6a11a7bbc6c50da2bb5a2eff0b677871c8cb9627492c5510a5 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-remaining-results.txt +4ed1e2baa6a8a575d13d01686f1aff8ce54a4cff34b69ad6e53a466b6767c6f5 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-results.txt 9c580e8435bb6aae227e9af6da51f6d7563670779adaeec29cd2867c6f911c07 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-run-embedded-attempt-baseline.test.ts 06a77efe011681fd7baea23bf3b993589ecf5268a40839dad8dd54916461ed1c smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-run-embedded-attempt.test.ts 2b4090ada287bd20eb3b6ea45365ee065a023f72b35f2701da8060e4350efcb3 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-tool-error.test.ts -eb79f2c2c296f528ec93f1ce38581780b4d17afd566ba10fd3f07333169dd34d smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/openclaw-2026.4.14-trusted-native-boundaries.patch -52fcc896751cbbda796e3c7a78a6b5abf3f81748c4d4a37f4c434040016e7376 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/source-contract-results.txt +3c81b8333aa23772989763a2328396e3132d424ce438f81e841f9e3bfc6019eb smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/openclaw-2026.4.14-trusted-native-boundaries.patch +62bc7f13b5f19bff177597e2a26c20f1aa7ae36a12f317eba4aa611612d0eacd smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/source-contract-results.txt dcbff559f4cdf28bc2118de22edbbd5f228dabfe7e69afeae1a2354cb2480ed5 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/trusted-boundaries.provider.test.ts -d6d9ff6355162a4a5e9e345010c04aa189738a487d23b949881ed30e72596548 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/trusted-boundaries.test.ts +9837af7e6ad2e3aa87391fb51c98c690e75af7144bc520d1b01a26a0611c3221 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/trusted-boundaries.test.ts diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-compaction-bypass-diagnostic.test.ts b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-compaction-bypass-diagnostic.test.ts new file mode 100644 index 0000000..36b6bbb --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-compaction-bypass-diagnostic.test.ts @@ -0,0 +1,233 @@ +import fs from "node:fs/promises"; +import http from "node:http"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { SessionManager } from "@mariozechner/pi-coding-agent"; +import { clearPluginLoaderCache, loadOpenClawPlugins } from "../../plugins/loader.js"; +import { resetGlobalHookRunner } from "../../plugins/hook-runner-global.js"; +import { compactEmbeddedPiSessionDirect } from "./compact.js"; + +const roots: string[] = []; +const servers: http.Server[] = []; + +async function writePlugin(root: string, eventsFile: string): Promise { + const plugin = path.join(root, "tiangong-control.cjs"); + await Promise.all([ + fs.writeFile( + path.join(root, "openclaw.plugin.json"), + JSON.stringify({ + id: "tiangong-control", + providers: ["fake-provider"], + configSchema: { type: "object", properties: {} }, + }), + "utf8", + ), + fs.writeFile( + plugin, + `const fs = require("node:fs"); +function emit(event, fields) { + fs.appendFileSync(${JSON.stringify(eventsFile)}, JSON.stringify({ event, ...fields }) + "\\n"); +} +module.exports = { + id: "tiangong-control", + register(api) { + api.registerProvider({ id: "fake-provider", label: "M9 A0 fake provider", auth: [] }); + api.on("before_model_call", async (event) => { + emit("compaction-model-handler", { provider: event.provider, model: event.model }); + return { allow: true, payload: { ...event.payload, m9Trusted: "allowed" } }; + }); + api.on("before_tool_result_release", async () => ({ release: true })); + }, +}; +`, + "utf8", + ), + ]); + return plugin; +} + +async function listenProvider(): Promise<{ + baseUrl: string; + requestBodies: Array>; + close: () => Promise; +}> { + const requestBodies: Array> = []; + const server = http.createServer(async (request, response) => { + const chunks: Buffer[] = []; + for await (const chunk of request) chunks.push(Buffer.from(chunk)); + requestBodies.push( + JSON.parse(Buffer.concat(chunks).toString("utf8")) as Record, + ); + response.writeHead(200, { + "content-type": "text/event-stream", + connection: "keep-alive", + "cache-control": "no-cache", + }); + const id = `m9-a0-compaction-${requestBodies.length}`; + response.write( + `data: ${JSON.stringify({ + id, + object: "chat.completion.chunk", + created: 1, + model: "fake-model", + choices: [ + { + index: 0, + delta: { role: "assistant", content: "compacted summary" }, + finish_reason: null, + }, + ], + })}\n\n`, + ); + response.write( + `data: ${JSON.stringify({ + id, + object: "chat.completion.chunk", + created: 1, + model: "fake-model", + choices: [{ index: 0, delta: {}, finish_reason: "stop" }], + })}\n\n`, + ); + response.end("data: [DONE]\n\n"); + }); + servers.push(server); + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", resolve); + }); + const address = server.address(); + if (!address || typeof address === "string") throw new Error("FAKE_PROVIDER_ADDRESS_MISSING"); + return { + baseUrl: `http://127.0.0.1:${address.port}/v1`, + requestBodies, + close: async () => { + await new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())), + ); + }, + }; +} + +function appendConversation(sessionFile: string): void { + const manager = SessionManager.open(sessionFile); + manager.appendMessage({ + role: "user", + content: [{ type: "text", text: "old user turn" }], + timestamp: Date.now(), + } as never); + manager.appendMessage({ + role: "assistant", + content: [{ type: "text", text: "old assistant turn" }], + api: "openai-completions", + provider: "fake-provider", + model: "fake-model", + usage: { + input: 10, + output: 4, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 14, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, + }, + stopReason: "stop", + timestamp: Date.now(), + } as never); +} + +afterEach(async () => { + resetGlobalHookRunner(); + clearPluginLoaderCache(); + for (const server of servers.splice(0)) { + if (server.listening) await new Promise((resolve) => server.close(() => resolve())); + } + while (roots.length > 0) await fs.rm(roots.pop()!, { recursive: true, force: true }); +}); + +describe("M9-A0 Layer 2 OpenClaw-owned compaction diagnostic", () => { + it("proves the current compaction path bypasses the trusted model boundary", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "m9-a0-compaction-")); + roots.push(root); + const eventsFile = path.join(root, "events.ndjson"); + const pluginFile = await writePlugin(root, eventsFile); + const provider = await listenProvider(); + const sessionFile = path.join(root, "session.jsonl"); + appendConversation(sessionFile); + const config = { + plugins: { + allow: ["tiangong-control"], + load: { paths: [pluginFile] }, + }, + models: { + providers: { + "fake-provider": { + baseUrl: provider.baseUrl, + api: "openai-completions", + apiKey: "fixture-provider-value", + models: [ + { + id: "fake-model", + name: "M9 A0 fake model", + api: "openai-completions", + input: ["text"], + reasoning: false, + contextWindow: 32768, + maxTokens: 256, + }, + ], + }, + }, + }, + }; + const registry = loadOpenClawPlugins({ cache: false, workspaceDir: root, config }); + expect(registry.plugins.find((plugin) => plugin.id === "tiangong-control")?.status).toBe( + "loaded", + ); + expect( + registry.providers.filter((entry) => entry.provider.id === "fake-provider"), + ).toHaveLength(1); + expect( + registry.typedHooks.filter((hook) => hook.hookName === "before_model_call"), + ).toHaveLength(1); + expect( + registry.typedHooks.filter((hook) => hook.hookName === "before_tool_result_release"), + ).toHaveLength(1); + + const previousRequired = process.env.TIANGONG_TRUSTED_BOUNDARIES_REQUIRED; + process.env.TIANGONG_TRUSTED_BOUNDARIES_REQUIRED = "1"; + try { + const result = await compactEmbeddedPiSessionDirect({ + sessionId: "layer2-compaction-session", + runId: "layer2-compaction-run", + sessionKey: "agent:layer2:compaction", + sessionFile, + workspaceDir: root, + agentDir: root, + config, + provider: "fake-provider", + model: "fake-model", + thinkLevel: "off", + force: true, + trigger: "manual", + }); + expect(result.ok).toBe(true); + expect(result.compacted).toBe(true); + expect(provider.requestBodies).toHaveLength(1); + expect(provider.requestBodies[0]?.m9Trusted).not.toBe("allowed"); + const events = (await fs.readFile(eventsFile, "utf8").catch(() => "")) + .trim() + .split("\n") + .filter(Boolean) + .map((line) => (JSON.parse(line) as { event: string }).event); + expect(events).toEqual([]); + const entries = (await fs.readFile(sessionFile, "utf8")) + .split("\n") + .filter(Boolean) + .map((line) => JSON.parse(line) as { type?: string }); + expect(entries.some((entry) => entry.type === "compaction")).toBe(true); + } finally { + if (previousRequired === undefined) delete process.env.TIANGONG_TRUSTED_BOUNDARIES_REQUIRED; + else process.env.TIANGONG_TRUSTED_BOUNDARIES_REQUIRED = previousRequired; + } + }, 180_000); +}); diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-compaction.test.ts b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-compaction.test.ts index 36b6bbb..3f3787a 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-compaction.test.ts +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-compaction.test.ts @@ -2,11 +2,13 @@ import fs from "node:fs/promises"; import http from "node:http"; import os from "node:os"; import path from "node:path"; -import { afterEach, describe, expect, it } from "vitest"; import { SessionManager } from "@mariozechner/pi-coding-agent"; +import { afterEach, describe, expect, it } from "vitest"; import { clearPluginLoaderCache, loadOpenClawPlugins } from "../../plugins/loader.js"; import { resetGlobalHookRunner } from "../../plugins/hook-runner-global.js"; import { compactEmbeddedPiSessionDirect } from "./compact.js"; +import { compactEmbeddedPiSession } from "./compact.queued.js"; +import { TIANGONG_MODEL_COMPACTION_DISABLED_REASON } from "./trusted-boundaries.js"; const roots: string[] = []; const servers: http.Server[] = []; @@ -50,7 +52,6 @@ module.exports = { async function listenProvider(): Promise<{ baseUrl: string; requestBodies: Array>; - close: () => Promise; }> { const requestBodies: Array> = []; const server = http.createServer(async (request, response) => { @@ -101,11 +102,6 @@ async function listenProvider(): Promise<{ return { baseUrl: `http://127.0.0.1:${address.port}/v1`, requestBodies, - close: async () => { - await new Promise((resolve, reject) => - server.close((error) => (error ? reject(error) : resolve())), - ); - }, }; } @@ -135,6 +131,87 @@ function appendConversation(sessionFile: string): void { } as never); } +async function prepareFixture(): Promise<{ + root: string; + eventsFile: string; + sessionFile: string; + config: Record; + requestBodies: Array>; +}> { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "m9-a0-compaction-")); + roots.push(root); + const eventsFile = path.join(root, "events.ndjson"); + const pluginFile = await writePlugin(root, eventsFile); + const provider = await listenProvider(); + const sessionFile = path.join(root, "session.jsonl"); + appendConversation(sessionFile); + const config = { + plugins: { + allow: ["tiangong-control"], + load: { paths: [pluginFile] }, + }, + models: { + providers: { + "fake-provider": { + baseUrl: provider.baseUrl, + api: "openai-completions", + apiKey: "fixture-provider-value", + models: [ + { + id: "fake-model", + name: "M9 A0 fake model", + api: "openai-completions", + input: ["text"], + reasoning: false, + contextWindow: 32768, + maxTokens: 256, + }, + ], + }, + }, + }, + }; + const registry = loadOpenClawPlugins({ cache: false, workspaceDir: root, config }); + expect(registry.plugins.find((plugin) => plugin.id === "tiangong-control")?.status).toBe( + "loaded", + ); + expect(registry.providers.filter((entry) => entry.provider.id === "fake-provider")).toHaveLength( + 1, + ); + expect(registry.typedHooks.filter((hook) => hook.hookName === "before_model_call")).toHaveLength( + 1, + ); + return { root, eventsFile, sessionFile, config, requestBodies: provider.requestBodies }; +} + +function compactionParams( + fixture: Awaited>, + trigger: "budget" | "overflow" | "manual", +) { + return { + sessionId: `layer2-compaction-${trigger}`, + runId: `layer2-compaction-${trigger}-run`, + sessionKey: `agent:layer2:compaction:${trigger}`, + sessionFile: fixture.sessionFile, + workspaceDir: fixture.root, + agentDir: fixture.root, + config: fixture.config as never, + provider: "fake-provider", + model: "fake-model", + thinkLevel: "off" as const, + force: true, + trigger, + }; +} + +async function readEvents(eventsFile: string): Promise { + return (await fs.readFile(eventsFile, "utf8").catch(() => "")) + .trim() + .split("\n") + .filter(Boolean) + .map((line) => (JSON.parse(line) as { event: string }).event); +} + afterEach(async () => { resetGlobalHookRunner(); clearPluginLoaderCache(); @@ -144,83 +221,49 @@ afterEach(async () => { while (roots.length > 0) await fs.rm(roots.pop()!, { recursive: true, force: true }); }); -describe("M9-A0 Layer 2 OpenClaw-owned compaction diagnostic", () => { - it("proves the current compaction path bypasses the trusted model boundary", async () => { - const root = await fs.mkdtemp(path.join(os.tmpdir(), "m9-a0-compaction-")); - roots.push(root); - const eventsFile = path.join(root, "events.ndjson"); - const pluginFile = await writePlugin(root, eventsFile); - const provider = await listenProvider(); - const sessionFile = path.join(root, "session.jsonl"); - appendConversation(sessionFile); - const config = { - plugins: { - allow: ["tiangong-control"], - load: { paths: [pluginFile] }, - }, - models: { - providers: { - "fake-provider": { - baseUrl: provider.baseUrl, - api: "openai-completions", - apiKey: "fixture-provider-value", - models: [ - { - id: "fake-model", - name: "M9 A0 fake model", - api: "openai-completions", - input: ["text"], - reasoning: false, - contextWindow: 32768, - maxTokens: 256, - }, - ], - }, - }, - }, - }; - const registry = loadOpenClawPlugins({ cache: false, workspaceDir: root, config }); - expect(registry.plugins.find((plugin) => plugin.id === "tiangong-control")?.status).toBe( - "loaded", - ); - expect( - registry.providers.filter((entry) => entry.provider.id === "fake-provider"), - ).toHaveLength(1); - expect( - registry.typedHooks.filter((hook) => hook.hookName === "before_model_call"), - ).toHaveLength(1); - expect( - registry.typedHooks.filter((hook) => hook.hookName === "before_tool_result_release"), - ).toHaveLength(1); - +describe("M9-A0 Layer 2 model-compaction guard", () => { + it("denies manual, budget, and overflow compaction before provider or session mutation", async () => { + const fixture = await prepareFixture(); + const sessionBefore = await fs.readFile(fixture.sessionFile, "utf8"); const previousRequired = process.env.TIANGONG_TRUSTED_BOUNDARIES_REQUIRED; process.env.TIANGONG_TRUSTED_BOUNDARIES_REQUIRED = "1"; try { - const result = await compactEmbeddedPiSessionDirect({ - sessionId: "layer2-compaction-session", - runId: "layer2-compaction-run", - sessionKey: "agent:layer2:compaction", - sessionFile, - workspaceDir: root, - agentDir: root, - config, - provider: "fake-provider", - model: "fake-model", - thinkLevel: "off", - force: true, - trigger: "manual", - }); - expect(result.ok).toBe(true); - expect(result.compacted).toBe(true); - expect(provider.requestBodies).toHaveLength(1); - expect(provider.requestBodies[0]?.m9Trusted).not.toBe("allowed"); - const events = (await fs.readFile(eventsFile, "utf8").catch(() => "")) - .trim() + const denied = { + ok: false, + compacted: false, + reason: TIANGONG_MODEL_COMPACTION_DISABLED_REASON, + }; + for (const trigger of ["manual", "budget", "overflow"] as const) { + const result = await compactEmbeddedPiSessionDirect(compactionParams(fixture, trigger)); + expect(result).toEqual(denied); + } + expect(await compactEmbeddedPiSession(compactionParams(fixture, "manual"))).toEqual(denied); + expect(fixture.requestBodies).toEqual([]); + expect(await readEvents(fixture.eventsFile)).toEqual([]); + expect(await fs.readFile(fixture.sessionFile, "utf8")).toBe(sessionBefore); + const entries = sessionBefore .split("\n") .filter(Boolean) - .map((line) => (JSON.parse(line) as { event: string }).event); - expect(events).toEqual([]); - const entries = (await fs.readFile(sessionFile, "utf8")) + .map((line) => JSON.parse(line) as { type?: string }); + expect(entries.some((entry) => entry.type === "compaction")).toBe(false); + } finally { + if (previousRequired === undefined) delete process.env.TIANGONG_TRUSTED_BOUNDARIES_REQUIRED; + else process.env.TIANGONG_TRUSTED_BOUNDARIES_REQUIRED = previousRequired; + } + }, 180_000); + + it("keeps stock compaction behavior when required mode is absent", async () => { + const fixture = await prepareFixture(); + const previousRequired = process.env.TIANGONG_TRUSTED_BOUNDARIES_REQUIRED; + delete process.env.TIANGONG_TRUSTED_BOUNDARIES_REQUIRED; + try { + const result = await compactEmbeddedPiSessionDirect(compactionParams(fixture, "manual")); + expect(result.ok).toBe(true); + expect(result.compacted).toBe(true); + expect(fixture.requestBodies).toHaveLength(1); + expect(fixture.requestBodies[0]?.m9Trusted).not.toBe("allowed"); + expect(await readEvents(fixture.eventsFile)).toEqual([]); + const entries = (await fs.readFile(fixture.sessionFile, "utf8")) .split("\n") .filter(Boolean) .map((line) => JSON.parse(line) as { type?: string }); diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-diagnostic-cleanup.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-diagnostic-cleanup.txt index f0bcd91..109b1be 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-diagnostic-cleanup.txt +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-diagnostic-cleanup.txt @@ -1,8 +1,2 @@ -container_absent[tiangong-m9a0-provider-diag-352362_20260823T022015Z]=true -container_absent[tiangong-m9a0-plugins-off-351963_20260823T021911Z]=true -container_absent[tiangong-m9a0-openai-diag-349977_20260823T021738Z]=true -container_absent[tiangong-m9a0-openai-349504_20260823T021645Z]=true -container_absent[tiangong-m9a0-diag-347936_20260823T021516Z]=true -container_absent[tiangong-m9a0-diag-347271_20260823T021339Z]=true -container_absent[tiangong-m9a0-diag-345061_20260823T021117Z]=true +checked_at=2026-08-23T06:04:42Z m9_a0_owned_container_prefix_absent=true diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-pre-guard-results.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-pre-guard-results.txt new file mode 100644 index 0000000..1a7c8a5 --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-pre-guard-results.txt @@ -0,0 +1,76 @@ +layer=2-remaining +status=blocked_at_compaction_stop_line +image=tg-worker:dev +image_id=sha256:ac1cb183e5b2c82982f6473fef86ccf128763a31192d711526d843a79edb69ff +openclaw=OpenClaw 2026.4.14 (2f35b6f) +network=none +patch_sha256=eb79f2c2c296f528ec93f1ce38581780b4d17afd566ba10fd3f07333169dd34d +patch_bytes=10942 +upstream_license=MIT +baseline_tiangong_commit=992665f +activation=TIANGONG_TRUSTED_BOUNDARIES_REQUIRED=1 +provider=owned_loopback_http_fake_provider + +main_readiness_runner=pass +main_readiness_container=tiangong-m9a0-layer2-422961_20260823T033559Z +unpatched_registered_provider_baseline=1/1 +patched_initial_tool_post_tool=1/1 +patched_provider_requests=2 +patched_tool_executions=1 +patched_event_order=attempt-ready,model-handler,tool-executed,tool-handler,model-handler +main_readiness_cleanup_container_absent=true + +normalized_tool_error=pass +normalized_tool_error_container=tiangong-m9a0-layer2-tool-error-fixed-399791_20260823T031300Z +normalized_tool_error_provider_requests=2 +normalized_tool_error_tool_executions=1 +normalized_tool_error_handler_isError=true +normalized_tool_error_event_order=attempt-ready,model-handler,tool-executed,tool-handler,model-handler +normalized_tool_error_cleanup_container_absent=true + +capture_failure=pass +capture_failure_container=tiangong-m9a0-layer2-capture-failure-fixed-404122_20260823T031624Z +capture_failure_provider_requests=1 +capture_failure_tool_executions=1 +capture_failure_next_model_handler=not_observed +capture_failure_tool_result_callback=not_observed +capture_failure_session_tool_result_messages=0 +capture_failure_cleanup_container_absent=true + +persisted_follow_up=pass +persisted_follow_up_container=tiangong-m9a0-layer2-followup-407234_20260823T032018Z +persisted_follow_up_provider_requests=2 +persisted_follow_up_model_handlers=2 +persisted_follow_up_event_order=attempt-ready,model-handler,attempt-ready,model-handler +persisted_follow_up_session_user_messages=2 +persisted_follow_up_cleanup_container_absent=true + +compaction_boundary=blocked +compaction_diagnostic_container=tiangong-m9a0-layer2-compaction-diag2-421077_20260823T033407Z +compaction_result_ok=true +compaction_result_compacted=true +compaction_provider_requests=1 +compaction_trusted_marker_in_provider_body=absent +compaction_before_model_call_handler_events=0 +compaction_entry_persisted=true +compaction_cleanup_container_absent=true +compaction_direct_fact=pi-coding-agent compaction calls completeSimple directly; the OpenClaw agent.onPayload seam is not on that call path + +initial_tool_error_diagnostic=source-failure-led-to-source-correction +initial_tool_error_container=tiangong-m9a0-layer2-tool-error-385530_20260823T025755Z +initial_tool_error_observation=OpenClaw normalized a thrown tool error to details.status=error while pi afterToolCall isError remained false +initial_tool_error_assertion=received_false_expected_true +initial_tool_error_cleanup_container_absent=true + +source_contract_after_tool_error_correction=pass +source_contract_container=tiangong-m9a0-source-399111_20260823T031231Z +source_contract_boundary_tests=10/10 +source_contract_adjacent_regressions=156/156 +source_contract_cleanup_container_absent=true + +stop_reason=supported_compaction_model_emitting_path_bypasses_required_final_handler +layer3=not_started +layer4_basic_matrix=not_started +external_resources=none +credentials=none +owned_container_prefix_absent=true diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-readiness-results.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-readiness-results.txt index c255517..c245059 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-readiness-results.txt +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-readiness-results.txt @@ -1,47 +1,51 @@ -container=tiangong-m9a0-layer2-422961_20260823T033559Z +container=tiangong-m9a0-layer2-531499_20260823T055810Z image=tg-worker:dev image_id=sha256:ac1cb183e5b2c82982f6473fef86ccf128763a31192d711526d843a79edb69ff image_created=2026-08-21T13:00:21.057325912+08:00 openclaw_version=OpenClaw 2026.4.14 (2f35b6f) -patch_sha256=eb79f2c2c296f528ec93f1ce38581780b4d17afd566ba10fd3f07333169dd34d +patch_sha256=3c81b8333aa23772989763a2328396e3132d424ce438f81e841f9e3bfc6019eb Checking formatting... No config found, using defaults. Please add a config file or try `oxfmt --init` if needed. All matched files use the correct format. -Finished in 46ms on 1 files using 16 threads. +Finished in 40ms on 1 files using 16 threads. - RUN v4.1.4 /opt/openclaw + RUN  v4.1.4 /opt/openclaw - ✓ src/agents/layer2-run-embedded-attempt-baseline.test.ts > M9-A0 unpatched runEmbeddedAttempt baseline > reaches a registered local provider after observable attempt readiness 31775ms + ✓ src/agents/layer2-run-embedded-attempt-baseline.test.ts > M9-A0 unpatched runEmbeddedAttempt baseline > reaches a registered local provider after observable attempt readiness 31900ms - Test Files 1 passed (1) - Tests 1 passed (1) - Start at 03:36:00 - Duration 37.33s (transform 3.83s, setup 0ms, import 5.48s, tests 31.78s, environment 0ms) + Test Files  1 passed (1) + Tests  1 passed (1) + Start at  05:58:11 + Duration  37.23s (transform 3.63s, setup 0ms, import 5.25s, tests 31.90s, environment 0ms) +checking file src/agents/pi-embedded-runner/compact.queued.ts checking file src/agents/pi-embedded-runner/compact.ts +checking file src/agents/pi-embedded-runner/run.ts checking file src/agents/pi-embedded-runner/run/attempt.ts checking file src/agents/pi-embedded-runner/trusted-boundaries.ts checking file src/plugins/hook-types.ts +patching file src/agents/pi-embedded-runner/compact.queued.ts patching file src/agents/pi-embedded-runner/compact.ts +patching file src/agents/pi-embedded-runner/run.ts patching file src/agents/pi-embedded-runner/run/attempt.ts patching file src/agents/pi-embedded-runner/trusted-boundaries.ts patching file src/plugins/hook-types.ts Checking formatting... -All matched files use the correct format. -Finished in 40ms on 1 files using 16 threads. No config found, using defaults. Please add a config file or try `oxfmt --init` if needed. +All matched files use the correct format. +Finished in 38ms on 1 files using 16 threads. - RUN v4.1.4 /opt/openclaw + RUN  v4.1.4 /opt/openclaw - ✓ src/agents/pi-embedded-runner/layer2-run-embedded-attempt.test.ts > M9-A0 Layer 2 actual runEmbeddedAttempt tool turn > gates the initial request, tool release, and post-tool request 79054ms + ✓ src/agents/pi-embedded-runner/layer2-run-embedded-attempt.test.ts > M9-A0 Layer 2 actual runEmbeddedAttempt tool turn > gates the initial request, tool release, and post-tool request 79768ms - Test Files 1 passed (1) - Tests 1 passed (1) - Start at 03:36:37 - Duration 84.46s (transform 3.69s, setup 0ms, import 5.33s, tests 79.06s, environment 0ms) + Test Files  1 passed (1) + Tests  1 passed (1) + Start at  05:58:49 + Duration  85.14s (transform 3.67s, setup 0ms, import 5.30s, tests 79.77s, environment 0ms) -cleanup_owner=tiangong-m9a0-layer2-422961_20260823T033559Z +cleanup_owner=tiangong-m9a0-layer2-531499_20260823T055810Z cleanup_container_absent=true layer2_readiness_exit=0 diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-remaining-results.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-remaining-results.txt index 1a7c8a5..3a30a86 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-remaining-results.txt +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-remaining-results.txt @@ -1,76 +1,81 @@ -layer=2-remaining -status=blocked_at_compaction_stop_line +container=tiangong-m9a0-layer2-remaining-534108_20260823T060021Z image=tg-worker:dev image_id=sha256:ac1cb183e5b2c82982f6473fef86ccf128763a31192d711526d843a79edb69ff -openclaw=OpenClaw 2026.4.14 (2f35b6f) -network=none -patch_sha256=eb79f2c2c296f528ec93f1ce38581780b4d17afd566ba10fd3f07333169dd34d -patch_bytes=10942 -upstream_license=MIT -baseline_tiangong_commit=992665f -activation=TIANGONG_TRUSTED_BOUNDARIES_REQUIRED=1 -provider=owned_loopback_http_fake_provider - -main_readiness_runner=pass -main_readiness_container=tiangong-m9a0-layer2-422961_20260823T033559Z -unpatched_registered_provider_baseline=1/1 -patched_initial_tool_post_tool=1/1 -patched_provider_requests=2 -patched_tool_executions=1 -patched_event_order=attempt-ready,model-handler,tool-executed,tool-handler,model-handler -main_readiness_cleanup_container_absent=true - -normalized_tool_error=pass -normalized_tool_error_container=tiangong-m9a0-layer2-tool-error-fixed-399791_20260823T031300Z -normalized_tool_error_provider_requests=2 -normalized_tool_error_tool_executions=1 -normalized_tool_error_handler_isError=true -normalized_tool_error_event_order=attempt-ready,model-handler,tool-executed,tool-handler,model-handler -normalized_tool_error_cleanup_container_absent=true - -capture_failure=pass -capture_failure_container=tiangong-m9a0-layer2-capture-failure-fixed-404122_20260823T031624Z -capture_failure_provider_requests=1 -capture_failure_tool_executions=1 -capture_failure_next_model_handler=not_observed -capture_failure_tool_result_callback=not_observed -capture_failure_session_tool_result_messages=0 -capture_failure_cleanup_container_absent=true - -persisted_follow_up=pass -persisted_follow_up_container=tiangong-m9a0-layer2-followup-407234_20260823T032018Z -persisted_follow_up_provider_requests=2 -persisted_follow_up_model_handlers=2 -persisted_follow_up_event_order=attempt-ready,model-handler,attempt-ready,model-handler -persisted_follow_up_session_user_messages=2 -persisted_follow_up_cleanup_container_absent=true - -compaction_boundary=blocked -compaction_diagnostic_container=tiangong-m9a0-layer2-compaction-diag2-421077_20260823T033407Z -compaction_result_ok=true -compaction_result_compacted=true -compaction_provider_requests=1 -compaction_trusted_marker_in_provider_body=absent -compaction_before_model_call_handler_events=0 -compaction_entry_persisted=true -compaction_cleanup_container_absent=true -compaction_direct_fact=pi-coding-agent compaction calls completeSimple directly; the OpenClaw agent.onPayload seam is not on that call path - -initial_tool_error_diagnostic=source-failure-led-to-source-correction -initial_tool_error_container=tiangong-m9a0-layer2-tool-error-385530_20260823T025755Z -initial_tool_error_observation=OpenClaw normalized a thrown tool error to details.status=error while pi afterToolCall isError remained false -initial_tool_error_assertion=received_false_expected_true -initial_tool_error_cleanup_container_absent=true - -source_contract_after_tool_error_correction=pass -source_contract_container=tiangong-m9a0-source-399111_20260823T031231Z -source_contract_boundary_tests=10/10 -source_contract_adjacent_regressions=156/156 -source_contract_cleanup_container_absent=true - -stop_reason=supported_compaction_model_emitting_path_bypasses_required_final_handler -layer3=not_started -layer4_basic_matrix=not_started -external_resources=none -credentials=none -owned_container_prefix_absent=true +image_created=2026-08-21T13:00:21.057325912+08:00 +openclaw_version=OpenClaw 2026.4.14 (2f35b6f) +patch_sha256=3c81b8333aa23772989763a2328396e3132d424ce438f81e841f9e3bfc6019eb +checking file src/agents/pi-embedded-runner/compact.queued.ts +checking file src/agents/pi-embedded-runner/compact.ts +checking file src/agents/pi-embedded-runner/run.ts +checking file src/agents/pi-embedded-runner/run/attempt.ts +checking file src/agents/pi-embedded-runner/trusted-boundaries.ts +checking file src/plugins/hook-types.ts +patching file src/agents/pi-embedded-runner/compact.queued.ts +patching file src/agents/pi-embedded-runner/compact.ts +patching file src/agents/pi-embedded-runner/run.ts +patching file src/agents/pi-embedded-runner/run/attempt.ts +patching file src/agents/pi-embedded-runner/trusted-boundaries.ts +patching file src/plugins/hook-types.ts +Checking formatting... + +No config found, using defaults. Please add a config file or try `oxfmt --init` if needed. +All matched files use the correct format. +Finished in 40ms on 4 files using 16 threads. +layer2_case_start=layer2-tool-error.test.ts + + RUN  v4.1.4 /opt/openclaw + +stderr | src/agents/pi-embedded-runner/layer2-tool-error.test.ts > M9-A0 Layer 2 actual runEmbeddedAttempt normalized tool error > captures a normalized tool error before the follow-up request +[tools] synthetic failed: fixture-tool-failure raw_params={} + + ✓ src/agents/pi-embedded-runner/layer2-tool-error.test.ts > M9-A0 Layer 2 actual runEmbeddedAttempt normalized tool error > captures a normalized tool error before the follow-up request 88095ms + + Test Files  1 passed (1) + Tests  1 passed (1) + Start at  06:00:22 + Duration  93.45s (transform 3.68s, setup 0ms, import 5.28s, tests 88.10s, environment 0ms) + +layer2_case_pass=layer2-tool-error.test.ts +layer2_case_start=layer2-capture-failure.test.ts + + RUN  v4.1.4 /opt/openclaw + + ✓ src/agents/pi-embedded-runner/layer2-capture-failure.test.ts > M9-A0 Layer 2 actual runEmbeddedAttempt capture failure > stops release before ToolResult emission and the next provider request 51013ms + + Test Files  1 passed (1) + Tests  1 passed (1) + Start at  06:01:56 + Duration  56.37s (transform 3.65s, setup 0ms, import 5.26s, tests 51.01s, environment 0ms) + +layer2_case_pass=layer2-capture-failure.test.ts +layer2_case_start=layer2-followup.test.ts + + RUN  v4.1.4 /opt/openclaw + + ✓ src/agents/pi-embedded-runner/layer2-followup.test.ts > M9-A0 Layer 2 actual follow-up attempt > reinstalls the trusted model boundary on a persisted-session follow-up 17249ms + + Test Files  1 passed (1) + Tests  1 passed (1) + Start at  06:02:53 + Duration  22.69s (transform 3.77s, setup 0ms, import 5.37s, tests 17.25s, environment 0ms) + +layer2_case_pass=layer2-followup.test.ts +layer2_case_start=layer2-compaction.test.ts + + RUN  v4.1.4 /opt/openclaw + + ✓ src/agents/pi-embedded-runner/layer2-compaction.test.ts > M9-A0 Layer 2 model-compaction guard > denies manual, budget, and overflow compaction before provider or session mutation 153ms +stderr | src/agents/pi-embedded-runner/layer2-compaction.test.ts > M9-A0 Layer 2 model-compaction guard > keeps stock compaction behavior when required mode is absent +[session-compaction-checkpoints] skipping compaction checkpoint persist: session not found + + ✓ src/agents/pi-embedded-runner/layer2-compaction.test.ts > M9-A0 Layer 2 model-compaction guard > keeps stock compaction behavior when required mode is absent 71432ms + + Test Files  1 passed (1) + Tests  2 passed (2) + Start at  06:03:16 + Duration  77.16s (transform 3.88s, setup 0ms, import 5.47s, tests 71.59s, environment 0ms) + +layer2_case_pass=layer2-compaction.test.ts +cleanup_owner=tiangong-m9a0-layer2-remaining-534108_20260823T060021Z +cleanup_container_absent=true +layer2_remaining_exit=0 diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-results.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-results.txt index 0178a42..b166456 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-results.txt +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-results.txt @@ -1,51 +1,56 @@ layer=2 -status=incomplete_but_readiness_repaired +status=pass_with_model_compaction_disabled image=tg-worker:dev image_id=sha256:ac1cb183e5b2c82982f6473fef86ccf128763a31192d711526d843a79edb69ff openclaw=OpenClaw 2026.4.14 (2f35b6f) network=none activation=TIANGONG_TRUSTED_BOUNDARIES_REQUIRED=1 provider=owned_loopback_http_fake_provider - -initial_failure_class=test_driver_plus_adapter_host_fixture -initial_failure_root_cause_1=fake provider model was not registered through the OpenClaw provider plugin contract -initial_failure_root_cause_2=five-second boundary timer included synchronous adapter-host startup and expired before observable attempt readiness -lower_level_agent_session_readiness=pass -lower_level_agent_session_provider_requests=1 - -repair_provider_manifest_declared=true -repair_provider_registered_through_plugin_api=true -repair_observable_readiness=before_prompt_build -repair_startup_and_provider_budgets_separate=true -repair_timeout_timer_cleanup=true +patch_sha256=3c81b8333aa23772989763a2328396e3132d424ce438f81e841f9e3bfc6019eb unpatched_registered_provider_baseline=pass -unpatched_registered_provider_baseline_tests=1/1 unpatched_registered_provider_requests=1 patched_runEmbeddedAttempt_main_post_tool=pass -patched_runEmbeddedAttempt_tests=1/1 patched_runEmbeddedAttempt_provider_requests=2 patched_runEmbeddedAttempt_tool_executions=1 patched_event_order=attempt-ready,model-handler,tool-executed,tool-handler,model-handler patched_provider_payloads_from_trusted_handler=2/2 selected_stream_strategy=boundary-aware:openai-completions +layer2_tool_error=pass +layer2_tool_error_provider_requests=2 +layer2_tool_error_captured_isError=true +layer2_capture_failure=pass +layer2_capture_failure_provider_requests=1 +layer2_capture_failure_persisted_tool_results=0 +layer2_retry_followup=pass +layer2_followup_provider_requests=2 +layer2_followup_model_handlers=2 +layer2_followup_persisted_user_messages=2 + +layer2_model_compaction_guard=pass +layer2_compaction_disabled_reason=TRUSTED_BOUNDARY_MODEL_COMPACTION_DISABLED +layer2_denied_direct_triggers=manual,budget,overflow +layer2_denied_queued_trigger=manual +layer2_required_compaction_provider_requests=0 +layer2_required_compaction_model_handlers=0 +layer2_required_compaction_entries=0 +layer2_required_compaction_session_byte_identical=true +layer2_activation_absent_stock_compaction=pass +layer2_activation_absent_stock_provider_requests=1 + layer1_source_contract=pass -layer1_boundary_tests=10/10 +layer1_boundary_tests=11/11 layer1_adjacent_regressions=156/156 layer1_cleanup_container_absent=true +layer2_readiness_cleanup_container_absent=true +layer2_remaining_cleanup_container_absent=true +m9_a0_owned_container_prefix_absent=true -layer2_tool_error=not_started_in_repaired_host_harness -layer2_capture_failure=not_started_in_repaired_host_harness -layer2_retry_followup=not_started_in_repaired_host_harness -layer2_compaction_session=not_started_in_repaired_host_harness layer3_tiangong_handler=not_started +layer3_deterministic_session_rollover=not_started layer4_matrix=not_authorized -formal_m9_a_implementation=blocked_by_incomplete_spike - -final_runner_container=tiangong-m9a0-layer2-374508_20260823T024326Z -final_runner_cleanup_container_absent=true -diagnostic_cleanup_reconciled=true -m9_a0_owned_container_prefix_absent=true +formal_m9_a_implementation=blocked_by_incomplete_a0_spike +semantic_model_compaction_in_required_mode=disabled external_resources_created=none credentials_or_durable_provider_config=none diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/openclaw-2026.4.14-trusted-native-boundaries.patch b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/openclaw-2026.4.14-trusted-native-boundaries.patch index 6377127..0b41747 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/openclaw-2026.4.14-trusted-native-boundaries.patch +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/openclaw-2026.4.14-trusted-native-boundaries.patch @@ -1,46 +1,129 @@ +diff --git a/src/agents/pi-embedded-runner/compact.queued.ts b/src/agents/pi-embedded-runner/compact.queued.ts +index 4932863..057c5dc 100644 +--- a/src/agents/pi-embedded-runner/compact.queued.ts ++++ b/src/agents/pi-embedded-runner/compact.queued.ts +@@ -30,6 +30,7 @@ import { resolveGlobalLane, resolveSessionLane } from "./lanes.js"; + import { log } from "./logger.js"; + import { readPiModelContextTokens } from "./model-context-tokens.js"; + import { resolveModelAsync } from "./model.js"; ++import { resolveTiangongModelCompactionGuard } from "./trusted-boundaries.js"; + import type { EmbeddedPiCompactResult } from "./types.js"; + + /** +@@ -40,6 +41,10 @@ import type { EmbeddedPiCompactResult } from "./types.js"; + export async function compactEmbeddedPiSession( + params: CompactEmbeddedPiSessionParams, + ): Promise { ++ const disabledReason = resolveTiangongModelCompactionGuard(process.env); ++ if (disabledReason) { ++ return { ok: false, compacted: false, reason: disabledReason }; ++ } + const harnessResult = await maybeCompactAgentHarnessSession(params); + if (harnessResult) { + return harnessResult; diff --git a/src/agents/pi-embedded-runner/compact.ts b/src/agents/pi-embedded-runner/compact.ts -index 2c4236e..7366b84 100644 +index 2c4236e..a0a10ce 100644 --- a/src/agents/pi-embedded-runner/compact.ts +++ b/src/agents/pi-embedded-runner/compact.ts -@@ -121,0 +122,4 @@ import { truncateSessionAfterCompaction } from "./session-truncation.js"; -+import { -+ areTiangongTrustedBoundariesRequired, -+ installTiangongTrustedBoundariesFromEnv, -+} from "./trusted-boundaries.js"; -@@ -789,0 +794,3 @@ export async function compactEmbeddedPiSessionDirect( -+ if (areTiangongTrustedBoundariesRequired(process.env)) { -+ settingsManager.setCompactionEnabled(false); -+ } -@@ -879,0 +887,12 @@ export async function compactEmbeddedPiSessionDirect( -+ installTiangongTrustedBoundariesFromEnv({ -+ agent: session.agent, -+ context: { -+ runId, -+ agentId: sessionAgentId, -+ sessionId: session.sessionId, -+ workspaceDir: effectiveWorkspace, -+ ...(params.sessionKey ? { sessionKey: params.sessionKey } : {}), -+ ...(trigger ? { trigger } : {}), -+ }, -+ env: process.env, -+ }); +@@ -119,6 +119,7 @@ import { shouldUseOpenAIWebSocketTransport } from "./run/attempt.thread-helpers. + import { buildEmbeddedSandboxInfo } from "./sandbox-info.js"; + import { prewarmSessionFile, trackSessionManagerAccess } from "./session-manager-cache.js"; + import { truncateSessionAfterCompaction } from "./session-truncation.js"; ++import { resolveTiangongModelCompactionGuard } from "./trusted-boundaries.js"; + import { resolveEmbeddedRunSkillEntries } from "./skills-runtime.js"; + import { + resolveEmbeddedAgentApiKey, +@@ -304,6 +305,10 @@ function containsRealConversationMessages(messages: AgentMessage[]): boolean { + export async function compactEmbeddedPiSessionDirect( + params: CompactEmbeddedPiSessionParams, + ): Promise { ++ const disabledReason = resolveTiangongModelCompactionGuard(process.env); ++ if (disabledReason) { ++ return { ok: false, compacted: false, reason: disabledReason }; ++ } + const startedAt = Date.now(); + const diagId = params.diagId?.trim() || createCompactionDiagId(); + const trigger = params.trigger ?? "manual"; +@@ -877,7 +882,6 @@ export async function compactEmbeddedPiSessionDirect( + effectiveWorkspace, + agentDir, + }); +- + const prior = await sanitizeSessionHistory({ + messages: session.messages, + modelApi: model.api, +diff --git a/src/agents/pi-embedded-runner/run.ts b/src/agents/pi-embedded-runner/run.ts +index 8f0398e..2b80c31 100644 +--- a/src/agents/pi-embedded-runner/run.ts ++++ b/src/agents/pi-embedded-runner/run.ts +@@ -74,6 +74,7 @@ import { runContextEngineMaintenance } from "./context-engine-maintenance.js"; + import { resolveGlobalLane, resolveSessionLane } from "./lanes.js"; + import { log } from "./logger.js"; + import { resolveModelAsync } from "./model.js"; ++import { resolveTiangongModelCompactionGuard } from "./trusted-boundaries.js"; + import { createEmbeddedRunReplayState, observeReplayMetadata } from "./replay-state.js"; + import { handleAssistantFailover } from "./run/assistant-failover.js"; + import { createEmbeddedRunAuthController } from "./run/auth-controller.js"; +@@ -558,6 +559,7 @@ export async function runEmbeddedPiAgent( + // repeated initialization/connection overhead per attempt. + ensureContextEnginesInitialized(); + const contextEngine = await resolveContextEngine(params.config); ++ const modelCompactionDisabledReason = resolveTiangongModelCompactionGuard(process.env); + try { + // When the engine owns compaction, compactEmbeddedPiSessionDirect is + // bypassed. Fire lifecycle hooks here so recovery paths still notify +@@ -913,6 +915,9 @@ export async function runEmbeddedPiAgent( + attempt: timeoutCompactionAttempts, + maxAttempts: MAX_TIMEOUT_COMPACTION_ATTEMPTS, + }; ++ if (modelCompactionDisabledReason) { ++ throw new Error(modelCompactionDisabledReason); ++ } + timeoutCompactResult = await contextEngine.compact({ + sessionId: params.sessionId, + sessionKey: params.sessionKey, +@@ -1058,6 +1063,9 @@ export async function runEmbeddedPiAgent( + attempt: overflowCompactionAttempts, + maxAttempts: MAX_OVERFLOW_COMPACTION_ATTEMPTS, + }; ++ if (modelCompactionDisabledReason) { ++ throw new Error(modelCompactionDisabledReason); ++ } + compactResult = await contextEngine.compact({ + sessionId: params.sessionId, + sessionKey: params.sessionKey, diff --git a/src/agents/pi-embedded-runner/run/attempt.ts b/src/agents/pi-embedded-runner/run/attempt.ts -index 38aca02..4280460 100644 +index 38aca02..d9e90d9 100644 --- a/src/agents/pi-embedded-runner/run/attempt.ts +++ b/src/agents/pi-embedded-runner/run/attempt.ts -@@ -168,0 +169,4 @@ import { truncateOversizedToolResultsInSessionManager } from "../tool-result-tru +@@ -166,6 +166,10 @@ import { + installToolResultContextGuard, + } from "../tool-result-context-guard.js"; + import { truncateOversizedToolResultsInSessionManager } from "../tool-result-truncation.js"; +import { + areTiangongTrustedBoundariesRequired, + installTiangongTrustedBoundariesFromEnv, +} from "../trusted-boundaries.js"; -@@ -916,0 +921,6 @@ export async function runEmbeddedAttempt( -+ // Pi AgentSession compaction calls the provider outside agent.onPayload. -+ // Required Tiangong mode disables that bypass and uses the separately -+ // wrapped OpenClaw compaction path instead. + import { + logProviderToolSchemaDiagnostics, + normalizeProviderToolSchemas, +@@ -914,6 +918,12 @@ export async function runEmbeddedAttempt( + agentDir, + cfg: params.config, + }); ++ // Pi AgentSession model compaction calls the provider outside agent.onPayload. ++ // Required Tiangong mode disables it until the dependency exposes the same ++ // final trusted payload seam as ordinary turns. + if (areTiangongTrustedBoundariesRequired(process.env)) { + settingsManager.setCompactionEnabled(false); + } -@@ -993,0 +1004,12 @@ export async function runEmbeddedAttempt( + applyPiAutoCompactionGuard({ + settingsManager, + contextEngineInfo: params.contextEngine?.info, +@@ -991,6 +1001,18 @@ export async function runEmbeddedAttempt( + throw new Error("Embedded agent session missing"); + } + const activeSession = session; + installTiangongTrustedBoundariesFromEnv({ + agent: activeSession.agent, + context: { @@ -53,12 +136,15 @@ index 38aca02..4280460 100644 + }, + env: process.env, + }); + let prePromptMessageCount = activeSession.messages.length; + abortSessionForYield = () => { + yieldAbortSettled = Promise.resolve(activeSession.abort()); diff --git a/src/agents/pi-embedded-runner/trusted-boundaries.ts b/src/agents/pi-embedded-runner/trusted-boundaries.ts new file mode 100644 -index 0000000..2977f53 +index 0000000..abc3393 --- /dev/null +++ b/src/agents/pi-embedded-runner/trusted-boundaries.ts -@@ -0,0 +1,162 @@ +@@ -0,0 +1,171 @@ +import type { AfterToolCallContext, Agent } from "@mariozechner/pi-agent-core"; +import type { GlobalHookRunnerRegistry } from "../../plugins/hook-registry.types.js"; +import * as globalHookRunner from "../../plugins/hook-runner-global.js"; @@ -71,6 +157,8 @@ index 0000000..2977f53 +import { isToolResultError } from "../pi-embedded-subscribe.tools.js"; + +export const TIANGONG_TRUSTED_BOUNDARIES_REQUIRED_ENV = "TIANGONG_TRUSTED_BOUNDARIES_REQUIRED"; ++export const TIANGONG_MODEL_COMPACTION_DISABLED_REASON = ++ "TRUSTED_BOUNDARY_MODEL_COMPACTION_DISABLED"; +const PLUGIN_ID = "tiangong-control"; + +type BoundaryAgent = Pick; @@ -207,6 +295,13 @@ index 0000000..2977f53 + return true; +} + ++/** Model-backed compaction remains unavailable until it exposes the same final payload seam. */ ++export function resolveTiangongModelCompactionGuard(env: NodeJS.ProcessEnv): string | undefined { ++ return areTiangongTrustedBoundariesRequired(env) ++ ? TIANGONG_MODEL_COMPACTION_DISABLED_REASON ++ : undefined; ++} ++ +export function installTiangongTrustedBoundariesFromEnv(params: { + agent: BoundaryAgent; + registry?: GlobalHookRunnerRegistry | null; @@ -225,15 +320,42 @@ diff --git a/src/plugins/hook-types.ts b/src/plugins/hook-types.ts index a518668..62ac45a 100644 --- a/src/plugins/hook-types.ts +++ b/src/plugins/hook-types.ts -@@ -59,0 +60 @@ export type PluginHookName = +@@ -57,6 +57,7 @@ export type PluginHookName = + | "before_prompt_build" + | "before_agent_start" + | "before_agent_reply" + | "before_model_call" -@@ -71,0 +73 @@ export type PluginHookName = + | "llm_input" + | "llm_output" + | "agent_end" +@@ -69,6 +70,7 @@ export type PluginHookName = + | "message_sent" + | "before_tool_call" + | "after_tool_call" + | "before_tool_result_release" -@@ -90,0 +93 @@ export const PLUGIN_HOOK_NAMES = [ + | "tool_result_persist" + | "before_message_write" + | "session_start" +@@ -88,6 +90,7 @@ export const PLUGIN_HOOK_NAMES = [ + "before_prompt_build", + "before_agent_start", + "before_agent_reply", + "before_model_call", -@@ -102,0 +106 @@ export const PLUGIN_HOOK_NAMES = [ + "llm_input", + "llm_output", + "agent_end", +@@ -100,6 +103,7 @@ export const PLUGIN_HOOK_NAMES = [ + "message_sent", + "before_tool_call", + "after_tool_call", + "before_tool_result_release", -@@ -162,0 +167,14 @@ export type PluginHookBeforeAgentReplyResult = { + "tool_result_persist", + "before_message_write", + "session_start", +@@ -160,6 +164,20 @@ export type PluginHookBeforeAgentReplyResult = { + reason?: string; + }; + +/** Final provider payload after native before_provider_request transforms. */ +export type PluginHookBeforeModelCallEvent = { + payload: unknown; @@ -248,7 +370,13 @@ index a518668..62ac45a 100644 + payload: unknown; +}; + -@@ -333,0 +352,15 @@ export type PluginHookAfterToolCallEvent = { + export type PluginHookLlmInputEvent = { + runId: string; + sessionId: string; +@@ -331,6 +349,21 @@ export type PluginHookAfterToolCallEvent = { + durationMs?: number; + }; + +/** Final normalized tool outcome immediately before agent-loop release. */ +export type PluginHookBeforeToolResultReleaseEvent = { + toolName: string; @@ -264,12 +392,24 @@ index a518668..62ac45a 100644 + release: true; +}; + -@@ -587,0 +621,4 @@ export type PluginHookHandlerMap = { + export type PluginHookToolResultPersistContext = { + agentId?: string; + sessionKey?: string; +@@ -585,6 +618,10 @@ export type PluginHookHandlerMap = { + event: PluginHookBeforeAgentReplyEvent, + ctx: PluginHookAgentContext, + ) => Promise | PluginHookBeforeAgentReplyResult | void; + before_model_call: ( + event: PluginHookBeforeModelCallEvent, + ctx: PluginHookAgentContext, + ) => Promise | PluginHookBeforeModelCallResult | void; -@@ -637,0 +675,7 @@ export type PluginHookHandlerMap = { + llm_input: (event: PluginHookLlmInputEvent, ctx: PluginHookAgentContext) => Promise | void; + llm_output: ( + event: PluginHookLlmOutputEvent, +@@ -635,6 +672,13 @@ export type PluginHookHandlerMap = { + event: PluginHookAfterToolCallEvent, + ctx: PluginHookToolContext, + ) => Promise | void; + before_tool_result_release: ( + event: PluginHookBeforeToolResultReleaseEvent, + ctx: PluginHookToolContext, @@ -277,3 +417,6 @@ index a518668..62ac45a 100644 + | Promise + | PluginHookBeforeToolResultReleaseResult + | void; + tool_result_persist: ( + event: PluginHookToolResultPersistEvent, + ctx: PluginHookToolResultPersistContext, diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/source-contract-results.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/source-contract-results.txt index 58b76e5..5790e9b 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/source-contract-results.txt +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/source-contract-results.txt @@ -1,51 +1,56 @@ -container=tiangong-m9a0-source-399111_20260823T031231Z +container=tiangong-m9a0-source-530927_20260823T055756Z image=tg-worker:dev image_id=sha256:ac1cb183e5b2c82982f6473fef86ccf128763a31192d711526d843a79edb69ff image_created=2026-08-21T13:00:21.057325912+08:00 openclaw_version=OpenClaw 2026.4.14 (2f35b6f) -patch_sha256=eb79f2c2c296f528ec93f1ce38581780b4d17afd566ba10fd3f07333169dd34d +patch_sha256=3c81b8333aa23772989763a2328396e3132d424ce438f81e841f9e3bfc6019eb +checking file src/agents/pi-embedded-runner/compact.queued.ts checking file src/agents/pi-embedded-runner/compact.ts +checking file src/agents/pi-embedded-runner/run.ts checking file src/agents/pi-embedded-runner/run/attempt.ts checking file src/agents/pi-embedded-runner/trusted-boundaries.ts checking file src/plugins/hook-types.ts +patching file src/agents/pi-embedded-runner/compact.queued.ts patching file src/agents/pi-embedded-runner/compact.ts +patching file src/agents/pi-embedded-runner/run.ts patching file src/agents/pi-embedded-runner/run/attempt.ts patching file src/agents/pi-embedded-runner/trusted-boundaries.ts patching file src/plugins/hook-types.ts Checking formatting... -All matched files use the correct format. -Finished in 45ms on 6 files using 16 threads. No config found, using defaults. Please add a config file or try `oxfmt --init` if needed. +All matched files use the correct format. +Finished in 43ms on 8 files using 16 threads. - RUN v4.1.4 /opt/openclaw + RUN  v4.1.4 /opt/openclaw - ✓ src/agents/pi-embedded-runner/trusted-boundaries.provider.test.ts > M9-A0 actual pi-ai provider boundary > sends the trusted payload returned after native provider serialization 45ms - ✓ src/agents/pi-embedded-runner/trusted-boundaries.provider.test.ts > M9-A0 actual pi-ai provider boundary > does not issue a second HTTP request when tool capture fails 23ms - ✓ src/agents/pi-embedded-runner/trusted-boundaries.provider.test.ts > M9-A0 actual pi-ai provider boundary > makes zero HTTP requests when the trusted handler fails 1ms - ✓ src/agents/pi-embedded-runner/trusted-boundaries.test.ts > M9-A0 trusted boundaries > keeps stock behavior disabled but rejects an invalid activation value 2ms - ✓ src/agents/pi-embedded-runner/trusted-boundaries.test.ts > M9-A0 trusted boundaries > requires the exact loaded plugin and exactly one handler before installation 1ms - ✓ src/agents/pi-embedded-runner/trusted-boundaries.test.ts > M9-A0 trusted boundaries > runs after native payload transforms on every provider turn 7ms - ✓ src/agents/pi-embedded-runner/trusted-boundaries.test.ts > M9-A0 trusted boundaries > captures a normalized tool error before the follow-up provider turn 1ms - ✓ src/agents/pi-embedded-runner/trusted-boundaries.test.ts > M9-A0 trusted boundaries > keeps provider count at zero when the trusted model handler fails 0ms - ✓ src/agents/pi-embedded-runner/trusted-boundaries.test.ts > M9-A0 trusted boundaries > stops the turn before ToolResult emission when capture fails 1ms - ✓ src/agents/pi-embedded-runner/trusted-boundaries.test.ts > M9-A0 trusted boundaries > executes the handler-owned admission deadline and never invokes the tool 4ms + ✓ src/agents/pi-embedded-runner/trusted-boundaries.provider.test.ts > M9-A0 actual pi-ai provider boundary > sends the trusted payload returned after native provider serialization 46ms + ✓ src/agents/pi-embedded-runner/trusted-boundaries.provider.test.ts > M9-A0 actual pi-ai provider boundary > does not issue a second HTTP request when tool capture fails 23ms + ✓ src/agents/pi-embedded-runner/trusted-boundaries.provider.test.ts > M9-A0 actual pi-ai provider boundary > makes zero HTTP requests when the trusted handler fails 1ms + ✓ src/agents/pi-embedded-runner/trusted-boundaries.test.ts > M9-A0 trusted boundaries > keeps stock behavior disabled but rejects an invalid activation value 2ms + ✓ src/agents/pi-embedded-runner/trusted-boundaries.test.ts > M9-A0 trusted boundaries > disables model-backed compaction only in exact required mode 0ms + ✓ src/agents/pi-embedded-runner/trusted-boundaries.test.ts > M9-A0 trusted boundaries > requires the exact loaded plugin and exactly one handler before installation 1ms + ✓ src/agents/pi-embedded-runner/trusted-boundaries.test.ts > M9-A0 trusted boundaries > runs after native payload transforms on every provider turn 7ms + ✓ src/agents/pi-embedded-runner/trusted-boundaries.test.ts > M9-A0 trusted boundaries > captures a normalized tool error before the follow-up provider turn 1ms + ✓ src/agents/pi-embedded-runner/trusted-boundaries.test.ts > M9-A0 trusted boundaries > keeps provider count at zero when the trusted model handler fails 0ms + ✓ src/agents/pi-embedded-runner/trusted-boundaries.test.ts > M9-A0 trusted boundaries > stops the turn before ToolResult emission when capture fails 1ms + ✓ src/agents/pi-embedded-runner/trusted-boundaries.test.ts > M9-A0 trusted boundaries > executes the handler-owned admission deadline and never invokes the tool 3ms - Test Files 2 passed (2) - Tests 10 passed (10) - Start at 03:12:33 - Duration 1.82s (transform 1.40s, setup 0ms, import 2.24s, tests 86ms, environment 0ms) + Test Files  2 passed (2) + Tests  11 passed (11) + Start at  05:57:58 + Duration  1.80s (transform 1.35s, setup 0ms, import 2.22s, tests 87ms, environment 0ms) - RUN v4.1.4 /opt/openclaw + RUN  v4.1.4 /opt/openclaw -···························································································································································· +···························································································································································· - Test Files 4 passed (4) - Tests 156 passed (156) - Start at 03:12:36 - Duration 4.91s (transform 5.47s, setup 0ms, import 6.31s, tests 1.84s, environment 0ms) + Test Files  4 passed (4) + Tests  156 passed (156) + Start at  05:58:00 + Duration  5.01s (transform 5.51s, setup 0ms, import 6.31s, tests 1.82s, environment 0ms) -cleanup_owner=tiangong-m9a0-source-399111_20260823T031231Z +cleanup_owner=tiangong-m9a0-source-530927_20260823T055756Z cleanup_container_absent=true m9_a0_source_contract=pass diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/trusted-boundaries.test.ts b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/trusted-boundaries.test.ts index 3cc2735..f142109 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/trusted-boundaries.test.ts +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/trusted-boundaries.test.ts @@ -21,6 +21,8 @@ import { wrapToolWithBeforeToolCallHook } from "../pi-tools.before-tool-call.js" import { installTiangongTrustedBoundaries, installTiangongTrustedBoundariesFromEnv, + resolveTiangongModelCompactionGuard, + TIANGONG_MODEL_COMPACTION_DISABLED_REASON, } from "./trusted-boundaries.js"; const model: Model<"openai-completions"> = { @@ -190,6 +192,18 @@ describe("M9-A0 trusted boundaries", () => { ).toThrow("TRUSTED_BOUNDARY_INVALID_ACTIVATION:true"); }); + it("disables model-backed compaction only in exact required mode", () => { + expect(resolveTiangongModelCompactionGuard({})).toBeUndefined(); + expect(resolveTiangongModelCompactionGuard({ TIANGONG_TRUSTED_BOUNDARIES_REQUIRED: "1" })).toBe( + TIANGONG_MODEL_COMPACTION_DISABLED_REASON, + ); + expect(() => + resolveTiangongModelCompactionGuard({ + TIANGONG_TRUSTED_BOUNDARIES_REQUIRED: "true", + }), + ).toThrow("TRUSTED_BOUNDARY_INVALID_ACTIVATION:true"); + }); + it("requires the exact loaded plugin and exactly one handler before installation", () => { const agent = createAgent({ streamFn: makeProviderStream({ events: [], providerPayloads: [], responses: [] }), diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/plan.md b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/plan.md index bbfb724..a791253 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/plan.md +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/plan.md @@ -1,14 +1,14 @@ # M9-A0 trusted native-boundary follow-up -> Status: blocked at the Layer 2 compaction stop line; Layer 1, main/tool-error/capture-failure/follow-up cases pass, while compaction bypasses the required final model handler. +> Status: Layer 1 and Layer 2 pass for the research candidate. In required mode, model-backed compaction is intentionally unavailable and fails closed; Layer 3 is eligible but has not started. ## Scope - Issue: #116 - Baseline: pinned OpenClaw `2026.4.14` (`2f35b6f`) in the existing Worker image - Predecessor: [`../2026-08-22-m9-a0-source-seam-prototype/result.md`](../2026-08-22-m9-a0-source-seam-prototype/result.md) -- Purpose: replace the rejected session-level/tool-wrapper prototype with the thinnest source seams that can prove the accepted M9-A0 contracts -- Product boundary: research patch and test assets only; no Tiangong runtime enablement, OpenClaw upgrade, database change, external provider, or Matrix run +- Purpose: prove the thinnest OpenClaw source seams that satisfy the accepted M9-A0 model and ToolResult contracts +- Product boundary: research patch and test assets only; no Tiangong runtime enablement, OpenClaw upgrade, dependency modification, database change, external provider, or Matrix run The predecessor remains a blocked historical result. Its `before_model_call` placement before `activeSession.prompt(...)` and its raw tool-wrapper capture must not be reused. @@ -16,24 +16,29 @@ The predecessor remains a blocked historical result. Its `before_model_call` pla The candidate patch is [`evidence/openclaw-2026.4.14-trusted-native-boundaries.patch`](evidence/openclaw-2026.4.14-trusted-native-boundaries.patch). -- Patch SHA-256: `eb79f2c2c296f528ec93f1ce38581780b4d17afd566ba10fd3f07333169dd34d` +- Patch bytes: `17209` +- Patch SHA-256: `3c81b8333aa23772989763a2328396e3132d424ce438f81e841f9e3bfc6019eb` - Upstream license: MIT - Effective research identity: OpenClaw `2026.4.14 (2f35b6f)` plus the exact patch digest above -- Patch scope: four OpenClaw source files, including one new boundary helper; no dependency source, schema, table, ledger, or product Worker file +- Patch scope: six OpenClaw source files, including one new boundary helper; no dependency source, schema, table, ledger, or product Worker file The patch does three things: 1. **Final model seam:** wraps Pi Agent's native `onPayload` path after native `before_provider_request` transforms. Exactly one loaded `tiangong-control` handler must explicitly return `{ allow: true, payload }`. Missing registry/plugin/handler, duplicate handler, malformed result, timeout, or throw fails closed before the selected provider transport receives the payload. 2. **Final tool seam:** uses pi-agent-core's awaited `afterToolCall` callback. The handler sees the final normalized success/error result before `tool_execution_end` and ToolResult message emission and must explicitly return `{ release: true }`. A capture failure rejects the turn rather than becoming an ordinary ToolResult. -3. **Auxiliary-call guard:** when `TIANGONG_TRUSTED_BOUNDARIES_REQUIRED=1`, Pi AgentSession auto-compaction is disabled because it can call a provider outside `agent.onPayload`; OpenClaw's separately created compaction session receives the same trusted boundaries. +3. **Model-compaction guard:** when `TIANGONG_TRUSTED_BOUNDARIES_REQUIRED=1`, Pi AgentSession auto-compaction is disabled. OpenClaw direct, queued/harness, timeout-recovery, overflow-recovery, and context-engine-owned compaction entry points are denied with `TRUSTED_BOUNDARY_MODEL_COMPACTION_DISABLED` before a compaction provider request. Manual compaction uses the same denied path. Stock OpenClaw behavior remains unchanged when activation is absent. -Stock OpenClaw behavior remains unchanged when the activation variable is absent. A future dedicated Tiangong image must pin it to exactly `1` and verify it in build/entrypoint/preflight code; that product change is outside this spike. +The guard replaces the rejected attempt to install `agent.onPayload` on a separately created compaction session. Pinned `pi-coding-agent` compaction calls `completeSimple(...)` directly, so that session handler cannot intercept the provider payload. Semantic compaction may be reconsidered only after a reviewed upstream/dependency seam exposes the same awaited final payload transform. A manual trigger is not a security boundary. + +A future dedicated Tiangong image must pin the activation variable to exactly `1` and verify it in build, entrypoint, and preflight code. That product change remains outside this spike. ## Cost discipline - Prefer this native `before_provider_request`/`afterToolCall` bridge over a larger model runtime or agent-loop patch. +- Do not add a Tiangong-owned summarization model runtime. OpenClaw remains responsible for provider turns and conversation state. - The boundaries own no persistence. A prototype handler may call an owned temporary spool, but no new ledger, table, or authoritative state is permitted. -- Stop if correctness requires patching pi-agent-core, adding another persistence domain, or spreading across additional model lifecycle implementations. Compare an upstream seam or reviewed OpenClaw upgrade instead; do not upgrade automatically or silently. +- A future deterministic session rollover must rebuild from durable Work, Task, Result, ToolResult, and Operation facts. It is not semantic compaction and must not treat a model summary as a machine fact. +- Do not modify pinned dependency internals or silently upgrade OpenClaw. Compare a small public `onPayload` plumbing seam with a reviewed upgrade in a separate decision. ## Boundary truth table @@ -49,11 +54,13 @@ Stock OpenClaw behavior remains unchanged when the activation variable is absent | Tool error | Capture receives the normalized error with `isError=true` before the next provider request | | Capture throws or returns malformed output | No ToolResult message is emitted and no next provider request occurs | | `before_tool_call` resolver never completes | Handler-owned AbortController deadline fires, the resolver observes abort, and the tool is never invoked | -| OpenClaw compaction path | The separately created compaction session passes the same model boundary | -| Pi AgentSession auto-compaction in required mode | Disabled; it cannot bypass `onPayload` | +| Required-mode manual/budget/overflow compaction | Stable disabled result; zero compaction provider requests, zero trusted-handler events, no compaction entry, and byte-identical session transcript | +| Required-mode queued/harness or context-engine-owned compaction | Denied before delegation to the compaction implementation | +| Pi AgentSession auto-compaction in required mode | Disabled before AgentSession creation | +| Activation-absent stock compaction | Existing compaction succeeds through one provider request, proving adjacent behavior is unchanged | | Selected HTTP/WebSocket/custom transport ignores `onPayload` before network | **RED**; stop at the source-contract layer | -Unknown-tool and invalid-argument immediate outcomes do not pass through pi-agent-core `afterToolCall`. They remain admission/normalization outcomes that M9-A must close through its existing pending-call/Gate path; this spike must not claim that the release seam alone implements their ToolResult persistence. +Unknown-tool and invalid-argument immediate outcomes do not pass through pi-agent-core `afterToolCall`. They remain admission/normalization outcomes that M9-A must close through its existing pending-call/Gate path; this spike does not claim that the release seam alone implements their ToolResult persistence. ## Serial execution @@ -67,41 +74,36 @@ smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-source-contrac 2>&1 | tee smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/source-contract-results.txt ``` -The runner owns one uniquely named `--network none` container, applies the exact patch, copies the two tests into disposable OpenClaw source, runs targeted type checking, exercises a real pi-agent-core loop and local HTTP fake provider, and runs adjacent OpenClaw regressions. It removes only its exact container name through a trap. +The runner owns one uniquely named `--network none` container, applies the exact patch, copies the tests into disposable OpenClaw source, runs focused type and formatting checks, exercises a real pi-agent-core loop and local HTTP fake provider, and runs adjacent OpenClaw regressions. It removes only its exact container name through a trap. -This layer must prove at least: +Current result: **PASS**, boundary/provider tests **11/11**, adjacent regressions **156/156**. -- actual pi-ai serialization calls the trusted handler before the local HTTP server observes a request; -- invalid bootstrap produces zero HTTP requests; -- capture failure after a real provider tool call produces no second HTTP request and no ToolResult message; -- successful and error tool outcomes are captured before follow-up provider calls; -- handler-owned admission timeout aborts its resolver and does not execute the tool; -- stock adjacent tests remain green. +### Layer 2: actual patched OpenClaw attempt and local fake provider -A full OpenClaw typecheck may be added only when the image contains the complete upstream test/helper tree. Missing baseline source files must be reported as readiness failure, not converted into pass evidence. +Run both reproducible runners: -### Layer 2: actual patched OpenClaw attempt and local fake provider +```bash +set -o pipefail +smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer2-readiness.sh \ + 2>&1 | tee smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-readiness-results.txt -Proceed only after Layer 1 passes. +smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer2-remaining.sh \ + 2>&1 | tee smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-remaining-results.txt +``` -- Set `TIANGONG_TRUSTED_BOUNDARIES_REQUIRED=1` in the disposable control process. -- Load exactly one test-only `tiangong-control` handler for each new hook through the real OpenClaw plugin loader. -- Use the actual `runEmbeddedAttempt`/gateway/session path and an owned loopback fake provider; do not manually increment a provider counter based on a hook return. -- Register the fake provider through a test-only plugin manifest and `registerProvider`; an ad hoc `Model` with an unknown provider ID does not satisfy OpenClaw's adapter-host contract. -- Record the selected stream strategy and prove that its implementation awaits `onPayload` before its first socket write. -- Exercise the main turn, post-tool turn, retry/follow-up path, and OpenClaw-owned compaction session. -- Use the test-only `before_prompt_build` event as the observable attempt-ready condition. Keep the bounded host-startup budget separate from the five-second provider-response budget, and clear both timers after their race. A timeout before readiness is red but is not provider-boundary evidence. +The tests use the real plugin loader, exactly one registered fake provider, actual `runEmbeddedAttempt`, persisted OpenClaw sessions, actual direct/queued compaction entry points, and an owned loopback provider inside `--network none` containers. `before_prompt_build` is the attempt-readiness observation; host startup and provider response budgets remain separate. -Current execution has passed the main/tool-error/capture-failure/persisted-follow-up cases. The separately created OpenClaw compaction session bypassed `agent.onPayload` through a direct pinned `completeSimple` call, so the supported-path bypass stop condition is red and the serial plan stops here. +Current result: **PASS** for the main tool turn, normalized tool error, capture failure, persisted follow-up, and required-mode compaction denial. The historical bypass diagnostic and pre-guard output remain preserved separately. ### Layer 3: Tiangong control-handler prototype -Proceed only after Layer 2 passes. +Proceed only from the passing Layer 2 result. - Use a bounded test bootstrap and an owned temporary spool beneath a unique temporary root. - Prove exact handler identity, bootstrap corruption denial, success/error capture, capture failure, recovery-required signaling, and no next provider request. - Reuse the planned ToolResult spool contract; do not add a table or a new ledger. - This remains a disposable prototype and must not delete the current product runtime paths. +- A deterministic session-rollover prototype, if needed, must be a separate focused boundary with no provider call and reconstruction from direct durable facts. ### Layer 4: Basic Matrix member turn @@ -109,14 +111,15 @@ Not authorized by this plan. It remains last and requires separate review of Lay ## Evidence requirements -The independent runner must create `result.md` and preserve only bounded, sanitized facts: +Preserve only bounded, sanitized facts: - baseline Tiangong commit and clean/dirty status; - image tag, immutable image ID, creation time, and exact OpenClaw version; - patch path, byte count, SHA-256, dry-run/apply exit codes, and license; -- every exact command, start/end time, exit code, file count, and test count; +- every runner, start/end time, exit code, file count, and test count; - provider request counts and stable event ordering for each truth-table case; - selected transport/stream strategy and direct socket-side observation; +- exact compaction disabled reason, provider count, handler count, transcript mutation observation, and compaction-entry count; - bounded error codes only, without raw prompts, credentials, provider config, or transcripts; - exact owned container/temp/spool identifiers and post-cleanup absence checks. @@ -126,12 +129,13 @@ Do not combine separate test invocations into an unexplained aggregate count. A Stop A0 and update the design or make a separate source-patch/upgrade decision if any of these occurs: -- a supported model-emitting path bypasses the required final handler; +- a supported required-mode model-emitting path bypasses the final handler or the explicit compaction guard; +- required-mode compaction emits a provider request, invokes a model handler, or mutates the transcript; - missing or malformed trusted registration permits a provider request; - capture failure emits a model-visible ToolResult or permits a subsequent provider request; - the underlying admission resolver continues with a late state mutation after deadline abort; -- the actual selected transport does not await `onPayload` before network I/O; -- the patch must expand into dependency internals, persistence, or another model/session runtime; +- the selected transport does not await `onPayload` before network I/O; +- correctness requires a Tiangong-owned model runtime, new persistence domain, or another model/session lifecycle; - cleanup cannot prove absence of every owned resource. -No failure may be addressed by silently upgrading OpenClaw, weakening the truth table, using native fail-open observations as enforcement, or starting M9-A implementation in parallel. +No failure may be addressed by silently upgrading OpenClaw, weakening the truth table, relabeling a manual trigger as trusted, using native fail-open observations as enforcement, or starting formal M9-A implementation before the spike is complete. diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/result.md b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/result.md index 5ed96de..1b9f6ba 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/result.md +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/result.md @@ -2,31 +2,40 @@ ## Decision -**Layer 2 main, normalized tool-error, capture-failure, and persisted follow-up cases pass. A0 stops at the compaction stop line.** +**Layer 1 and Layer 2 pass for the revised research candidate. Model-backed compaction is fail-closed and unavailable in required mode.** -The real OpenClaw-owned compaction path emits one provider request without the trusted payload marker and without invoking the required `before_model_call` handler. This is a supported model-emitting path which bypasses the final handler, so Layer 3 and Layer 4 were not started. +The initial real compaction diagnostic proved that pinned `pi-coding-agent` calls `completeSimple(...)` outside the session `agent.onPayload` path. The separately installed handler could not protect that provider call. Following an explicit source-patch decision, the candidate now disables Pi AgentSession auto-compaction and denies OpenClaw manual, budget, overflow, queued/harness, and context-engine-owned compaction paths in required mode. -M9-A formal implementation remains blocked. The candidate remains a research artifact; no Tiangong runtime, installed OpenClaw tree, database, provider service, Matrix resource, or external service was changed. +This is not semantic compaction support. Manual compaction remains disabled because changing the trigger does not change the provider boundary. A future manual canary requires a reviewed upstream/dependency payload seam; deterministic session rollover remains separate future work. + +Layer 3 is now eligible but was not started in this change. M9-A formal implementation remains blocked until the disposable Tiangong control-handler prototype passes. Matrix Layer 4 remains unauthorized. + +The candidate remains a research artifact. No Tiangong runtime, installed OpenClaw tree, dependency, database, provider service, Matrix resource, or external service was changed. ## Pinned source contract -**PASS** after the Layer 2 tool-error correction via [`run-source-contract.sh`](run-source-contract.sh). +**PASS** via [`run-source-contract.sh`](run-source-contract.sh). +- baseline Tiangong commit: `8bb697a` (clean before this follow-up) - image: `tg-worker:dev` - image ID: `sha256:ac1cb183e5b2c82982f6473fef86ccf128763a31192d711526d843a79edb69ff` - OpenClaw: `2026.4.14 (2f35b6f)` - network: `none` -- patch SHA-256: `eb79f2c2c296f528ec93f1ce38581780b4d17afd566ba10fd3f07333169dd34d` -- boundary/provider tests: **10/10** +- patch bytes: `17209` +- patch SHA-256: `3c81b8333aa23772989763a2328396e3132d424ce438f81e841f9e3bfc6019eb` +- patch scope: six OpenClaw source files; no dependency source +- boundary/provider tests: **11/11** - adjacent OpenClaw regressions: **156/156** -- targeted typecheck, formatting, patch dry-run: passed +- focused helper typecheck, formatting, and patch dry-run/application: passed - cleanup container: absent Direct output: [`evidence/source-contract-results.txt`](evidence/source-contract-results.txt). +An expanded touched-file typecheck was not counted as passing evidence: its transitive graph reaches the stock `src/media/pdf-extract.ts` baseline and reports `TS2353` for `disableWorker`. Actual patched modules were loaded by the focused Vitest runs; the bounded helper/type contract passed. No full-tree typecheck success is claimed. + ## Layer 2 passing cases -The repaired runner uses the real plugin loader, registered fake provider, actual `runEmbeddedAttempt`, and an owned loopback HTTP provider in `--network none` containers. +The runners use the real OpenClaw plugin loader, registered fake provider, actual `runEmbeddedAttempt`, persisted sessions, direct and queued compaction entry points, and an owned loopback HTTP provider in disposable `--network none` containers. ### Main/tool success @@ -41,31 +50,30 @@ The repaired runner uses the real plugin loader, registered fake provider, actua ### Normalized tool error -**PASS** via [`evidence/layer2-tool-error.test.ts`](evidence/layer2-tool-error.test.ts). - -The first diagnostic exposed a real adapter detail: OpenClaw's `toToolDefinitions` catches a thrown tool error and returns a normalized result with `details.status="error"`, while Pi's `afterToolCall` `isError` remained false. The candidate seam now ORs the existing OpenClaw `isToolResultError(event.result)` classification into the trusted release event. Layer 1 was rerun after this correction and passed. - -Direct facts after correction: +**PASS** via [`evidence/layer2-remaining-results.txt`](evidence/layer2-remaining-results.txt). - provider requests: **2**; - tool executions: **1**; -- trusted release handler saw `isError=true`; +- trusted release handler observed `isError=true`; - the normalized error was captured before the follow-up provider request. +OpenClaw's tool adapter catches the thrown fixture error and returns a normalized result. The candidate ORs OpenClaw's existing `isToolResultError(event.result)` classification into the trusted release event. + ### Capture failure -**PASS** via [`evidence/layer2-capture-failure.test.ts`](evidence/layer2-capture-failure.test.ts). +**PASS** via [`evidence/layer2-remaining-results.txt`](evidence/layer2-remaining-results.txt). - provider requests: **1**; - tool executions: **1**; - no next model handler; - no `onToolResult` callback; -- no persisted `toolResult` message; -- capture handler failure therefore stops release before ordinary ToolResult emission and before the next provider request. +- no persisted `toolResult` message. + +The handler failure therefore stops release before ordinary ToolResult emission and before the next provider request. ### Persisted follow-up -**PASS** via [`evidence/layer2-followup.test.ts`](evidence/layer2-followup.test.ts). +**PASS** via [`evidence/layer2-remaining-results.txt`](evidence/layer2-remaining-results.txt). Two real `runEmbeddedAttempt` invocations used the same persisted session and plugin/provider setup: @@ -76,40 +84,57 @@ Two real `runEmbeddedAttempt` invocations used the same persisted session and pl This proves the final model seam is reinstalled on the persisted-session follow-up path. -## Compaction stop line +### Required-mode compaction guard -**BLOCKED** via [`evidence/layer2-compaction.test.ts`](evidence/layer2-compaction.test.ts). +**PASS** via [`evidence/layer2-compaction.test.ts`](evidence/layer2-compaction.test.ts) and the final case in [`evidence/layer2-remaining-results.txt`](evidence/layer2-remaining-results.txt). -The diagnostic creates an actual OpenClaw compaction session through `compactEmbeddedPiSessionDirect`, using the registered provider and a real local provider request. Compaction itself reports `ok=true` and `compacted=true`, but the direct provider facts are: +With `TIANGONG_TRUSTED_BOUNDARIES_REQUIRED=1`: -- provider requests: **1**; -- trusted payload marker: absent; -- `before_model_call` handler events: **0**; -- compaction entry: persisted. +- direct manual, budget, and overflow calls each returned `TRUSTED_BOUNDARY_MODEL_COMPACTION_DISABLED`; +- the public queued compaction call returned the same stable result before harness/context-engine delegation; +- provider requests: **0**; +- trusted model-handler events: **0**; +- compaction entries appended: **0**; +- session transcript: byte-identical before and after all denied calls. + +The source candidate also checks the same guard before timeout- and overflow-recovery `contextEngine.compact(...)` calls, including engines which own compaction. Pi AgentSession auto-compaction remains disabled before session creation. -Source inspection identifies the bypass: pinned `@mariozechner/pi-coding-agent` compaction calls `completeSimple(...)` directly. Installing `agent.onPayload` on the separately created session cannot intercept that direct `completeSimple` call. The initial assertion-failing diagnostic and the passing bypass diagnostic are preserved as bounded evidence; this is not converted into a success claim. +The activation-absent adjacent case also passed: stock compaction made one provider request and persisted a compaction entry. This proves the guard does not change stock OpenClaw behavior when required mode is absent. -This meets the A0 stop condition for a supported model-emitting path bypassing the required final handler. Fixing it would require a separately reviewed upstream seam or a dependency/internal compaction change, not a prompt, Skill, Matrix, or silent upgrade workaround. +## Historical compaction bypass -## Not started +The pre-guard diagnostic is retained as bounded failure evidence: -Because the compaction stop line is red: +- [`evidence/layer2-compaction-bypass-diagnostic.test.ts`](evidence/layer2-compaction-bypass-diagnostic.test.ts) +- [`evidence/layer2-pre-guard-results.txt`](evidence/layer2-pre-guard-results.txt) + +It observed one unmarked provider request, zero `before_model_call` handler events, and a persisted compaction entry. Source inspection identified direct `completeSimple(...)` calls in pinned `pi-coding-agent` compaction. The current result does not relabel that bypass as trusted; it removes the model-emitting capability in required mode. + +The same dependency contains model-backed branch summarization, but source audit found no production OpenClaw invocation of `AgentSession.navigateTree(...)` in the pinned integration. Tiangong's future image must continue to disable unowned extensions and discovery; exposing a new branch-navigation path would reopen this audit. + +## Not started and known limitations - Layer 3 Tiangong control-handler prototype: **not started**; +- deterministic session rollover from durable facts: **not implemented**; +- semantic or manual model compaction: **not supported in required mode**; - Basic Matrix Layer 4: **not authorized**; -- no further retry/compaction variants were run; -- M9-A formal implementation remains blocked. +- M9-A formal implementation: **still blocked by incomplete A0 spike**. + +Until rollover or a trusted compaction seam is implemented, long sessions may reach OpenClaw's ordinary context-overflow failure/recovery behavior. This run proves that compaction cannot issue an untrusted provider request; it does not prove a seamless long-session user experience. ## Evidence index -- [`evidence/layer2-remaining-results.txt`](evidence/layer2-remaining-results.txt) +- [`evidence/source-contract-results.txt`](evidence/source-contract-results.txt) - [`evidence/layer2-readiness-results.txt`](evidence/layer2-readiness-results.txt) +- [`evidence/layer2-remaining-results.txt`](evidence/layer2-remaining-results.txt) - [`evidence/layer2-tool-error.test.ts`](evidence/layer2-tool-error.test.ts) - [`evidence/layer2-capture-failure.test.ts`](evidence/layer2-capture-failure.test.ts) - [`evidence/layer2-followup.test.ts`](evidence/layer2-followup.test.ts) - [`evidence/layer2-compaction.test.ts`](evidence/layer2-compaction.test.ts) -- [`evidence/source-contract-results.txt`](evidence/source-contract-results.txt) +- [`evidence/layer2-compaction-bypass-diagnostic.test.ts`](evidence/layer2-compaction-bypass-diagnostic.test.ts) +- [`evidence/layer2-pre-guard-results.txt`](evidence/layer2-pre-guard-results.txt) +- [`run-layer2-remaining.sh`](run-layer2-remaining.sh) ## Cleanup -The exact test containers recorded in the evidence were removed and individually verified absent. A final Docker inspection also verified that no exited or running container with the owned `tiangong-m9a0-` prefix remains. Temporary test roots and provider servers were removed by test teardown. No external resource or credential was created. +Each runner removed only its exact owned container and verified it absent. The final Docker inspection at `2026-08-23T06:04:42Z` verified that no exited or running container with the `tiangong-m9a0-` prefix remained. Temporary test roots and provider servers were removed by test teardown. No external resource or credential was created. diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer2-readiness.sh b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer2-readiness.sh index 02ff22e..454b8c5 100755 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer2-readiness.sh +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer2-readiness.sh @@ -4,7 +4,7 @@ set -euo pipefail RUN_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" IMAGE="${M9_A0_IMAGE:-tg-worker:dev}" PATCH="$RUN_DIR/evidence/openclaw-2026.4.14-trusted-native-boundaries.patch" -EXPECTED_PATCH_SHA256="eb79f2c2c296f528ec93f1ce38581780b4d17afd566ba10fd3f07333169dd34d" +EXPECTED_PATCH_SHA256="3c81b8333aa23772989763a2328396e3132d424ce438f81e841f9e3bfc6019eb" EXPECTED_VERSION="OpenClaw 2026.4.14 (2f35b6f)" CONTAINER="tiangong-m9a0-layer2-$$_$(date -u +%Y%m%dT%H%M%SZ)" diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer2-remaining.sh b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer2-remaining.sh new file mode 100755 index 0000000..42f1769 --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer2-remaining.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +set -euo pipefail + +RUN_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +IMAGE="${M9_A0_IMAGE:-tg-worker:dev}" +PATCH="$RUN_DIR/evidence/openclaw-2026.4.14-trusted-native-boundaries.patch" +EXPECTED_PATCH_SHA256="3c81b8333aa23772989763a2328396e3132d424ce438f81e841f9e3bfc6019eb" +EXPECTED_VERSION="OpenClaw 2026.4.14 (2f35b6f)" +CONTAINER="tiangong-m9a0-layer2-remaining-$$_$(date -u +%Y%m%dT%H%M%SZ)" + +cleanup() { + if docker container inspect "$CONTAINER" >/dev/null 2>&1; then + docker rm -f "$CONTAINER" >/dev/null + fi +} +trap cleanup EXIT INT TERM + +actual_patch_sha256="$(sha256sum "$PATCH" | awk '{print $1}')" +if [[ "$actual_patch_sha256" != "$EXPECTED_PATCH_SHA256" ]]; then + printf 'patch_sha256_mismatch expected=%s actual=%s\n' \ + "$EXPECTED_PATCH_SHA256" "$actual_patch_sha256" >&2 + exit 1 +fi + +image_id="$(docker image inspect --format '{{.Id}}' "$IMAGE")" +image_created="$(docker image inspect --format '{{.Created}}' "$IMAGE")" +version="$(docker run --rm --network none --entrypoint openclaw "$IMAGE" --version)" +if [[ "$version" != "$EXPECTED_VERSION" ]]; then + printf 'openclaw_version_mismatch expected=%q actual=%q\n' "$EXPECTED_VERSION" "$version" >&2 + exit 1 +fi + +printf 'container=%s\nimage=%s\nimage_id=%s\nimage_created=%s\nopenclaw_version=%s\npatch_sha256=%s\n' \ + "$CONTAINER" "$IMAGE" "$image_id" "$image_created" "$version" "$actual_patch_sha256" + +status=0 +docker run --name "$CONTAINER" --network none \ + --mount "type=bind,src=$RUN_DIR,dst=/m9-a0,readonly" \ + --entrypoint sh "$IMAGE" -lc ' + set -eu + cd /opt/openclaw + + patch -p1 --dry-run < /m9-a0/evidence/openclaw-2026.4.14-trusted-native-boundaries.patch + patch -p1 < /m9-a0/evidence/openclaw-2026.4.14-trusted-native-boundaries.patch + for test_file in \ + layer2-tool-error.test.ts \ + layer2-capture-failure.test.ts \ + layer2-followup.test.ts \ + layer2-compaction.test.ts; do + cp "/m9-a0/evidence/$test_file" "src/agents/pi-embedded-runner/$test_file" + done + + ./node_modules/.bin/oxfmt --check \ + src/agents/pi-embedded-runner/layer2-tool-error.test.ts \ + src/agents/pi-embedded-runner/layer2-capture-failure.test.ts \ + src/agents/pi-embedded-runner/layer2-followup.test.ts \ + src/agents/pi-embedded-runner/layer2-compaction.test.ts + + for test_file in \ + layer2-tool-error.test.ts \ + layer2-capture-failure.test.ts \ + layer2-followup.test.ts \ + layer2-compaction.test.ts; do + printf "layer2_case_start=%s\n" "$test_file" + OPENCLAW_LOG_LEVEL=warn ./node_modules/.bin/vitest run \ + "src/agents/pi-embedded-runner/$test_file" \ + --reporter=verbose --hookTimeout=20000 + printf "layer2_case_pass=%s\n" "$test_file" + done + ' || status=$? + +cleanup +if docker container inspect "$CONTAINER" >/dev/null 2>&1; then + printf 'cleanup_container_still_present=%s\n' "$CONTAINER" >&2 + exit 1 +fi +printf 'cleanup_owner=%s\ncleanup_container_absent=true\nlayer2_remaining_exit=%s\n' \ + "$CONTAINER" "$status" +exit "$status" diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-source-contract.sh b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-source-contract.sh index 4d490e1..6d052df 100755 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-source-contract.sh +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-source-contract.sh @@ -4,7 +4,7 @@ set -euo pipefail RUN_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" IMAGE="${M9_A0_IMAGE:-tg-worker:dev}" PATCH="$RUN_DIR/evidence/openclaw-2026.4.14-trusted-native-boundaries.patch" -EXPECTED_PATCH_SHA256="eb79f2c2c296f528ec93f1ce38581780b4d17afd566ba10fd3f07333169dd34d" +EXPECTED_PATCH_SHA256="3c81b8333aa23772989763a2328396e3132d424ce438f81e841f9e3bfc6019eb" EXPECTED_VERSION="OpenClaw 2026.4.14 (2f35b6f)" CONTAINER="tiangong-m9a0-source-$$_$(date -u +%Y%m%dT%H%M%SZ)" @@ -53,7 +53,9 @@ docker run --rm --name "$CONTAINER" --network none \ src/agents/pi-embedded-runner/trusted-boundaries.test.ts \ src/agents/pi-embedded-runner/trusted-boundaries.provider.test.ts \ src/agents/pi-embedded-runner/run/attempt.ts \ - src/agents/pi-embedded-runner/compact.ts + src/agents/pi-embedded-runner/run.ts \ + src/agents/pi-embedded-runner/compact.ts \ + src/agents/pi-embedded-runner/compact.queued.ts cat > tsconfig.m9-a0.json <<"JSON" { From beba37cd459418c77daf93a5b78da41e1c916881 Mon Sep 17 00:00:00 2001 From: Jay Shen Date: Sun, 23 Aug 2026 14:58:23 +0800 Subject: [PATCH 08/13] test: add M9-A0 Layer 3 prototype Signed-off-by: Jay Shen --- .../evidence/artifact-sha256.txt | 3 + .../evidence/layer3-case-results.txt | 61 ++ .../evidence/layer3-control-handler.test.ts | 671 ++++++++++++++++++ .../evidence/layer3-results.txt | 40 ++ .../plan.md | 9 +- .../result.md | 53 +- .../run-layer3-control-handler.sh | 70 ++ 7 files changed, 901 insertions(+), 6 deletions(-) create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer3-case-results.txt create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer3-control-handler.test.ts create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer3-results.txt create mode 100755 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer3-control-handler.sh diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/artifact-sha256.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/artifact-sha256.txt index d045379..4875f1d 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/artifact-sha256.txt +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/artifact-sha256.txt @@ -11,6 +11,9 @@ de4f2cb56f30ffb356ed66234af7f57f015d9e475e4ea955fea3bc6de9b5bef8 smoke-testing/ 9c580e8435bb6aae227e9af6da51f6d7563670779adaeec29cd2867c6f911c07 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-run-embedded-attempt-baseline.test.ts 06a77efe011681fd7baea23bf3b993589ecf5268a40839dad8dd54916461ed1c smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-run-embedded-attempt.test.ts 2b4090ada287bd20eb3b6ea45365ee065a023f72b35f2701da8060e4350efcb3 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-tool-error.test.ts +f852008a1571ffbe01f3eb626ccaf4242ded470705828cbe627315b7caa68510 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer3-case-results.txt +62b91062b9043076e3f0c061f5cbb8b0eaf78f23759f8cc2e7d50828156e7c36 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer3-control-handler.test.ts +a04fe972d174d2b49f6cab0542293e435737d8c1a35d1bb17a3d54842d5d6462 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer3-results.txt 3c81b8333aa23772989763a2328396e3132d424ce438f81e841f9e3bfc6019eb smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/openclaw-2026.4.14-trusted-native-boundaries.patch 62bc7f13b5f19bff177597e2a26c20f1aa7ae36a12f317eba4aa611612d0eacd smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/source-contract-results.txt dcbff559f4cdf28bc2118de22edbbd5f228dabfe7e69afeae1a2354cb2480ed5 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/trusted-boundaries.provider.test.ts diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer3-case-results.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer3-case-results.txt new file mode 100644 index 0000000..da1c521 --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer3-case-results.txt @@ -0,0 +1,61 @@ +layer=3 +status=pass +image=tg-worker:dev +image_id=sha256:ac1cb183e5b2c82982f6473fef86ccf128763a31192d711526d843a79edb69ff +openclaw=OpenClaw 2026.4.14 (2f35b6f) +network=none +patch_sha256=3c81b8333aa23772989763a2328396e3132d424ce438f81e841f9e3bfc6019eb +activation=TIANGONG_TRUSTED_BOUNDARIES_REQUIRED=1 +runner_container=tiangong-m9a0-layer3-control-handler-581787_20260823T065127Z +runner_exit=0 +vitest_files=1 +vitest_tests=3 + +handler_identity_registry_plugin_id=tiangong-control +handler_identity_registry_plugin_count=1 +handler_identity_before_model_call_count=1 +handler_identity_before_tool_result_release_count=1 +handler_identity_event_observed=true + +valid_bootstrap_success_and_error=pass +valid_provider_requests=3 +valid_model_handler_events=3 +valid_tool_executions=2 +tool_execution_isError_sequence=false,true +valid_capture_start_events=2 +valid_capture_shape_events=2 +valid_capture_shape_all_tool_call_content_session_fields=true +valid_capture_closed_events=2 +valid_capture_closed_outcome_sequence=success,error +valid_spool_result_records=2 +valid_spool_result_outcome_sequence=success,error +valid_provider_request_bootstrap_provenance=exact_agents_soul_bundle_digest +valid_event_order=attempt-ready,model-handler,provider-request,tool-executed,capture-start,capture-shape,capture-closed,model-handler,provider-request,tool-executed,capture-start,capture-shape,capture-closed,model-handler,provider-request +valid_capture_closed_before_follow_up_requests=true + +corrupted_bootstrap=pass +corrupt_provider_requests=0 +corrupt_bootstrap_denied_events=1 +corrupt_model_handler_events=0 +corrupt_provider_request_events=0 +bootstrap_denial_reason=TRUSTED_BOOTSTRAP_INVALID + +capture_failure=pass +failure_provider_requests=1 +failure_model_handler_events=1 +failure_tool_executions=1 +failure_capture_start_events=1 +failure_capture_shape_tool_call_content_session_fields=true +failure_capture_closed_events=0 +failure_recovery_required_events=1 +failure_recovery_reason=TOOL_RESULT_CAPTURE_FAILED +failure_recovery_code=EEXIST +failure_next_provider_request=not_observed +failure_spool_result_records=0 +capture_failure_event_order=attempt-ready,model-handler,provider-request,tool-executed,capture-start,capture-shape,recovery-required + +cleanup_owner=tiangong-m9a0-layer3-control-handler-581787_20260823T065127Z +cleanup_container_absent=true +owned_container_prefix_absent=true +external_resources=none +credentials=none diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer3-control-handler.test.ts b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer3-control-handler.test.ts new file mode 100644 index 0000000..ddbc3fe --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer3-control-handler.test.ts @@ -0,0 +1,671 @@ +import { createHash } from "node:crypto"; +import fs from "node:fs/promises"; +import fsSync from "node:fs"; +import http from "node:http"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { AuthStorage, ModelRegistry } from "@mariozechner/pi-coding-agent"; +import { loadOpenClawPlugins, clearPluginLoaderCache } from "../../plugins/loader.js"; +import { + getGlobalPluginRegistry, + resetGlobalHookRunner, +} from "../../plugins/hook-runner-global.js"; +import { runEmbeddedAttempt } from "./run/attempt.js"; +import type { Model } from "@mariozechner/pi-ai"; + +const tempRoots: string[] = []; +const servers: http.Server[] = []; +const ATTEMPT_STARTUP_TIMEOUT_MS = 120_000; +const PROVIDER_RESPONSE_TIMEOUT_MS = 30_000; +const CAPTURE_MODULE = "file:///tiangong/worker/agent/gates/tool-result-capture.mjs"; +const STORE_MODULE = "file:///tiangong/worker/agent/gates/tool-result-store.mjs"; + +function digest(value: string): string { + return createHash("sha256").update(value).digest("hex"); +} + +function appendEvent(filePath: string, event: string, fields: Record = {}): void { + fsSync.appendFileSync(filePath, `${JSON.stringify({ event, ...fields })}\n`); +} + +async function withTimeout(params: { + promise: Promise; + timeoutMs: number; + onTimeout: () => Error; +}): Promise { + let timer: NodeJS.Timeout | undefined; + const timeout = new Promise((_, reject) => { + timer = setTimeout(() => reject(params.onTimeout()), params.timeoutMs); + }); + try { + return await Promise.race([params.promise, timeout]); + } finally { + if (timer) clearTimeout(timer); + } +} + +async function makeRoot(): Promise { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "m9-a0-layer3-")); + tempRoots.push(root); + return root; +} + +function sse(value: Record): string { + return `data: ${JSON.stringify(value)}\n\n`; +} + +async function listenFakeProvider(eventsFile: string): Promise<{ + baseUrl: string; + requestBodies: Array>; +}> { + const requestBodies: Array> = []; + const server = http.createServer(async (_request, response) => { + const chunks: Buffer[] = []; + for await (const chunk of _request) chunks.push(Buffer.from(chunk)); + const body = JSON.parse(Buffer.concat(chunks).toString("utf8")) as Record; + requestBodies.push(body); + const requestNumber = requestBodies.length; + appendEvent(eventsFile, "provider-request", { requestNumber }); + response.writeHead(200, { + "content-type": "text/event-stream", + connection: "keep-alive", + "cache-control": "no-cache", + }); + if (requestNumber === 1 || requestNumber === 2) { + const callId = requestNumber === 1 ? "call-layer3-success" : "call-layer3-error"; + response.write( + sse({ + id: `m9-a0-layer3-${requestNumber}`, + object: "chat.completion.chunk", + created: 1, + model: "fake-model", + choices: [ + { + index: 0, + delta: { + role: "assistant", + tool_calls: [ + { + index: 0, + id: callId, + type: "function", + function: { name: "synthetic", arguments: "{}" }, + }, + ], + }, + finish_reason: null, + }, + ], + }), + ); + response.write( + sse({ + id: `m9-a0-layer3-${requestNumber}`, + object: "chat.completion.chunk", + created: 1, + model: "fake-model", + choices: [{ index: 0, delta: {}, finish_reason: "tool_calls" }], + }), + ); + } else { + response.write( + sse({ + id: "m9-a0-layer3-final", + object: "chat.completion.chunk", + created: 1, + model: "fake-model", + choices: [ + { index: 0, delta: { role: "assistant", content: "done" }, finish_reason: null }, + ], + }), + ); + response.write( + sse({ + id: "m9-a0-layer3-final", + object: "chat.completion.chunk", + created: 1, + model: "fake-model", + choices: [{ index: 0, delta: {}, finish_reason: "stop" }], + }), + ); + } + response.end("data: [DONE]\n\n"); + }); + servers.push(server); + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", resolve); + }); + const address = server.address(); + if (!address || typeof address === "string") throw new Error("FAKE_PROVIDER_ADDRESS_MISSING"); + return { baseUrl: `http://127.0.0.1:${address.port}/v1`, requestBodies }; +} + +function model(baseUrl: string): Model<"openai-completions"> { + return { + id: "fake-model", + name: "M9 A0 Layer 3 fake model", + api: "openai-completions", + provider: "fake-provider", + baseUrl, + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 32768, + maxTokens: 256, + }; +} + +function writePlugin(root: string): Promise { + const plugin = path.join(root, "tiangong-control.cjs"); + const manifest = path.join(root, "openclaw.plugin.json"); + return Promise.all([ + fs.writeFile( + manifest, + JSON.stringify({ + id: "tiangong-control", + providers: ["fake-provider"], + configSchema: { type: "object", properties: {} }, + }), + "utf8", + ), + fs.writeFile( + plugin, + `const fs = require("node:fs"); +const crypto = require("node:crypto"); +function emit(event, fields) { + fs.appendFileSync(process.env.M9_A0_EVENTS_FILE, JSON.stringify({ event, ...fields }) + "\\n"); +} +function sha256(value) { + return crypto.createHash("sha256").update(value).digest("hex"); +} +function readImmutable(file, expected, label) { + if (!file || !expected) throw new Error("TRUSTED_BOOTSTRAP_MISSING"); + const stat = fs.statSync(file); + if (!stat.isFile() || (stat.mode & 0o222) !== 0) throw new Error("TRUSTED_BOOTSTRAP_NOT_IMMUTABLE"); + const content = fs.readFileSync(file, "utf8"); + if (Buffer.byteLength(content, "utf8") > 4096 || sha256(content) !== expected) { + throw new Error("TRUSTED_BOOTSTRAP_DIGEST_MISMATCH"); + } + return { label, content, digest: expected }; +} +function readBootstrap() { + const agents = readImmutable( + process.env.M9_A0_AGENTS_FILE, + process.env.M9_A0_AGENTS_DIGEST, + "AGENTS.md", + ); + const soul = readImmutable( + process.env.M9_A0_SOUL_FILE, + process.env.M9_A0_SOUL_DIGEST, + "SOUL.md", + ); + return { + agents, + soul, + bundleDigest: sha256(JSON.stringify({ agents: agents.digest, soul: soul.digest })), + }; +} +let toolInvocation = 0; +let captureHook; +function captureFailureCode(error) { + if (error && typeof error.code === "string" && ["EACCES", "ENOTDIR", "ENOENT", "EEXIST", "ERR_MODULE_NOT_FOUND"].includes(error.code)) { + return error.code; + } + if (error && error.message === "TOOL_RESULT_CAPTURE_GAP") return "TOOL_RESULT_CAPTURE_GAP"; + return error && typeof error.name === "string" ? "ERROR_" + error.name : "ERROR_UNKNOWN"; +} +async function capture(event, ctx) { + emit("capture-start", { toolName: event.toolName, toolCallId: event.toolCallId }); + emit("capture-shape", { + toolCallIdPresent: typeof event.toolCallId === "string" && event.toolCallId.length > 0, + contentArray: Array.isArray(event.result && event.result.content), + sessionKeyPresent: typeof ctx.sessionKey === "string" && ctx.sessionKey.length > 0, + }); + try { + const module = await import(process.env.M9_A0_CAPTURE_MODULE); + if (!captureHook) { + captureHook = module.createToolResultCaptureHook({ + filePath: process.env.M9_A0_CAPTURE_FILE, + now: () => new Date("2026-08-23T00:00:00.000Z"), + onRecord: (record) => emit("capture-closed", { + toolResultId: record.toolResultId, + outcome: record.resultSummary.outcome, + }), + }); + } + captureHook( + { + toolName: event.toolName, + toolCallId: event.toolCallId, + outcome: event.isError ? "error" : "success", + message: { role: "toolResult", content: event.result.content }, + }, + { + actorId: "m9-a0-member", + workId: "m9-a0-work", + taskId: "m9-a0-task", + runtimeProfile: "m9-a0-layer3", + sessionKey: ctx.sessionKey || "m9-a0-layer3-session", + }, + ); + } catch (error) { + emit("recovery-required", { + reason: "TOOL_RESULT_CAPTURE_FAILED", + failureCode: captureFailureCode(error), + }); + throw new Error("TOOL_RESULT_CAPTURE_FAILED"); + } +} +module.exports = { + id: "tiangong-control", + register(api) { + emit("handler-identity", { pluginId: "tiangong-control" }); + api.registerProvider({ id: "fake-provider", label: "M9 A0 Layer 3 fake provider", auth: [] }); + api.registerTool({ + name: "synthetic", + label: "synthetic", + description: "M9 A0 Layer 3 synthetic tool", + parameters: { type: "object", properties: {}, additionalProperties: false }, + execute: async () => { + toolInvocation += 1; + const isError = toolInvocation === 2; + emit("tool-executed", { invocation: toolInvocation, isError }); + if (isError) throw new Error("fixture-tool-error"); + return { content: [{ type: "text", text: "synthetic-ok" }], details: { status: "ok" } }; + }, + }); + api.on("before_prompt_build", async () => { + emit("attempt-ready", {}); + const readyEvent = process.env.M9_A0_ATTEMPT_READY_EVENT; + if (readyEvent) process.emit(readyEvent); + }); + api.on("before_model_call", async (event) => { + let bootstrap; + try { + bootstrap = readBootstrap(); + } catch { + emit("bootstrap-denied", { reason: "TRUSTED_BOOTSTRAP_INVALID" }); + throw new Error("TRUSTED_BOOTSTRAP_INVALID"); + } + emit("model-handler", { provider: event.provider, model: event.model }); + return { + allow: true, + payload: { + ...event.payload, + m9Layer3Handler: "tiangong-control", + m9Bootstrap: bootstrap, + }, + }; + }); + api.on("before_tool_result_release", async (event, ctx) => { + await capture(event, ctx); + return { release: true }; + }); + }, +}; +`, + "utf8", + ), + ]).then(() => plugin); +} + +async function readEvents(eventsFile: string): Promise>> { + const content = await fs.readFile(eventsFile, "utf8").catch(() => ""); + return content + .trim() + .split("\n") + .filter(Boolean) + .map((line) => JSON.parse(line) as Record); +} + +async function readToolResults(filePath: string): Promise>> { + const { ToolResultStore } = await import(/* @vite-ignore */ STORE_MODULE); + const store = new ToolResultStore({ filePath }); + const state = await store.list(); + return state.results as Array>; +} + +async function diagnostic( + fixture: Awaited>, + result: Awaited>, +): Promise { + const events = await readEvents(fixture.eventsFile); + return `promptError=${result.promptError ? "present" : "none"} promptErrorSource=${result.promptErrorSource} providerRequests=${fixture.provider.requestBodies.length} events=${events.map((event) => [event.event, event.failureCode, event.toolCallIdPresent, event.contentArray, event.sessionKeyPresent].filter((value) => value !== undefined).join(":")).join(",")}`; +} + +function config(pluginFile: string): Record { + return { + plugins: { + allow: ["tiangong-control"], + load: { paths: [pluginFile] }, + }, + }; +} + +async function prepareFixture(): Promise<{ + root: string; + eventsFile: string; + sessionFile: string; + agentsFile: string; + soulFile: string; + agentsDigest: string; + soulDigest: string; + pluginFile: string; + provider: Awaited>; + captureFile: string; +}> { + const root = await makeRoot(); + const eventsFile = path.join(root, "events.ndjson"); + const agentsFile = path.join(root, "AGENTS.md"); + const soulFile = path.join(root, "SOUL.md"); + const agentsContent = "# M9-A0 Layer 3 controller\\nBounded trusted bootstrap.\\n"; + const soulContent = "# M9-A0 Layer 3 stance\\nEvidence before claims.\\n"; + await fs.writeFile(agentsFile, agentsContent, { mode: 0o444 }); + await fs.writeFile(soulFile, soulContent, { mode: 0o444 }); + await fs.chmod(agentsFile, 0o444); + await fs.chmod(soulFile, 0o444); + const pluginFile = await writePlugin(root); + const provider = await listenFakeProvider(eventsFile); + const sessionFile = path.join(root, "session.jsonl"); + await fs.writeFile(sessionFile, "", "utf8"); + return { + root, + eventsFile, + sessionFile, + agentsFile, + soulFile, + agentsDigest: digest(agentsContent), + soulDigest: digest(soulContent), + pluginFile, + provider, + captureFile: path.join(root, "tool-results", "openclaw.json"), + }; +} + +function saveEnv(): Record { + return Object.fromEntries( + [ + "TIANGONG_TRUSTED_BOUNDARIES_REQUIRED", + "M9_A0_EVENTS_FILE", + "M9_A0_ATTEMPT_READY_EVENT", + "M9_A0_CAPTURE_MODULE", + "M9_A0_CAPTURE_FILE", + "M9_A0_AGENTS_FILE", + "M9_A0_SOUL_FILE", + "M9_A0_AGENTS_DIGEST", + "M9_A0_SOUL_DIGEST", + ].map((key) => [key, process.env[key]]), + ); +} + +function restoreEnv(previous: Record): void { + for (const [key, value] of Object.entries(previous)) { + if (value === undefined) delete process.env[key]; + else process.env[key] = value; + } +} + +async function loadFixture(fixture: Awaited>): Promise { + const registry = loadOpenClawPlugins({ + cache: false, + workspaceDir: fixture.root, + config: config(fixture.pluginFile), + }); + const controlPlugin = registry.plugins.find((plugin) => plugin.id === "tiangong-control"); + expect(controlPlugin?.status).toBe("loaded"); + expect(registry.plugins.filter((plugin) => plugin.id === "tiangong-control")).toHaveLength(1); + expect(registry.typedHooks.filter((hook) => hook.pluginId === "tiangong-control")).toHaveLength( + 3, + ); + expect(registry.typedHooks.filter((hook) => hook.hookName === "before_model_call")).toHaveLength( + 1, + ); + expect( + registry.typedHooks.filter((hook) => hook.hookName === "before_tool_result_release"), + ).toHaveLength(1); + expect(getGlobalPluginRegistry()).toBe(registry); +} + +async function runAttempt( + fixture: Awaited>, + runId: string, +): Promise>> { + const readyEvent = `m9-a0-layer3-ready:${process.pid}:${path.basename(fixture.root)}:${runId}`; + let markReady: () => void = () => undefined; + const ready = new Promise((resolve) => { + markReady = resolve; + }); + process.once(readyEvent, markReady); + process.env.M9_A0_ATTEMPT_READY_EVENT = readyEvent; + const authStorage = AuthStorage.inMemory(); + authStorage.setRuntimeApiKey("fake-provider", "fixture-provider-value"); + const abortController = new AbortController(); + const attemptPromise = runEmbeddedAttempt({ + sessionId: `m9-a0-layer3-session-${runId}`, + sessionKey: `agent:m9-a0:layer3:${runId}`, + sessionFile: fixture.sessionFile, + workspaceDir: fixture.root, + agentDir: fixture.root, + config: config(fixture.pluginFile), + prompt: "M9-A0 Layer 3 deterministic control-handler prototype", + timeoutMs: 10_000, + runId: `m9-a0-layer3-run-${runId}`, + provider: "fake-provider", + modelId: "fake-model", + model: model(fixture.provider.baseUrl), + resolvedApiKey: "fixture-provider-value", + authStorage, + modelRegistry: ModelRegistry.inMemory(authStorage) as never, + thinkLevel: "off", + senderIsOwner: true, + disableMessageTool: true, + disableTools: false, + toolsAllow: ["synthetic"], + abortSignal: abortController.signal, + }); + void attemptPromise.catch(() => undefined); + try { + await withTimeout({ + promise: Promise.race([ + ready, + attemptPromise.then((result) => { + throw new Error( + `ATTEMPT_COMPLETED_BEFORE_READINESS promptError=${result.promptError ? "present" : "none"}`, + ); + }), + ]), + timeoutMs: ATTEMPT_STARTUP_TIMEOUT_MS, + onTimeout: () => { + abortController.abort("M9_A0_LAYER3_STARTUP_TIMEOUT"); + return new Error( + `LAYER3_STARTUP_TIMEOUT providerRequests=${fixture.provider.requestBodies.length}`, + ); + }, + }); + return await withTimeout({ + promise: attemptPromise, + timeoutMs: PROVIDER_RESPONSE_TIMEOUT_MS, + onTimeout: () => { + abortController.abort("M9_A0_LAYER3_PROVIDER_RESPONSE_TIMEOUT"); + return new Error( + `LAYER3_PROVIDER_RESPONSE_TIMEOUT providerRequests=${fixture.provider.requestBodies.length}`, + ); + }, + }); + } finally { + process.off(readyEvent, markReady); + } +} + +async function setupEnv( + fixture: Awaited>, + captureFile = fixture.captureFile, +): Promise> { + const previous = saveEnv(); + process.env.TIANGONG_TRUSTED_BOUNDARIES_REQUIRED = "1"; + process.env.M9_A0_EVENTS_FILE = fixture.eventsFile; + process.env.M9_A0_ATTEMPT_READY_EVENT = ""; + process.env.M9_A0_CAPTURE_MODULE = CAPTURE_MODULE; + process.env.M9_A0_CAPTURE_FILE = captureFile; + process.env.M9_A0_AGENTS_FILE = fixture.agentsFile; + process.env.M9_A0_SOUL_FILE = fixture.soulFile; + process.env.M9_A0_AGENTS_DIGEST = fixture.agentsDigest; + process.env.M9_A0_SOUL_DIGEST = fixture.soulDigest; + return previous; +} + +afterEach(async () => { + resetGlobalHookRunner(); + clearPluginLoaderCache(); + for (const server of servers.splice(0)) { + if (server.listening) { + await new Promise((resolve) => server.close(() => resolve())); + } + } + while (tempRoots.length > 0) await fs.rm(tempRoots.pop()!, { recursive: true, force: true }); + for (const key of [ + "TIANGONG_TRUSTED_BOUNDARIES_REQUIRED", + "M9_A0_EVENTS_FILE", + "M9_A0_ATTEMPT_READY_EVENT", + "M9_A0_CAPTURE_MODULE", + "M9_A0_CAPTURE_FILE", + "M9_A0_AGENTS_FILE", + "M9_A0_SOUL_FILE", + "M9_A0_AGENTS_DIGEST", + "M9_A0_SOUL_DIGEST", + ]) + delete process.env[key]; +}); + +describe("M9-A0 Layer 3 Tiangong control-handler prototype", () => { + it("verifies immutable bootstrap and closes success/error ToolResults before follow-up requests", async () => { + const fixture = await prepareFixture(); + const previous = await setupEnv(fixture); + try { + await loadFixture(fixture); + const result = await runAttempt(fixture, "success-error"); + if (result.promptError !== null || fixture.provider.requestBodies.length !== 3) { + throw new Error(`LAYER3_SUCCESS_ERROR_DIAGNOSTIC:${await diagnostic(fixture, result)}`); + } + expect(result.promptError).toBeNull(); + expect(fixture.provider.requestBodies).toHaveLength(3); + for (const body of fixture.provider.requestBodies) { + expect(body).toMatchObject({ + m9Layer3Handler: "tiangong-control", + m9Bootstrap: { + agents: { + label: "AGENTS.md", + content: "# M9-A0 Layer 3 controller\\nBounded trusted bootstrap.\\n", + digest: fixture.agentsDigest, + }, + soul: { + label: "SOUL.md", + content: "# M9-A0 Layer 3 stance\\nEvidence before claims.\\n", + digest: fixture.soulDigest, + }, + bundleDigest: digest( + JSON.stringify({ agents: fixture.agentsDigest, soul: fixture.soulDigest }), + ), + }, + }); + } + const events = await readEvents(fixture.eventsFile); + const names = events.map((event) => event.event); + expect(events.some((event) => event.event === "handler-identity")).toBe(true); + expect(events.find((event) => event.event === "handler-identity")?.pluginId).toBe( + "tiangong-control", + ); + expect(names.filter((name) => name === "provider-request")).toHaveLength(3); + expect(names.filter((name) => name === "tool-executed")).toHaveLength(2); + expect(names.filter((name) => name === "capture-closed")).toHaveLength(2); + expect(names.indexOf("capture-closed")).toBeLessThan( + names.indexOf("provider-request", names.indexOf("provider-request") + 1), + ); + expect(names.lastIndexOf("capture-closed")).toBeLessThan( + names.lastIndexOf("provider-request"), + ); + expect( + events + .filter((event) => event.event === "capture-shape") + .map((event) => ({ + toolCallIdPresent: event.toolCallIdPresent, + contentArray: event.contentArray, + sessionKeyPresent: event.sessionKeyPresent, + })), + ).toEqual([ + { toolCallIdPresent: true, contentArray: true, sessionKeyPresent: true }, + { toolCallIdPresent: true, contentArray: true, sessionKeyPresent: true }, + ]); + expect( + events.filter((event) => event.event === "capture-closed").map((event) => event.outcome), + ).toEqual(["success", "error"]); + expect( + events.find((event) => event.event === "tool-executed" && event.invocation === 2)?.isError, + ).toBe(true); + const records = await readToolResults(fixture.captureFile); + expect(records).toHaveLength(2); + expect( + records.map((record) => (record.resultSummary as { outcome: string }).outcome), + ).toEqual(["success", "error"]); + expect(records.map((record) => record.tool)).toEqual(["synthetic", "synthetic"]); + } finally { + restoreEnv(previous); + } + }, 240_000); + + it("denies a corrupted immutable bootstrap before any provider request", async () => { + const fixture = await prepareFixture(); + const previous = await setupEnv(fixture); + try { + await fs.chmod(fixture.agentsFile, 0o644); + await fs.writeFile(fixture.agentsFile, "# corrupted controller\\n", "utf8"); + await fs.chmod(fixture.agentsFile, 0o444); + await loadFixture(fixture); + await runAttempt(fixture, "corrupt-bootstrap"); + expect(fixture.provider.requestBodies).toEqual([]); + const events = await readEvents(fixture.eventsFile); + expect(events.some((event) => event.event === "handler-identity")).toBe(true); + expect(events.filter((event) => event.event === "bootstrap-denied")).toHaveLength(1); + expect(events.some((event) => event.event === "model-handler")).toBe(false); + expect(events.some((event) => event.event === "provider-request")).toBe(false); + expect(await readToolResults(fixture.captureFile)).toEqual([]); + } finally { + restoreEnv(previous); + } + }, 180_000); + + it("signals recovery-required and blocks the next provider request when the spool write fails", async () => { + const fixture = await prepareFixture(); + const blocker = path.join(fixture.root, "spool-blocker"); + await fs.writeFile(blocker, "not-a-directory", "utf8"); + const previous = await setupEnv(fixture, path.join(blocker, "openclaw.json")); + try { + await loadFixture(fixture); + await runAttempt(fixture, "capture-failure"); + expect(fixture.provider.requestBodies).toHaveLength(1); + const events = await readEvents(fixture.eventsFile); + const names = events.map((event) => event.event); + expect(names.filter((name) => name === "model-handler")).toHaveLength(1); + expect(names.filter((name) => name === "provider-request")).toHaveLength(1); + expect(names.filter((name) => name === "tool-executed")).toHaveLength(1); + expect(names.filter((name) => name === "capture-start")).toHaveLength(1); + expect(names.filter((name) => name === "recovery-required")).toHaveLength(1); + expect(events.find((event) => event.event === "recovery-required")).toMatchObject({ + reason: "TOOL_RESULT_CAPTURE_FAILED", + failureCode: "EEXIST", + }); + expect(events.find((event) => event.event === "capture-shape")).toMatchObject({ + toolCallIdPresent: true, + contentArray: true, + sessionKeyPresent: true, + }); + expect(names.includes("capture-closed")).toBe(false); + expect(names.includes("provider-request", names.indexOf("provider-request") + 1)).toBe(false); + expect(await readToolResults(fixture.captureFile)).toEqual([]); + } finally { + restoreEnv(previous); + } + }, 180_000); +}); diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer3-results.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer3-results.txt new file mode 100644 index 0000000..79abb5e --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer3-results.txt @@ -0,0 +1,40 @@ +container=tiangong-m9a0-layer3-control-handler-581787_20260823T065127Z +image=tg-worker:dev +image_id=sha256:ac1cb183e5b2c82982f6473fef86ccf128763a31192d711526d843a79edb69ff +image_created=2026-08-21T13:00:21.057325912+08:00 +openclaw_version=OpenClaw 2026.4.14 (2f35b6f) +patch_sha256=3c81b8333aa23772989763a2328396e3132d424ce438f81e841f9e3bfc6019eb +checking file src/agents/pi-embedded-runner/compact.queued.ts +checking file src/agents/pi-embedded-runner/compact.ts +checking file src/agents/pi-embedded-runner/run.ts +checking file src/agents/pi-embedded-runner/run/attempt.ts +checking file src/agents/pi-embedded-runner/trusted-boundaries.ts +checking file src/plugins/hook-types.ts +patching file src/agents/pi-embedded-runner/compact.queued.ts +patching file src/agents/pi-embedded-runner/compact.ts +patching file src/agents/pi-embedded-runner/run.ts +patching file src/agents/pi-embedded-runner/run/attempt.ts +patching file src/agents/pi-embedded-runner/trusted-boundaries.ts +patching file src/plugins/hook-types.ts +Checking formatting... + +All matched files use the correct format. +Finished in 37ms on 1 files using 16 threads. +No config found, using defaults. Please add a config file or try `oxfmt --init` if needed. + + RUN v4.1.4 /opt/openclaw + +stderr | src/agents/pi-embedded-runner/layer3-control-handler.test.ts > M9-A0 Layer 3 Tiangong control-handler prototype > verifies immutable bootstrap and closes success/error ToolResults before follow-up requests + + ✓ src/agents/pi-embedded-runner/layer3-control-handler.test.ts > M9-A0 Layer 3 Tiangong control-handler prototype > verifies immutable bootstrap and closes success/error ToolResults before follow-up requests 87200ms + ✓ src/agents/pi-embedded-runner/layer3-control-handler.test.ts > M9-A0 Layer 3 Tiangong control-handler prototype > denies a corrupted immutable bootstrap before any provider request 755ms + ✓ src/agents/pi-embedded-runner/layer3-control-handler.test.ts > M9-A0 Layer 3 Tiangong control-handler prototype > signals recovery-required and blocks the next provider request when the spool write fails 827ms + + Test Files 1 passed (1) + Tests 3 passed (3) + Start at 06:51:28 + Duration 94.14s (transform 3.71s, setup 0ms, import 5.29s, tests 88.78s, environment 0ms) + +cleanup_owner=tiangong-m9a0-layer3-control-handler-581787_20260823T065127Z +cleanup_container_absent=true +layer3_control_handler_exit=0 diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/plan.md b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/plan.md index a791253..04df486 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/plan.md +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/plan.md @@ -1,6 +1,6 @@ # M9-A0 trusted native-boundary follow-up -> Status: Layer 1 and Layer 2 pass for the research candidate. In required mode, model-backed compaction is intentionally unavailable and fails closed; Layer 3 is eligible but has not started. +> Status: Layer 1, Layer 2, and the disposable Layer 3 control-handler prototype pass for the research candidate. In required mode, model-backed compaction is intentionally unavailable and fails closed; Layer 4 remains unauthorized. ## Scope @@ -99,10 +99,17 @@ Current result: **PASS** for the main tool turn, normalized tool error, capture Proceed only from the passing Layer 2 result. +```bash +set -o pipefail +smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer3-control-handler.sh \ + 2>&1 | tee smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer3-results.txt +``` + - Use a bounded test bootstrap and an owned temporary spool beneath a unique temporary root. - Prove exact handler identity, bootstrap corruption denial, success/error capture, capture failure, recovery-required signaling, and no next provider request. - Reuse the planned ToolResult spool contract; do not add a table or a new ledger. - This remains a disposable prototype and must not delete the current product runtime paths. +- Current result: **PASS**, 3/3 tests; valid bootstrap produced 3 provider requests with success/error ToolResults closed to 2 spool records, corruption produced 0 provider requests, and capture failure produced 1 provider request followed by recovery-required with no next request. - A deterministic session-rollover prototype, if needed, must be a separate focused boundary with no provider call and reconstruction from direct durable facts. ### Layer 4: Basic Matrix member turn diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/result.md b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/result.md index 1b9f6ba..f0f673b 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/result.md +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/result.md @@ -2,13 +2,13 @@ ## Decision -**Layer 1 and Layer 2 pass for the revised research candidate. Model-backed compaction is fail-closed and unavailable in required mode.** +**Layer 1, Layer 2, and the disposable Layer 3 control-handler prototype pass for the revised research candidate. Model-backed compaction is fail-closed and unavailable in required mode.** The initial real compaction diagnostic proved that pinned `pi-coding-agent` calls `completeSimple(...)` outside the session `agent.onPayload` path. The separately installed handler could not protect that provider call. Following an explicit source-patch decision, the candidate now disables Pi AgentSession auto-compaction and denies OpenClaw manual, budget, overflow, queued/harness, and context-engine-owned compaction paths in required mode. This is not semantic compaction support. Manual compaction remains disabled because changing the trigger does not change the provider boundary. A future manual canary requires a reviewed upstream/dependency payload seam; deterministic session rollover remains separate future work. -Layer 3 is now eligible but was not started in this change. M9-A formal implementation remains blocked until the disposable Tiangong control-handler prototype passes. Matrix Layer 4 remains unauthorized. +Layer 3 now passes as a disposable Tiangong control-handler prototype. M9-A formal implementation remains blocked until the complete A0 sequence is reviewed and Layer 4 Basic Matrix is separately authorized. Matrix Layer 4 remains unauthorized. The candidate remains a research artifact. No Tiangong runtime, installed OpenClaw tree, dependency, database, provider service, Matrix resource, or external service was changed. @@ -101,6 +101,46 @@ The source candidate also checks the same guard before timeout- and overflow-rec The activation-absent adjacent case also passed: stock compaction made one provider request and persisted a compaction entry. This proves the guard does not change stock OpenClaw behavior when required mode is absent. +## Layer 3 control-handler prototype + +**PASS** via [`evidence/layer3-results.txt`](evidence/layer3-results.txt), [`evidence/layer3-case-results.txt`](evidence/layer3-case-results.txt), and [`evidence/layer3-control-handler.test.ts`](evidence/layer3-control-handler.test.ts). + +The test uses the real OpenClaw plugin loader and `runEmbeddedAttempt`, an owned loopback provider in the same `--network none` container, a bounded `AGENTS.md`/`SOUL.md` bootstrap bundle, and the existing Tiangong `ToolResultStore`/capture contract imported read-only from the repository. It does not alter Worker runtime code or create a new ledger/table. + +### Exact handler identity and valid bootstrap + +- loaded plugin identity: exactly one `tiangong-control` plugin in the loader registry; +- required handler counts: exactly one `before_model_call` and exactly one `before_tool_result_release`; +- the trusted handler returned exact bounded bootstrap provenance for both `AGENTS.md` and `SOUL.md`, including their digests and bundle digest; +- provider requests: **3**; +- tool executions: **2**, normalized `isError` sequence `false → true`; +- capture closes: **2**, outcomes `success → error`; +- existing ToolResult spool records: **2**, outcomes `success → error`; +- each capture had a tool call ID, content array, and session key before release; +- each capture closed before the subsequent provider request. + +### Bootstrap corruption denial + +- bootstrap digest/immutability validation failed closed; +- provider requests: **0**; +- model-handler events: **0**; +- bootstrap-denied events: **1**; +- no ToolResult spool record was created. + +### Capture failure and recovery-required signaling + +The prototype pointed the existing spool writer at an owned path whose parent was a regular file, causing a deterministic `EEXIST` write failure: + +- provider requests: **1**; +- tool executions: **1**; +- capture starts: **1**; +- capture closes: **0**; +- recovery-required events: **1**, reason `TOOL_RESULT_CAPTURE_FAILED`, failure code `EEXIST`; +- next provider request: **not observed**; +- ToolResult spool records: **0**. + +The prototype remains disposable. Its recovery-required event is bounded test control signaling, not a new authoritative recovery ledger. + ## Historical compaction bypass The pre-guard diagnostic is retained as bounded failure evidence: @@ -114,11 +154,10 @@ The same dependency contains model-backed branch summarization, but source audit ## Not started and known limitations -- Layer 3 Tiangong control-handler prototype: **not started**; - deterministic session rollover from durable facts: **not implemented**; - semantic or manual model compaction: **not supported in required mode**; - Basic Matrix Layer 4: **not authorized**; -- M9-A formal implementation: **still blocked by incomplete A0 spike**. +- M9-A formal implementation: **still blocked pending A0 review and Layer 4 authorization**. Until rollover or a trusted compaction seam is implemented, long sessions may reach OpenClaw's ordinary context-overflow failure/recovery behavior. This run proves that compaction cannot issue an untrusted provider request; it does not prove a seamless long-session user experience. @@ -133,8 +172,12 @@ Until rollover or a trusted compaction seam is implemented, long sessions may re - [`evidence/layer2-compaction.test.ts`](evidence/layer2-compaction.test.ts) - [`evidence/layer2-compaction-bypass-diagnostic.test.ts`](evidence/layer2-compaction-bypass-diagnostic.test.ts) - [`evidence/layer2-pre-guard-results.txt`](evidence/layer2-pre-guard-results.txt) +- [`evidence/layer3-results.txt`](evidence/layer3-results.txt) +- [`evidence/layer3-case-results.txt`](evidence/layer3-case-results.txt) +- [`evidence/layer3-control-handler.test.ts`](evidence/layer3-control-handler.test.ts) - [`run-layer2-remaining.sh`](run-layer2-remaining.sh) +- [`run-layer3-control-handler.sh`](run-layer3-control-handler.sh) ## Cleanup -Each runner removed only its exact owned container and verified it absent. The final Docker inspection at `2026-08-23T06:04:42Z` verified that no exited or running container with the `tiangong-m9a0-` prefix remained. Temporary test roots and provider servers were removed by test teardown. No external resource or credential was created. +Each runner removed only its exact owned container and verified it absent. The final Docker inspection after the Layer 3 run verified that no exited or running container with the `tiangong-m9a0-` prefix remained. The Layer 3 runner owner was `tiangong-m9a0-layer3-control-handler-581787_20260823T065127Z` and was verified absent. Temporary test roots and provider servers were removed by test teardown. No external resource or credential was created. diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer3-control-handler.sh b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer3-control-handler.sh new file mode 100755 index 0000000..50aedd1 --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer3-control-handler.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +set -euo pipefail + +RUN_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$RUN_DIR/../../.." && pwd)" +IMAGE="${M9_A0_IMAGE:-tg-worker:dev}" +PATCH="$RUN_DIR/evidence/openclaw-2026.4.14-trusted-native-boundaries.patch" +EXPECTED_PATCH_SHA256="3c81b8333aa23772989763a2328396e3132d424ce438f81e841f9e3bfc6019eb" +EXPECTED_VERSION="OpenClaw 2026.4.14 (2f35b6f)" +TEST_FILTER="${M9_A0_LAYER3_FILTER:-}" +CONTAINER="tiangong-m9a0-layer3-control-handler-$$_$(date -u +%Y%m%dT%H%M%SZ)" + +cleanup() { + if docker container inspect "$CONTAINER" >/dev/null 2>&1; then + docker rm -f "$CONTAINER" >/dev/null + fi +} +trap cleanup EXIT INT TERM + +actual_patch_sha256="$(sha256sum "$PATCH" | awk '{print $1}')" +if [[ "$actual_patch_sha256" != "$EXPECTED_PATCH_SHA256" ]]; then + printf 'patch_sha256_mismatch expected=%s actual=%s\n' \ + "$EXPECTED_PATCH_SHA256" "$actual_patch_sha256" >&2 + exit 1 +fi + +image_id="$(docker image inspect --format '{{.Id}}' "$IMAGE")" +image_created="$(docker image inspect --format '{{.Created}}' "$IMAGE")" +version="$(docker run --rm --network none --entrypoint openclaw "$IMAGE" --version)" +if [[ "$version" != "$EXPECTED_VERSION" ]]; then + printf 'openclaw_version_mismatch expected=%q actual=%q\n' "$EXPECTED_VERSION" "$version" >&2 + exit 1 +fi + +printf 'container=%s\nimage=%s\nimage_id=%s\nimage_created=%s\nopenclaw_version=%s\npatch_sha256=%s\n' \ + "$CONTAINER" "$IMAGE" "$image_id" "$image_created" "$version" "$actual_patch_sha256" + +status=0 +docker run --name "$CONTAINER" --network none \ + --env "M9_A0_LAYER3_FILTER=$TEST_FILTER" \ + --mount "type=bind,src=$RUN_DIR,dst=/m9-a0,readonly" \ + --mount "type=bind,src=$REPO_ROOT,dst=/tiangong,readonly" \ + --entrypoint sh "$IMAGE" -lc ' + set -eu + cd /opt/openclaw + patch -p1 --dry-run < /m9-a0/evidence/openclaw-2026.4.14-trusted-native-boundaries.patch + patch -p1 < /m9-a0/evidence/openclaw-2026.4.14-trusted-native-boundaries.patch + cp /m9-a0/evidence/layer3-control-handler.test.ts \ + src/agents/pi-embedded-runner/layer3-control-handler.test.ts + ./node_modules/.bin/oxfmt --check \ + src/agents/pi-embedded-runner/layer3-control-handler.test.ts + if [ -n "${M9_A0_LAYER3_FILTER:-}" ]; then + ./node_modules/.bin/vitest run \ + src/agents/pi-embedded-runner/layer3-control-handler.test.ts \ + --reporter=verbose --hookTimeout=20000 --testNamePattern "$M9_A0_LAYER3_FILTER" + else + ./node_modules/.bin/vitest run \ + src/agents/pi-embedded-runner/layer3-control-handler.test.ts \ + --reporter=verbose --hookTimeout=20000 + fi + ' || status=$? + +cleanup +if docker container inspect "$CONTAINER" >/dev/null 2>&1; then + printf 'cleanup_container_still_present=%s\n' "$CONTAINER" >&2 + exit 1 +fi +printf 'cleanup_owner=%s\ncleanup_container_absent=true\nlayer3_control_handler_exit=%s\n' \ + "$CONTAINER" "$status" +exit "$status" From 33c21d33996790680c360bbf940457aabf008902 Mon Sep 17 00:00:00 2001 From: Jay Shen Date: Sun, 23 Aug 2026 17:38:28 +0800 Subject: [PATCH 09/13] test: stop M9-A0 at Layer 4 Matrix boundary Signed-off-by: Jay Shen --- .../evidence/artifact-sha256.txt | 18 ++ .../evidence/layer4-AGENTS.md | 3 + .../evidence/layer4-SOUL.md | 3 + .../evidence/layer4-case-results.txt | 39 +++ .../evidence/layer4-cleanup.txt | 16 + .../evidence/layer4-control-plugin.mjs | 121 +++++++ .../layer4-fifth-attempt-diagnostic.txt | 16 + .../layer4-first-attempt-diagnostic.txt | 10 + .../layer4-fourth-attempt-diagnostic.txt | 10 + .../evidence/layer4-matrix-turn.sh | 122 +++++++ .../evidence/layer4-plugin-loader-results.txt | 17 + .../evidence/layer4-plugin-loader.test.ts | 36 +++ .../evidence/layer4-research-image.Dockerfile | 43 +++ .../evidence/layer4-results.txt | 26 ++ .../layer4-second-attempt-diagnostic.txt | 10 + .../evidence/layer4-team.yaml | 11 + .../layer4-third-attempt-diagnostic.txt | 15 + .../evidence/layer4-tsdown.config.mjs | 14 + .../evidence/layer4-workers.yaml | 28 ++ .../plan.md | 14 +- .../result.md | 49 ++- .../run-layer4-basic-matrix.sh | 306 ++++++++++++++++++ 22 files changed, 918 insertions(+), 9 deletions(-) create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-AGENTS.md create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-SOUL.md create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-case-results.txt create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-cleanup.txt create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-control-plugin.mjs create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-fifth-attempt-diagnostic.txt create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-first-attempt-diagnostic.txt create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-fourth-attempt-diagnostic.txt create mode 100755 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-matrix-turn.sh create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-plugin-loader-results.txt create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-plugin-loader.test.ts create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-research-image.Dockerfile create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-results.txt create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-second-attempt-diagnostic.txt create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-team.yaml create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-third-attempt-diagnostic.txt create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-tsdown.config.mjs create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-workers.yaml create mode 100755 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer4-basic-matrix.sh diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/artifact-sha256.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/artifact-sha256.txt index 4875f1d..b468f99 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/artifact-sha256.txt +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/artifact-sha256.txt @@ -14,6 +14,24 @@ de4f2cb56f30ffb356ed66234af7f57f015d9e475e4ea955fea3bc6de9b5bef8 smoke-testing/ f852008a1571ffbe01f3eb626ccaf4242ded470705828cbe627315b7caa68510 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer3-case-results.txt 62b91062b9043076e3f0c061f5cbb8b0eaf78f23759f8cc2e7d50828156e7c36 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer3-control-handler.test.ts a04fe972d174d2b49f6cab0542293e435737d8c1a35d1bb17a3d54842d5d6462 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer3-results.txt +79f61971edfec23f65e7ef3628400e29c35c075472699c2492c98f193ffa7773 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-AGENTS.md +b422b455b335c641406e4197bcea3bfc9952f29c4bb2cd7d3ce093aff9ca66b0 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-case-results.txt +47019b4c7da7d4b2b6b155b133045b9babf82591cef0a1c6501164738bd75872 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-cleanup.txt +23fd93ff9161a41433054870ec7624e200b0d61cfb4f105f35a5cf09c942e090 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-control-plugin.mjs +0387efa14540d6882891ffc2cd98495e8a319b9cc9de9fed3efa76948da1e757 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-fifth-attempt-diagnostic.txt +e13b9daeb82342f7f8e1791919b26c336382c1fc3bccb19e59a11b0251435c90 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-first-attempt-diagnostic.txt +b4b672aa5a573c4555e8dbfa87984990e3ef236a665f6a803a44051f6daf57cf smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-fourth-attempt-diagnostic.txt +3b4eefc64a548791858499c5b36bfaf643fca0e2d0717926e26c59d7b1a891f1 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-matrix-turn.sh +84be417bf2ba084f55fa58f67389537c23ff8587cb95f06140a272194cdbae18 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-plugin-loader-results.txt +8f48d929955c994cd7c2314661459e6eb24b3004d14e89913a0a47995f40d010 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-plugin-loader.test.ts +f727ea57999e0ad7f5e0f018eadf59cbd77c6f2acfd11fb99f1bc477bf92ff2d smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-research-image.Dockerfile +206492cc9a16de2e14643b89a699eb2c523fb0763f14655255779349874aa0e3 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-results.txt +c70b24e2b669186d301772496a526353f90c17674f778455dcaddad11c992abf smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-second-attempt-diagnostic.txt +de2ecf87e8b565134ee4d03d2604073c2b5caefff2cd8dcf92c2dccaeb06ebd5 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-SOUL.md +e36c8331ad71db81fa63bb1e7bc4da5db6f231dba8e9abc83bb32a48436aca82 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-team.yaml +4e48dd8b81058d8371f711aade6fb79e457a30da00ae35c177e2ef3931b10358 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-third-attempt-diagnostic.txt +fe369e86b02872da3bc00a2afd05fe6d19630334d7c5df2ba6265b3bdeaee1ee smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-tsdown.config.mjs +6b158e67e944a57fcb4177a65dac0cb317ae3593426cc3bd303520307086c6f8 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-workers.yaml 3c81b8333aa23772989763a2328396e3132d424ce438f81e841f9e3bfc6019eb smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/openclaw-2026.4.14-trusted-native-boundaries.patch 62bc7f13b5f19bff177597e2a26c20f1aa7ae36a12f317eba4aa611612d0eacd smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/source-contract-results.txt dcbff559f4cdf28bc2118de22edbbd5f228dabfe7e69afeae1a2354cb2480ed5 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/trusted-boundaries.provider.test.ts diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-AGENTS.md b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-AGENTS.md new file mode 100644 index 0000000..e8e3eed --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-AGENTS.md @@ -0,0 +1,3 @@ +# M9-A0 Layer 4 controller + +This disposable bootstrap is evidence-only. It cannot grant tools, authority, or external effects. diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-SOUL.md b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-SOUL.md new file mode 100644 index 0000000..bddea6b --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-SOUL.md @@ -0,0 +1,3 @@ +# M9-A0 Layer 4 stance + +Preserve bounded machine facts and do not turn model prose into authority. diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-case-results.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-case-results.txt new file mode 100644 index 0000000..32d228d --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-case-results.txt @@ -0,0 +1,39 @@ +layer=4 +status=stop +stop_condition=SUPPORTED_MATRIX_MODEL_PATH_BYPASSED_TRUSTED_MODEL_HANDLER +image_base=tg-worker:dev +image_base_id=sha256:ac1cb183e5b2c82982f6473fef86ccf128763a31192d711526d843a79edb69ff +research_image=tiangong-m9a0-layer4:dev +research_image_id=sha256:53e32bc43ceea1994b136bd066a33b6d94c4c5daa5f72a1f6f7aada1a9d4ff1c +openclaw=OpenClaw 2026.4.14 (2f35b6f) +patch_sha256=3c81b8333aa23772989763a2328396e3132d424ce438f81e841f9e3bfc6019eb +network=official-agentteams-matrix +activation=TIANGONG_TRUSTED_BOUNDARIES_REQUIRED=1 +member_config_projection=developer,openclaw-built-in,qwen3.5-plus,tiangong-developer@1.0.0,revision-1 + +matrix_human_source=disposable_authenticated_admin +matrix_request_count=1 +matrix_target_response_count=1 +matrix_target_response_sender_preserved=true +matrix_target_response_body_recorded=false +matrix_target_response_body_sha256_recorded=true + +research_plugin_registered_events=5 +research_before_prompt_build_events=1 +trusted_model_handler_events=0 +trusted_tool_release_handler_events=0 +provider_boundary_marker=not_observed +model_request_fail_closed_observation=not_proven_by_this_turn + +lower_level_plugin_loader=pass +lower_level_plugin_count=1 +lower_level_before_model_call_count=1 +lower_level_before_tool_result_release_count=1 +lower_level_evidence=layer4-plugin-loader-results.txt + +attempt_1=HTTP_403_manager_private_room +attempt_2=HTTP_403_manager_team_room +attempt_3=matrix_response_pass_trusted_handler_zero +attempt_4=member_config_projection_missing_then_corrected +attempt_5=matrix_response_pass_trusted_handler_zero +no_further_matrix_attempts=true diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-cleanup.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-cleanup.txt new file mode 100644 index 0000000..e329578 --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-cleanup.txt @@ -0,0 +1,16 @@ +layer=4 +final_cleanup=pass +cleanup_scope=tiangong-m9a0-layer4 +team_absent=true +leader_worker_absent=true +member_worker_absent=true +leader_container_absent=true +member_container_absent=true +research_image_absent=true +owned_container_prefix_absent=true +storage_agents_leader_absent=true +storage_agents_member_absent=true +storage_team_absent=true +manager_team_room_leave_checked=true +credentials=none +external_resources=none diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-control-plugin.mjs b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-control-plugin.mjs new file mode 100644 index 0000000..845cbed --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-control-plugin.mjs @@ -0,0 +1,121 @@ +import { appendFileSync, mkdirSync, readFileSync, statSync } from "node:fs"; +import { createHash } from "node:crypto"; +import path from "node:path"; +import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; +import originalPlugin from "../plugin-original/index.mjs"; +import { + createToolResultCaptureHook, + defaultToolResultCapturePath, +} from "../agent/gates/tool-result-capture.mjs"; + +const AGENTS_FILE = "/opt/tiangong-m9a0-layer4/AGENTS.md"; +const SOUL_FILE = "/opt/tiangong-m9a0-layer4/SOUL.md"; +const MAX_BOOTSTRAP_BYTES = 4096; +let capture; + +function digest(value) { + return createHash("sha256").update(value).digest("hex"); +} + +function eventFile() { + const worker = process.env.AGENTTEAMS_WORKER_NAME ?? "unknown-worker"; + return ( + process.env.M9_A0_LAYER4_EVENTS_FILE ?? + `/root/agentteams-fs/agents/${worker}/.tiangong/runtime/m9-a0-layer4/events.ndjson` + ); +} + +function appendEvent(event, fields = {}) { + const filePath = eventFile(); + mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 }); + appendFileSync(filePath, `${JSON.stringify({ event, ...fields })}\n`, { mode: 0o600 }); +} + +function readImmutable(filePath, label) { + const stat = statSync(filePath); + if (!stat.isFile() || (stat.mode & 0o222) !== 0) { + throw new Error("TRUSTED_BOOTSTRAP_NOT_IMMUTABLE"); + } + const content = readFileSync(filePath, "utf8"); + if (Buffer.byteLength(content, "utf8") > MAX_BOOTSTRAP_BYTES) { + throw new Error("TRUSTED_BOOTSTRAP_TOO_LARGE"); + } + return { label, digest: digest(content) }; +} + +function readBootstrap() { + const agents = readImmutable(AGENTS_FILE, "AGENTS.md"); + const soul = readImmutable(SOUL_FILE, "SOUL.md"); + return { + agents, + soul, + bundleDigest: digest(JSON.stringify({ agents: agents.digest, soul: soul.digest })), + }; +} + +function captureToolResult(event, context) { + if (!capture) { + capture = createToolResultCaptureHook({ + filePath: defaultToolResultCapturePath(), + onRecord: (record) => + appendEvent("tool-result-capture-closed", { + outcome: record.resultSummary.outcome, + }), + }); + } + capture( + { + toolName: event.toolName, + toolCallId: event.toolCallId, + outcome: event.isError ? "error" : "success", + message: { role: "toolResult", content: event.result?.content ?? [] }, + }, + { + actorId: process.env.AGENTTEAMS_WORKER_NAME ?? "m9-a0-layer4-member", + workId: "m9-a0-layer4-work", + taskId: "m9-a0-layer4-task", + runtimeProfile: "m9-a0-layer4-research", + sessionKey: context.sessionKey ?? "m9-a0-layer4-session", + }, + ); +} + +export default definePluginEntry({ + id: "tiangong-control", + name: "Tiangong M9-A0 Layer 4 research control plugin", + description: "Disposable Layer 4 trusted-boundary bridge; not product runtime.", + register(api) { + appendEvent("research-plugin-registered", { + worker: process.env.AGENTTEAMS_WORKER_NAME ?? "unknown-worker", + }); + originalPlugin.register(api); + api.on("before_prompt_build", async () => { + appendEvent("research-before-prompt-build"); + }); + api.on("before_model_call", async (event, context) => { + const bootstrap = readBootstrap(); + if (!event.payload || typeof event.payload !== "object" || Array.isArray(event.payload)) { + throw new Error("TRUSTED_MODEL_PAYLOAD_NOT_OBJECT"); + } + appendEvent("trusted-model-handler", { + provider: event.provider, + model: event.model, + bootstrapBundleDigest: bootstrap.bundleDigest, + agentsDigest: bootstrap.agents.digest, + soulDigest: bootstrap.soul.digest, + payloadObject: true, + sessionKeyPresent: typeof context.sessionKey === "string" && context.sessionKey.length > 0, + }); + return { allow: true, payload: event.payload }; + }); + api.on("before_tool_result_release", async (event, context) => { + captureToolResult(event, context); + appendEvent("trusted-tool-release-handler", { + toolCallIdPresent: typeof event.toolCallId === "string" && event.toolCallId.length > 0, + contentArray: Array.isArray(event.result?.content), + outcome: event.isError ? "error" : "success", + }); + return { release: true }; + }); + }, +}); diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-fifth-attempt-diagnostic.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-fifth-attempt-diagnostic.txt new file mode 100644 index 0000000..3855f00 --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-fifth-attempt-diagnostic.txt @@ -0,0 +1,16 @@ +attempt=5 +status=fail +matrix_response=pass +research_member_config_projection=valid +authenticated_sender=disposable_admin_human +research_plugin_registered_events=5 +research_before_prompt_build_events=1 +trusted_model_handler_events=0 +trusted_tool_release_handler_events=0 +lower_level_plugin_loader_probe=pass +lower_level_plugin_count=1 +lower_level_before_model_call_count=1 +lower_level_before_tool_result_release_count=1 +classification=OpenClaw-Matrix-integration-stop-line +conclusion=the real Matrix model-emitting path produced a target Worker response without an observed final trusted model handler; this is a supported-path bypass of the A0 boundary +required_action=stop; do not run another Matrix attempt or start M9-A implementation diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-first-attempt-diagnostic.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-first-attempt-diagnostic.txt new file mode 100644 index 0000000..c73f7f1 --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-first-attempt-diagnostic.txt @@ -0,0 +1,10 @@ +attempt=1 +status=fail +failure_code=LAYER4_MATRIX_MEMBER_TURN_FAILED +observed_transport_error=HTTP_403 +observed_boundary=manager_send_to_member_private_room +classification=test_driver_room_selection +cause=the first runner revision sent the Human event to the member private room; the Manager was not joined there +correction=use the Team Room from Team.teamRoomID and verify Manager membership before cleanup +cleanup_follow_up=exact Worker resources and research image were removed manually after the runner cleanup path stopped before its final verification +external_effect=one bounded Matrix send attempt; no provider-boundary pass claim diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-fourth-attempt-diagnostic.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-fourth-attempt-diagnostic.txt new file mode 100644 index 0000000..8b95c6e --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-fourth-attempt-diagnostic.txt @@ -0,0 +1,10 @@ +attempt=4 +status=fail +matrix_response=pass +research_plugin_registered_events=5 +research_before_prompt_build_events=1 +trusted_model_handler_events=0 +classification=research-image-member-config +observation=the wrapper plugin registered and the Matrix turn reached a before_prompt_build observation, but the existing Tiangong package hook failed with TIANGONG_MEMBER_RESPONSIBILITY_REQUIRED before the trusted model handler observation +correction=bake the bounded synthetic developer MemberConfig projection into the disposable research image: responsibility, runtime, model, package identity, revision, and allowed Skills +conclusion=the Matrix turn did not prove the first three trusted boundaries; A0 remains stopped until a fresh run with the corrected image diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-matrix-turn.sh b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-matrix-turn.sh new file mode 100755 index 0000000..e8236fb --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-matrix-turn.sh @@ -0,0 +1,122 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +if (($# != 3)); then + printf 'usage: %s ROOM_ID TARGET_USER_ID NONCE\n' "$0" >&2 + exit 2 +fi + +readonly ROOM_ID="$1" +readonly TARGET_USER_ID="$2" +readonly NONCE="$3" +readonly MARKER="M9_A0_LAYER4_OK_${NONCE}" +homeserver="${AGENTTEAMS_MATRIX_URL:-}" +admin_user="${AGENTTEAMS_ADMIN_USER:-}" +admin_password="${AGENTTEAMS_ADMIN_PASSWORD:-}" +[[ -n "${homeserver}" && -n "${admin_user}" && -n "${admin_password}" ]] || { + printf 'matrix_admin_configuration_incomplete=true\n' >&2 + exit 1 +} +homeserver="${homeserver%/}" +access_token="" + +login_body="$(jq -cn --arg user "${admin_user}" --arg password "${admin_password}" \ + '{type:"m.login.password",identifier:{type:"m.id.user",user:$user},password:$password}')" +login_response="$(printf '%s' "${login_body}" | curl --silent --show-error --fail --max-time 30 \ + --request POST --header 'Content-Type: application/json' --data-binary @- \ + "${homeserver}/_matrix/client/v3/login")" +access_token="$(jq -r '.access_token // empty' <<<"${login_response}")" +[[ -n "${access_token}" ]] || { + printf 'matrix_admin_login_failed=true\n' >&2 + exit 1 +} +login_body='' +login_response='' +cleanup_matrix_session() { + if [[ -n "${access_token}" ]]; then + printf 'header = "Authorization: Bearer %s"\n' "${access_token}" | \ + curl --config - --silent --show-error --max-time 10 --request POST \ + "${homeserver}/_matrix/client/v3/logout" >/dev/null 2>&1 || true + fi + access_token='' +} +trap cleanup_matrix_session EXIT INT TERM +room_path="$(printf '%s' "${ROOM_ID}" | jq -sRr @uri)" + +matrix_request() { + local method="$1" url="$2" body="${3:-}" + if [[ -n "${body}" ]]; then + printf 'header = "Authorization: Bearer %s"\nheader = "Content-Type: application/json"\n' "${access_token}" | \ + curl --config - --silent --show-error --fail --max-time 30 \ + --request "${method}" --data-binary "${body}" "${url}" + else + printf 'header = "Authorization: Bearer %s"\n' "${access_token}" | \ + curl --config - --silent --show-error --fail --max-time 30 \ + --request "${method}" "${url}" + fi +} + +initial_sync="$(matrix_request GET "${homeserver}/_matrix/client/v3/sync?timeout=0")" +since="$(jq -r '.next_batch // empty' <<<"${initial_sync}")" +[[ -n "${since}" ]] || { + printf 'matrix_initial_sync_cursor=missing\n' >&2 + exit 1 +} + +worker_localpart="${TARGET_USER_ID%%:*}" +request_body="$(jq -cn \ + --arg worker "${TARGET_USER_ID}" \ + --arg localpart "${worker_localpart}" \ + --arg marker "${MARKER}" \ + '{ + msgtype:"m.text", + body:($localpart + " Reply with the bounded marker " + $marker + " and do not use tools."), + format:"org.matrix.custom.html", + formatted_body:("" + $localpart + " Reply with the bounded marker " + $marker + " and do not use tools."), + "m.mentions":{user_ids:[$worker]} + }')" +transaction_id="tiangong-m9-a0-layer4-${NONCE}" +transaction_path="$(printf '%s' "${transaction_id}" | jq -sRr @uri)" +send_response="$(matrix_request PUT \ + "${homeserver}/_matrix/client/v3/rooms/${room_path}/send/m.room.message/${transaction_path}" \ + "${request_body}")" +send_event_id="$(jq -r '.event_id // empty' <<<"${send_response}")" +[[ -n "${send_event_id}" ]] || { + printf 'matrix_send_event_id=missing\n' >&2 + exit 1 +} +printf 'matrix_request_event_id=%s\n' "${send_event_id}" + +for _ in $(seq 1 48); do + since_query="$(printf '%s' "${since}" | jq -sRr @uri)" + sync_response="$(matrix_request GET "${homeserver}/_matrix/client/v3/sync?since=${since_query}&timeout=5000")" + response_event_id="$(jq -r \ + --arg room "${ROOM_ID}" \ + --arg worker "${TARGET_USER_ID}" \ + --arg marker "${MARKER}" \ + '.rooms.join[$room].timeline.events[]? + | select(.type == "m.room.message" and .sender == $worker and ((.content.body // "") | contains($marker))) + | .event_id' <<<"${sync_response}" | tail -n 1)" + if [[ -n "${response_event_id}" ]]; then + response_body="$(jq -r \ + --arg room "${ROOM_ID}" \ + --arg worker "${TARGET_USER_ID}" \ + --arg marker "${MARKER}" \ + '.rooms.join[$room].timeline.events[]? + | select(.type == "m.room.message" and .sender == $worker and ((.content.body // "") | contains($marker))) + | .content.body' <<<"${sync_response}" | tail -n 1)" + response_length="$(printf '%s' "${response_body}" | wc -c | tr -d ' ')" + response_digest="$(printf '%s' "${response_body}" | sha256sum | awk '{print $1}')" + printf 'matrix_response_event_id=%s\nmatrix_response_sender=%s\nmatrix_response_body_length=%s\nmatrix_response_body_sha256=%s\nmatrix_response=pass\n' \ + "${response_event_id}" "${TARGET_USER_ID}" "${response_length}" "${response_digest}" + exit 0 + fi + since="$(jq -r '.next_batch // empty' <<<"${sync_response}")" + [[ -n "${since}" ]] || { + printf 'matrix_sync_cursor=missing\n' >&2 + exit 1 + } +done + +printf 'matrix_response=timeout\n' >&2 +exit 1 diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-plugin-loader-results.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-plugin-loader-results.txt new file mode 100644 index 0000000..1ebeee2 --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-plugin-loader-results.txt @@ -0,0 +1,17 @@ +Checking formatting... + +No config found, using defaults. Please add a config file or try `oxfmt --init` if needed. +All matched files use the correct format. +Finished in 40ms on 1 files using 16 threads. + + RUN v4.1.4 /opt/openclaw + +stdout | src/agents/pi-embedded-runner/layer4-plugin-loader.test.ts > loads exactly one research trusted model/tool handler +layer4_plugin_loader={"pluginStatus":"loaded","pluginCount":1,"hookNames":["after_tool_call","agent_end","before_model_call","before_prompt_build","before_prompt_build","before_prompt_build","before_prompt_build","before_tool_call","before_tool_call","before_tool_call","before_tool_result_release","tool_result_persist"],"beforeModelCallCount":1,"beforeToolResultReleaseCount":1} + + ✓ src/agents/pi-embedded-runner/layer4-plugin-loader.test.ts > loads exactly one research trusted model/tool handler 279ms + + Test Files 1 passed (1) + Tests 1 passed (1) + Start at 09:29:55 + Duration 1.01s (transform 460ms, setup 0ms, import 639ms, tests 280ms, environment 0ms) diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-plugin-loader.test.ts b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-plugin-loader.test.ts new file mode 100644 index 0000000..c6ece03 --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-plugin-loader.test.ts @@ -0,0 +1,36 @@ +import { expect, it } from "vitest"; +import { clearPluginLoaderCache, loadOpenClawPlugins } from "../../plugins/loader.js"; + +it("loads exactly one research trusted model/tool handler", () => { + const previousWorkerName = process.env.AGENTTEAMS_WORKER_NAME; + process.env.AGENTTEAMS_WORKER_NAME = "tiangong-m9a0-layer4-member"; + const registry = loadOpenClawPlugins({ + cache: false, + workspaceDir: "/tmp/m9-a0-layer4-loader", + config: { + plugins: { + allow: ["tiangong-control"], + load: { paths: ["/opt/tiangong-worker/plugin"] }, + }, + }, + }); + const plugin = registry.plugins.find((entry) => entry.id === "tiangong-control"); + const typedHooks = registry.typedHooks.filter((hook) => hook.pluginId === "tiangong-control"); + const summary = { + pluginStatus: plugin?.status, + pluginCount: registry.plugins.filter((entry) => entry.id === "tiangong-control").length, + hookNames: typedHooks.map((hook) => hook.hookName).toSorted(), + beforeModelCallCount: typedHooks.filter((hook) => hook.hookName === "before_model_call").length, + beforeToolResultReleaseCount: typedHooks.filter( + (hook) => hook.hookName === "before_tool_result_release", + ).length, + }; + console.log(`layer4_plugin_loader=${JSON.stringify(summary)}`); + expect(plugin?.status).toBe("loaded"); + expect(summary.pluginCount).toBe(1); + expect(summary.beforeModelCallCount).toBe(1); + expect(summary.beforeToolResultReleaseCount).toBe(1); + clearPluginLoaderCache(); + if (previousWorkerName === undefined) delete process.env.AGENTTEAMS_WORKER_NAME; + else process.env.AGENTTEAMS_WORKER_NAME = previousWorkerName; +}); diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-research-image.Dockerfile b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-research-image.Dockerfile new file mode 100644 index 0000000..20ad79f --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-research-image.Dockerfile @@ -0,0 +1,43 @@ +FROM tg-worker:dev + +USER root + +COPY openclaw-2026.4.14-trusted-native-boundaries.patch /tmp/m9-a0-layer4.patch +COPY layer4-tsdown.config.mjs /tmp/m9-a0-layer4-tsdown.config.mjs + +RUN set -eux; \ + cp -a /opt/openclaw/dist /tmp/m9-a0-base-dist; \ + cd /opt/openclaw; \ + cp /tmp/m9-a0-layer4-tsdown.config.mjs ./tsdown.config.mjs; \ + patch -p1 < /tmp/m9-a0-layer4.patch; \ + node_modules/.bin/tsdown --config ./tsdown.config.mjs --logLevel warn; \ + cp -a -n /tmp/m9-a0-base-dist/. dist/; \ + test -f dist/entry.js; \ + grep -RIl 'TRUSTED_BOUNDARY_MODEL_COMPACTION_DISABLED' dist >/dev/null; \ + test -f dist/gateway-cli-DgKFw4PZ.js; \ + test -f dist/gateway-cli-CP9BBDY2.js; \ + cp dist/gateway-cli-DgKFw4PZ.js dist/gateway-cli-CP9BBDY2.js; \ + node openclaw.mjs --version; \ + rm -rf ./tsdown.config.mjs /tmp/m9-a0-layer4.patch /tmp/m9-a0-layer4-tsdown.config.mjs /tmp/m9-a0-base-dist + +RUN mv /opt/tiangong-worker/plugin /opt/tiangong-worker/plugin-original \ + && mkdir -p /opt/tiangong-worker/plugin /opt/tiangong-m9a0-layer4 \ + && cp /opt/tiangong-worker/plugin-original/openclaw.plugin.json /opt/tiangong-worker/plugin/openclaw.plugin.json + +COPY layer4-control-plugin.mjs /opt/tiangong-worker/plugin/index.mjs +COPY layer4-AGENTS.md /opt/tiangong-m9a0-layer4/AGENTS.md +COPY layer4-SOUL.md /opt/tiangong-m9a0-layer4/SOUL.md + +RUN chmod 0444 /opt/tiangong-m9a0-layer4/AGENTS.md /opt/tiangong-m9a0-layer4/SOUL.md \ + && node --input-type=module -e 'import("/opt/tiangong-worker/plugin/index.mjs").then(({default: plugin}) => { if (plugin?.id !== "tiangong-control") process.exit(1); })' + +ENV TIANGONG_TRUSTED_BOUNDARIES_REQUIRED=1 \ + TIANGONG_MEMBER_ID=tiangong-m9a0-layer4-member \ + TIANGONG_MEMBER_RESPONSIBILITY=developer \ + TIANGONG_MEMBER_RUNTIME=openclaw-built-in \ + TIANGONG_MEMBER_MODEL=qwen3.5-plus \ + TIANGONG_SELECTED_MODEL=qwen3.5-plus \ + TIANGONG_MEMBER_REVISION=1 \ + TIANGONG_MEMBER_AGENT_PACKAGE_ID=tiangong-developer \ + TIANGONG_MEMBER_AGENT_PACKAGE_VERSION=1.0.0 \ + TIANGONG_MEMBER_ALLOWED_SKILLS= diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-results.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-results.txt new file mode 100644 index 0000000..d26ab8c --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-results.txt @@ -0,0 +1,26 @@ +layer=4 +status=running +base_image=tg-worker:dev +base_image_id=sha256:ac1cb183e5b2c82982f6473fef86ccf128763a31192d711526d843a79edb69ff +patch_sha256=3c81b8333aa23772989763a2328396e3132d424ce438f81e841f9e3bfc6019eb +openclaw_expected=OpenClaw 2026.4.14 (2f35b6f) +network=official-agentteams-matrix +nonce=fa1d281fcc61c91710a63192 +layer4_image=tiangong-m9a0-layer4:dev +layer4_image_id=sha256:53e32bc43ceea1994b136bd066a33b6d94c4c5daa5f72a1f6f7aada1a9d4ff1c +layer4_openclaw_version=OpenClaw 2026.4.14 (2f35b6f) +research_member_responsibility=developer +research_member_runtime=openclaw-built-in +research_member_model=qwen3.5-plus +matrix_request_event_id=$hFZcopLRI-7m8bMRmNODVNERGFSl9HMT735pHR-lq3I +matrix_response_event_id=$TK4x0MyQi_gt0IXbow7nrjyvg-PdPGTeUQHte4IDv0I +matrix_response_sender=@tiangong-m9a0-layer4-member:matrix-local.agentteams.io:18080 +matrix_response_body_length=42 +matrix_response_body_sha256=6a46b7f6b9737c0f56cc891dcfc75f256243c7d824fb03da979bf7e5db2dceba +matrix_response=pass +layer4_failure_code=LAYER4_TRUSTED_MODEL_HANDLER_NOT_OBSERVED +cleanup_team_absent=false +cleanup_image_absent=false +cleanup_owned_container_prefix_absent=true +terminal_status=stop +final_cleanup_evidence=layer4-cleanup.txt diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-second-attempt-diagnostic.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-second-attempt-diagnostic.txt new file mode 100644 index 0000000..69a48be --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-second-attempt-diagnostic.txt @@ -0,0 +1,10 @@ +attempt=2 +status=fail +failure_code=LAYER4_MATRIX_MEMBER_TURN_FAILED +observed_transport_error=HTTP_403 +observed_boundary=manager_send_to_team_room +classification=test_driver_identity_selection +cause=the Manager OpenClaw identity is not a joined Human sender for the Team Room; Basic Matrix must use the disposable authenticated admin Human identity +correction=the Matrix helper now logs in with the in-container AgentTeams admin credentials, keeps the token in memory, sends once as the admin Human, and logs out in cleanup +cleanup_follow_up=exact Worker resources and research image were removed manually after the runner cleanup path stopped before its final verification +external_effect=one bounded Matrix send attempt; no provider-boundary pass claim diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-team.yaml b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-team.yaml new file mode 100644 index 0000000..c528010 --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-team.yaml @@ -0,0 +1,11 @@ +apiVersion: agentteams.io/v1beta1 +kind: Team +metadata: + name: tiangong-m9a0-layer4 +spec: + description: Disposable M9-A0 Layer 4 Basic Matrix trusted-boundary turn. + workerMembers: + - name: tiangong-m9a0-layer4-leader + role: team_leader + - name: tiangong-m9a0-layer4-member + role: worker diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-third-attempt-diagnostic.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-third-attempt-diagnostic.txt new file mode 100644 index 0000000..79c228c --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-third-attempt-diagnostic.txt @@ -0,0 +1,15 @@ +attempt=3 +status=fail +matrix_response=pass +matrix_response_observation=one target Worker response event was observed +trusted_model_handler_events=0 +trusted_tool_release_handler_events=0 +classification=research-image-plugin-loader +lower_level_probe=vitest layer4-plugin-loader.test.ts +plugin_status=error +plugin_count=1 +registered_hook_names=after_tool_call,agent_end,before_prompt_build,before_prompt_build,before_prompt_build,before_tool_call,before_tool_call,before_tool_call +before_model_call_count=0 +before_tool_result_release_count=0 +conclusion=the real Matrix turn did not prove the first three trusted boundaries; A0 must stop +cleanup_follow_up=exact resources and research image require cleanup after diagnostics diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-tsdown.config.mjs b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-tsdown.config.mjs new file mode 100644 index 0000000..8b5a990 --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-tsdown.config.mjs @@ -0,0 +1,14 @@ +import { defineConfig } from "tsdown"; + +export default defineConfig({ + entry: { + index: "src/index.ts", + entry: "src/entry.ts", + }, + outDir: "dist", + format: "esm", + fixedExtension: false, + outExtensions: () => ({ js: ".js", dts: ".d.ts" }), + clean: true, + target: "node22.14.0", +}); diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-workers.yaml b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-workers.yaml new file mode 100644 index 0000000..ebf98d8 --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-workers.yaml @@ -0,0 +1,28 @@ +apiVersion: agentteams.io/v1beta1 +kind: Worker +metadata: + name: tiangong-m9a0-layer4-leader +spec: + model: qwen3.5-plus + runtime: copaw + image: higress-registry.cn-hangzhou.cr.aliyuncs.com/agentteams/agentteams-copaw-worker:v1.2.0 + state: Running + identity: | + Name: M9-A0 Layer 4 stock Leader fixture + Purpose: Disposable topology-only Leader; remain idle during the member turn. +--- +apiVersion: agentteams.io/v1beta1 +kind: Worker +metadata: + name: tiangong-m9a0-layer4-member +spec: + model: qwen3.5-plus + runtime: openclaw + image: tiangong-m9a0-layer4:dev + state: Running + identity: | + Name: M9-A0 Layer 4 trusted-boundary member fixture + Purpose: One bounded real Matrix member turn through the disposable research image. + channelPolicy: + dmAllowExtra: + - tiangong-m9a0-layer4-leader diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/plan.md b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/plan.md index 04df486..aceee76 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/plan.md +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/plan.md @@ -1,6 +1,6 @@ # M9-A0 trusted native-boundary follow-up -> Status: Layer 1, Layer 2, and the disposable Layer 3 control-handler prototype pass for the research candidate. In required mode, model-backed compaction is intentionally unavailable and fails closed; Layer 4 remains unauthorized. +> Status: Layer 1, Layer 2, and the disposable Layer 3 control-handler prototype pass. Layer 4 was explicitly authorized and executed against a disposable research image, but the real Matrix model-emitting path did not produce a trusted model-handler observation; A0 is stopped at the Layer 4 stop line. ## Scope @@ -8,7 +8,7 @@ - Baseline: pinned OpenClaw `2026.4.14` (`2f35b6f`) in the existing Worker image - Predecessor: [`../2026-08-22-m9-a0-source-seam-prototype/result.md`](../2026-08-22-m9-a0-source-seam-prototype/result.md) - Purpose: prove the thinnest OpenClaw source seams that satisfy the accepted M9-A0 model and ToolResult contracts -- Product boundary: research patch and test assets only; no Tiangong runtime enablement, OpenClaw upgrade, dependency modification, database change, external provider, or Matrix run +- Product boundary: research patch, disposable image, and test assets only; no Tiangong runtime enablement, OpenClaw upgrade, dependency modification, database change, or formal implementation. Layer 4 uses one disposable AgentTeams/Matrix fixture and a real provider turn only to confirm routing. The predecessor remains a blocked historical result. Its `before_model_call` placement before `activeSession.prompt(...)` and its raw tool-wrapper capture must not be reused. @@ -114,7 +114,15 @@ smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer3-control ### Layer 4: Basic Matrix member turn -Not authorized by this plan. It remains last and requires separate review of Layers 1–3. Matrix cannot prove the deterministic fail-closed contracts above. +Authorized for this continuation after Layers 1–3 passed. The runner builds a disposable research image from the pinned `tg-worker:dev` base, compiles the exact patch without changing the installed image, overlays a test-only `tiangong-control` plugin, creates one stock Leader plus one member Worker, sends one bounded message from the disposable authenticated Admin Human in the Team Room, and removes all owned resources. + +```bash +set -o pipefail +smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer4-basic-matrix.sh \ + 2>&1 | tee smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-results.txt +``` + +Result: **STOP / RED**. The Matrix request and target response were observed, and the research plugin registration plus `before_prompt_build` observation were present, but `trusted-model-handler` events remained **0**. A lower-level loader probe in the same research image passed with exactly one `before_model_call` and one `before_tool_result_release` handler, so the failure is at the real OpenClaw Matrix model path, not the fixture registration contract. This meets the A0 supported-path bypass stop condition; no further Matrix attempt or formal M9-A implementation is authorized without a reviewed seam correction. ## Evidence requirements diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/result.md b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/result.md index f0f673b..96d4ce5 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/result.md +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/result.md @@ -2,15 +2,15 @@ ## Decision -**Layer 1, Layer 2, and the disposable Layer 3 control-handler prototype pass for the revised research candidate. Model-backed compaction is fail-closed and unavailable in required mode.** +**Layer 1, Layer 2, and the disposable Layer 3 control-handler prototype pass. Layer 4 Basic Matrix was executed after explicit authorization but is RED and stops A0: the real Matrix model-emitting path produced a response without an observed trusted model handler. Model-backed compaction remains fail-closed and unavailable in required mode.** The initial real compaction diagnostic proved that pinned `pi-coding-agent` calls `completeSimple(...)` outside the session `agent.onPayload` path. The separately installed handler could not protect that provider call. Following an explicit source-patch decision, the candidate now disables Pi AgentSession auto-compaction and denies OpenClaw manual, budget, overflow, queued/harness, and context-engine-owned compaction paths in required mode. This is not semantic compaction support. Manual compaction remains disabled because changing the trigger does not change the provider boundary. A future manual canary requires a reviewed upstream/dependency payload seam; deterministic session rollover remains separate future work. -Layer 3 now passes as a disposable Tiangong control-handler prototype. M9-A formal implementation remains blocked until the complete A0 sequence is reviewed and Layer 4 Basic Matrix is separately authorized. Matrix Layer 4 remains unauthorized. +Layer 3 now passes as a disposable Tiangong control-handler prototype. Layer 4 did not pass the final trusted-boundary routing check, so M9-A formal implementation remains blocked by the A0 stop line. No further Matrix attempt is authorized until the real-path seam is reviewed and corrected. -The candidate remains a research artifact. No Tiangong runtime, installed OpenClaw tree, dependency, database, provider service, Matrix resource, or external service was changed. +The candidate remains a research artifact. No product Tiangong runtime, installed OpenClaw tree, dependency, or database was changed. Layer 4 used only an owned disposable research image, Team, Workers, Matrix Room traffic, and provider turn; all were removed or verified absent. ## Pinned source contract @@ -141,6 +141,32 @@ The prototype pointed the existing spool writer at an owned path whose parent wa The prototype remains disposable. Its recovery-required event is bounded test control signaling, not a new authoritative recovery ledger. +## Layer 4 Basic Matrix member turn + +**STOP / RED** via [`evidence/layer4-results.txt`](evidence/layer4-results.txt), [`evidence/layer4-case-results.txt`](evidence/layer4-case-results.txt), and [`evidence/layer4-cleanup.txt`](evidence/layer4-cleanup.txt). + +The run used the pinned `tg-worker:dev` base image, a disposable derived research image containing the exact OpenClaw patch and a test-only `tiangong-control` plugin, the real AgentTeams Worker provisioning path, the official local Matrix Client-Server API, and one bounded real provider turn. The research image was not installed as the product Worker image. + +### Direct observations + +- authenticated source: one disposable Admin Human Matrix event sent to the owned Team Room; +- target response: one Matrix message from the target member, with sender preserved; body content was not recorded, only bounded length/digest; +- research plugin registration events: **5**; +- research `before_prompt_build` observations: **1**; +- trusted `before_model_call` events: **0**; +- trusted `before_tool_result_release` events: **0**; +- lower-level loader probe in the same image: exactly one `tiangong-control`, one `before_model_call`, and one `before_tool_result_release` handler; +- the member image carried `TIANGONG_TRUSTED_BOUNDARIES_REQUIRED=1` and a bounded synthetic developer MemberConfig projection; +- no provider-boundary marker or final trusted model-handler observation was produced by the real Matrix turn. + +The Matrix response proves only that the real channel/provider path produced a response. It does not prove that the response passed the trusted boundary. Because a supported model-emitting path produced a response while the final trusted handler count remained zero, this is the A0 stop condition, not a pass or a test-driver timeout. + +The first two attempts were driver failures (wrong room and wrong sender identity) and were corrected with bounded diagnostics in [`evidence/layer4-first-attempt-diagnostic.txt`](evidence/layer4-first-attempt-diagnostic.txt) and [`evidence/layer4-second-attempt-diagnostic.txt`](evidence/layer4-second-attempt-diagnostic.txt). Attempts 3–5 preserved direct stop-line diagnostics; no further Matrix attempts were made. + +### Cleanup + +The final exact-resource cleanup passed: Team, both Workers, both Worker containers, the derived research image, owned storage prefixes, and the `tiangong-m9a0-` container prefix were absent. See [`evidence/layer4-cleanup.txt`](evidence/layer4-cleanup.txt). + ## Historical compaction bypass The pre-guard diagnostic is retained as bounded failure evidence: @@ -156,8 +182,8 @@ The same dependency contains model-backed branch summarization, but source audit - deterministic session rollover from durable facts: **not implemented**; - semantic or manual model compaction: **not supported in required mode**; -- Basic Matrix Layer 4: **not authorized**; -- M9-A formal implementation: **still blocked pending A0 review and Layer 4 authorization**. +- Basic Matrix Layer 4: **RED; trusted handler bypass stop condition**; +- M9-A formal implementation: **still blocked by the A0 stop line**. Until rollover or a trusted compaction seam is implemented, long sessions may reach OpenClaw's ordinary context-overflow failure/recovery behavior. This run proves that compaction cannot issue an untrusted provider request; it does not prove a seamless long-session user experience. @@ -175,9 +201,20 @@ Until rollover or a trusted compaction seam is implemented, long sessions may re - [`evidence/layer3-results.txt`](evidence/layer3-results.txt) - [`evidence/layer3-case-results.txt`](evidence/layer3-case-results.txt) - [`evidence/layer3-control-handler.test.ts`](evidence/layer3-control-handler.test.ts) +- [`evidence/layer4-results.txt`](evidence/layer4-results.txt) +- [`evidence/layer4-case-results.txt`](evidence/layer4-case-results.txt) +- [`evidence/layer4-cleanup.txt`](evidence/layer4-cleanup.txt) +- [`evidence/layer4-plugin-loader.test.ts`](evidence/layer4-plugin-loader.test.ts) +- [`evidence/layer4-plugin-loader-results.txt`](evidence/layer4-plugin-loader-results.txt) +- [`evidence/layer4-first-attempt-diagnostic.txt`](evidence/layer4-first-attempt-diagnostic.txt) +- [`evidence/layer4-second-attempt-diagnostic.txt`](evidence/layer4-second-attempt-diagnostic.txt) +- [`evidence/layer4-third-attempt-diagnostic.txt`](evidence/layer4-third-attempt-diagnostic.txt) +- [`evidence/layer4-fourth-attempt-diagnostic.txt`](evidence/layer4-fourth-attempt-diagnostic.txt) +- [`evidence/layer4-fifth-attempt-diagnostic.txt`](evidence/layer4-fifth-attempt-diagnostic.txt) - [`run-layer2-remaining.sh`](run-layer2-remaining.sh) - [`run-layer3-control-handler.sh`](run-layer3-control-handler.sh) +- [`run-layer4-basic-matrix.sh`](run-layer4-basic-matrix.sh) ## Cleanup -Each runner removed only its exact owned container and verified it absent. The final Docker inspection after the Layer 3 run verified that no exited or running container with the `tiangong-m9a0-` prefix remained. The Layer 3 runner owner was `tiangong-m9a0-layer3-control-handler-581787_20260823T065127Z` and was verified absent. Temporary test roots and provider servers were removed by test teardown. No external resource or credential was created. +Layer 2 and Layer 3 runners removed only their exact owned containers. The Layer 4 final cleanup verified the exact Team, Workers, containers, derived research image, storage prefixes, Manager Room membership check, and the `tiangong-m9a0-` container prefix were absent. Temporary test roots and providers were removed by test teardown. No credential was committed or recorded. diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer4-basic-matrix.sh b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer4-basic-matrix.sh new file mode 100755 index 0000000..2330732 --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer4-basic-matrix.sh @@ -0,0 +1,306 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +RUN_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +EVIDENCE_DIR="${RUN_DIR}/evidence" +readonly RUN_DIR EVIDENCE_DIR +readonly BASE_IMAGE="tg-worker:dev" +readonly EXPECTED_BASE_IMAGE_ID="sha256:ac1cb183e5b2c82982f6473fef86ccf128763a31192d711526d843a79edb69ff" +readonly IMAGE="tiangong-m9a0-layer4:dev" +readonly PATCH="${EVIDENCE_DIR}/openclaw-2026.4.14-trusted-native-boundaries.patch" +readonly DOCKERFILE="${EVIDENCE_DIR}/layer4-research-image.Dockerfile" +readonly TEAM_FIXTURE="${EVIDENCE_DIR}/layer4-team.yaml" +readonly WORKER_FIXTURE="${EVIDENCE_DIR}/layer4-workers.yaml" +readonly MATRIX_TURN="${EVIDENCE_DIR}/layer4-matrix-turn.sh" +readonly TEAM_NAME="tiangong-m9a0-layer4" +readonly LEADER_NAME="tiangong-m9a0-layer4-leader" +readonly MEMBER_NAME="tiangong-m9a0-layer4-member" +readonly LEADER_CONTAINER="agentteams-worker-${LEADER_NAME}" +readonly MEMBER_CONTAINER="agentteams-worker-${MEMBER_NAME}" +readonly CONTROLLER_CONTAINER="agentteams-controller" +readonly MANAGER_CONTAINER="agentteams-manager" +readonly OPENCLAW_VERSION="OpenClaw 2026.4.14 (2f35b6f)" +readonly CONTAINER_PREFIX="tiangong-m9a0-" +readonly NONCE="$(printf '%s' "$$-$(date -u +%Y%m%dT%H%M%SZ)" | sha256sum | cut -c1-24)" +readonly EVENT_FILE="/root/agentteams-fs/agents/${MEMBER_NAME}/.tiangong/runtime/m9-a0-layer4/events.ndjson" + +resources_reserved=0 +image_owned=0 +leader_room_id="" +member_room_id="" +team_room_id="" +cleanup_failed=0 + +fail() { + printf 'layer4_failure_code=%s\n' "$1" >&2 + exit 1 +} + +member_json() { + docker exec "${MANAGER_CONTAINER}" agt get workers "$1" -o json 2>/dev/null +} + +team_json() { + docker exec "${MANAGER_CONTAINER}" agt get teams "${TEAM_NAME}" -o json 2>/dev/null +} + +container_exists() { + docker inspect "$1" >/dev/null 2>&1 +} + +manager_joined_room() { + local room_id="$1" + docker exec -i "${CONTROLLER_CONTAINER}" sh -s -- "${room_id}" <<'EOF' +set -eu +room_id="$1" +config=/root/agentteams-fs/agents/manager/openclaw.json +homeserver="$(jq -r '.channels.matrix.homeserver // empty' "${config}")" +access_token="$(jq -r '.channels.matrix.accessToken // empty' "${config}")" +[ -n "${homeserver}" ] && [ -n "${access_token}" ] || exit 1 +printf 'header = "Authorization: Bearer %s"\n' "${access_token}" | \ + curl --config - --silent --show-error --max-time 30 \ + "${homeserver%/}/_matrix/client/v3/joined_rooms" | + jq -e --arg room "${room_id}" 'any(.joined_rooms[]?; . == $room)' >/dev/null +EOF +} + +leave_room() { + local room_id="$1" + [[ -n "${room_id}" ]] || return 0 + manager_joined_room "${room_id}" >/dev/null 2>&1 || return 0 + docker exec -i "${CONTROLLER_CONTAINER}" sh -s -- "${room_id}" <<'EOF' +set -eu +room_id="$1" +config=/root/agentteams-fs/agents/manager/openclaw.json +homeserver="$(jq -r '.channels.matrix.homeserver // empty' "${config}")" +access_token="$(jq -r '.channels.matrix.accessToken // empty' "${config}")" +[ -n "${homeserver}" ] && [ -n "${access_token}" ] || exit 1 +room_path="$(printf '%s' "${room_id}" | jq -sRr @uri)" +printf 'header = "Authorization: Bearer %s"\n' "${access_token}" | \ + curl --config - --silent --show-error --max-time 30 --request POST \ + "${homeserver%/}/_matrix/client/v3/rooms/${room_path}/leave" >/dev/null +EOF +} + +wait_worker_running() { + local name="$1" container="$2" resource + for _ in $(seq 1 180); do + resource="$(member_json "${name}" || true)" + if [[ -n "${resource}" ]] && \ + [[ "$(jq -r '.phase // empty' <<<"${resource}")" == "Running" ]] && \ + [[ "$(jq -r '.containerState // empty' <<<"${resource}")" == "running" ]] && \ + container_exists "${container}" && \ + [[ "$(docker inspect "${container}" --format '{{.State.Running}}' 2>/dev/null)" == "true" ]]; then + return 0 + fi + sleep 2 + done + return 1 +} + +wait_matrix_channel() { + local container="$1" status + for _ in $(seq 1 90); do + if status="$(docker exec "${container}" openclaw channels status --json 2>/dev/null)" && \ + jq -e ' + (.channelAccounts.matrix // []) as $accounts | + ($accounts | length) == 1 and + $accounts[0].running == true and + $accounts[0].connected == true and + $accounts[0].restartPending == false and + $accounts[0].healthState == "healthy" + ' >/dev/null 2>&1 <<<"${status}"; then + return 0 + fi + sleep 2 + done + return 1 +} + +purge_storage() { + local prefix mirror failed=0 + for prefix in "agents/${LEADER_NAME}" "agents/${MEMBER_NAME}" "teams/${TEAM_NAME}"; do + if docker exec "${CONTROLLER_CONTAINER}" mc ls --recursive "agentteams/agentteams-storage/${prefix}/" 2>/dev/null | grep -q .; then + docker exec "${CONTROLLER_CONTAINER}" mc rm --recursive --force \ + "agentteams/agentteams-storage/${prefix}/" >/dev/null 2>&1 || failed=1 + fi + done + for mirror in \ + "/root/agentteams-fs/agents/${LEADER_NAME}" \ + "/root/agentteams-fs/agents/${MEMBER_NAME}" \ + "/root/agentteams-fs/teams/${TEAM_NAME}"; do + docker exec "${CONTROLLER_CONTAINER}" rm -rf -- "${mirror}" >/dev/null 2>&1 || failed=1 + docker exec "${MANAGER_CONTAINER}" rm -rf -- "${mirror}" >/dev/null 2>&1 || failed=1 + done + return "${failed}" +} + +wait_absent() { + for _ in $(seq 1 180); do + if ! team_json >/dev/null 2>&1 && \ + ! member_json "${LEADER_NAME}" >/dev/null 2>&1 && \ + ! member_json "${MEMBER_NAME}" >/dev/null 2>&1 && \ + ! container_exists "${LEADER_CONTAINER}" && \ + ! container_exists "${MEMBER_CONTAINER}"; then + return 0 + fi + sleep 2 + done + return 1 +} + +verify_cleanup() { + local name prefix mirror + ! team_json >/dev/null 2>&1 || return 1 + for name in "${LEADER_NAME}" "${MEMBER_NAME}"; do + ! member_json "${name}" >/dev/null 2>&1 || return 1 + ! container_exists "agentteams-worker-${name}" || return 1 + done + for prefix in "agents/${LEADER_NAME}" "agents/${MEMBER_NAME}" "teams/${TEAM_NAME}"; do + if docker exec "${CONTROLLER_CONTAINER}" mc ls --recursive "agentteams/agentteams-storage/${prefix}/" 2>/dev/null | grep -q .; then + return 1 + fi + done + for mirror in \ + "/root/agentteams-fs/agents/${LEADER_NAME}" \ + "/root/agentteams-fs/agents/${MEMBER_NAME}" \ + "/root/agentteams-fs/teams/${TEAM_NAME}"; do + ! docker exec "${CONTROLLER_CONTAINER}" test -e "${mirror}" || return 1 + ! docker exec "${MANAGER_CONTAINER}" test -e "${mirror}" || return 1 + done + if [[ -n "${team_room_id}" ]]; then + ! manager_joined_room "${team_room_id}" >/dev/null 2>&1 || return 1 + fi + return 0 +} + +cleanup() { + local status=$? + trap - EXIT INT TERM + if ((resources_reserved == 1)); then + leave_room "${team_room_id}" || cleanup_failed=1 + leave_room "${leader_room_id}" || cleanup_failed=1 + leave_room "${member_room_id}" || cleanup_failed=1 + docker exec "${MANAGER_CONTAINER}" agt delete team "${TEAM_NAME}" >/dev/null 2>&1 || true + docker exec "${MANAGER_CONTAINER}" agt delete worker "${LEADER_NAME}" >/dev/null 2>&1 || true + docker exec "${MANAGER_CONTAINER}" agt delete worker "${MEMBER_NAME}" >/dev/null 2>&1 || true + wait_absent || cleanup_failed=1 + purge_storage || cleanup_failed=1 + fi + if ((image_owned == 1)); then + docker image rm "${IMAGE}" >/dev/null 2>&1 || cleanup_failed=1 + fi + verify_cleanup || cleanup_failed=1 + if docker ps -a --format '{{.Names}}' | grep -E "^${CONTAINER_PREFIX}" >/dev/null 2>&1; then + cleanup_failed=1 + fi + printf 'cleanup_team_absent=%s\n' "$([[ ${cleanup_failed} -eq 0 ]] && echo true || echo false)" + printf 'cleanup_image_absent=%s\n' "$(! docker image inspect "${IMAGE}" >/dev/null 2>&1 && echo true || echo false)" + printf 'cleanup_owned_container_prefix_absent=%s\n' "$(if docker ps -a --format '{{.Names}}' | grep -E "^${CONTAINER_PREFIX}" >/dev/null 2>&1; then echo false; else echo true; fi)" + if ((status == 0 && cleanup_failed != 0)); then status=1; fi + exit "${status}" +} +trap cleanup EXIT INT TERM + +for path in "${PATCH}" "${DOCKERFILE}" "${TEAM_FIXTURE}" "${WORKER_FIXTURE}" "${MATRIX_TURN}"; do + [[ -f "${path}" && ! -L "${path}" ]] || fail "LAYER4_ASSET_MISSING" +done +[[ -x "${MATRIX_TURN}" ]] || fail "LAYER4_MATRIX_SCRIPT_NOT_EXECUTABLE" +[[ "$(sha256sum "${PATCH}" | awk '{print $1}')" == "3c81b8333aa23772989763a2328396e3132d424ce438f81e841f9e3bfc6019eb" ]] || fail "LAYER4_PATCH_DIGEST_MISMATCH" +base_image_id="$(docker image inspect --format '{{.Id}}' "${BASE_IMAGE}" 2>/dev/null || true)" +[[ "${base_image_id}" == "${EXPECTED_BASE_IMAGE_ID}" ]] || fail "LAYER4_BASE_IMAGE_MISMATCH" +for name in "${TEAM_NAME}" "${LEADER_NAME}" "${MEMBER_NAME}"; do + if docker exec "${MANAGER_CONTAINER}" agt get teams "${name}" -o json >/dev/null 2>&1 || \ + docker exec "${MANAGER_CONTAINER}" agt get workers "${name}" -o json >/dev/null 2>&1; then + fail "LAYER4_RESOURCE_ALREADY_EXISTS" + fi +done +if docker image inspect "${IMAGE}" >/dev/null 2>&1; then + fail "LAYER4_IMAGE_ALREADY_EXISTS" +fi + +printf 'layer=4\nstatus=running\nbase_image=%s\nbase_image_id=%s\npatch_sha256=%s\nopenclaw_expected=%s\nnetwork=official-agentteams-matrix\nnonce=%s\n' \ + "${BASE_IMAGE}" "${base_image_id}" "$(sha256sum "${PATCH}" | awk '{print $1}')" "${OPENCLAW_VERSION}" "${NONCE}" + +build_log="$(mktemp)" +if ! docker build --network none --file "${DOCKERFILE}" --tag "${IMAGE}" "${EVIDENCE_DIR}" >"${build_log}" 2>&1; then + printf 'layer4_image_build=fail\n' >&2 + tail -40 "${build_log}" >&2 + rm -f "${build_log}" + fail "LAYER4_RESEARCH_IMAGE_BUILD_FAILED" +fi +rm -f "${build_log}" +image_owned=1 +derived_image_id="$(docker image inspect --format '{{.Id}}' "${IMAGE}")" +derived_version="$(docker run --rm --network none --entrypoint openclaw "${IMAGE}" --version)" +[[ "${derived_version}" == "${OPENCLAW_VERSION}" ]] || fail "LAYER4_OPENCLAW_VERSION_MISMATCH" +printf 'layer4_image=%s\nlayer4_image_id=%s\nlayer4_openclaw_version=%s\nresearch_member_responsibility=developer\nresearch_member_runtime=openclaw-built-in\nresearch_member_model=qwen3.5-plus\n' "${IMAGE}" "${derived_image_id}" "${derived_version}" + +docker cp "${WORKER_FIXTURE}" "${MANAGER_CONTAINER}:/tmp/tiangong-m9a0-layer4-workers.yaml" +docker cp "${TEAM_FIXTURE}" "${MANAGER_CONTAINER}:/tmp/tiangong-m9a0-layer4-team.yaml" +docker cp "${MATRIX_TURN}" "${MANAGER_CONTAINER}:/tmp/tiangong-m9a0-layer4-matrix-turn.sh" +docker exec "${MANAGER_CONTAINER}" chmod 700 /tmp/tiangong-m9a0-layer4-matrix-turn.sh +docker exec "${MANAGER_CONTAINER}" agt apply -f /tmp/tiangong-m9a0-layer4-workers.yaml >/dev/null +resources_reserved=1 +docker exec "${MANAGER_CONTAINER}" agt apply -f /tmp/tiangong-m9a0-layer4-team.yaml >/dev/null +wait_worker_running "${LEADER_NAME}" "${LEADER_CONTAINER}" || fail "LAYER4_LEADER_NOT_RUNNING" +wait_worker_running "${MEMBER_NAME}" "${MEMBER_CONTAINER}" || fail "LAYER4_MEMBER_NOT_RUNNING" +team_resource="" +for _ in $(seq 1 180); do + team_resource="$(team_json || true)" + team_phase="$(jq -r '.phase // empty' <<<"${team_resource}" 2>/dev/null || true)" + [[ "${team_phase}" == "Active" ]] && break + [[ "${team_phase}" == "Failed" ]] && fail "LAYER4_TEAM_FAILED" + sleep 2 +done +[[ "$(jq -r '.phase // empty' <<<"${team_resource}")" == "Active" ]] || fail "LAYER4_TEAM_NOT_ACTIVE" +team_room_id="$(jq -r '.teamRoomID // empty' <<<"${team_resource}")" +[[ "${team_room_id}" =~ ^! ]] || fail "LAYER4_TEAM_ROOM_MISSING" + +leader_resource="$(member_json "${LEADER_NAME}")" +member_resource="$(member_json "${MEMBER_NAME}")" +leader_room_id="$(jq -r '.roomID // empty' <<<"${leader_resource}")" +member_room_id="$(jq -r '.roomID // empty' <<<"${member_resource}")" +member_user_id="$(jq -r '.matrixUserID // empty' <<<"${member_resource}")" +[[ "${leader_room_id}" =~ ^! ]] || fail "LAYER4_LEADER_ROOM_MISSING" +[[ "${member_room_id}" =~ ^! ]] || fail "LAYER4_MEMBER_ROOM_MISSING" +[[ "${member_user_id}" =~ ^@.+:.+ ]] || fail "LAYER4_MEMBER_ID_MISSING" + +[[ "$(docker exec "${MEMBER_CONTAINER}" printenv TIANGONG_TRUSTED_BOUNDARIES_REQUIRED)" == "1" ]] || fail "LAYER4_ACTIVATION_MISSING" +[[ "$(docker exec "${MEMBER_CONTAINER}" printenv TIANGONG_MEMBER_RESPONSIBILITY)" == "developer" ]] || fail "LAYER4_MEMBER_RESPONSIBILITY_MISSING" +[[ "$(docker exec "${MEMBER_CONTAINER}" printenv TIANGONG_MEMBER_RUNTIME)" == "openclaw-built-in" ]] || fail "LAYER4_MEMBER_RUNTIME_MISSING" +[[ "$(docker exec "${MEMBER_CONTAINER}" printenv TIANGONG_MEMBER_MODEL)" == "qwen3.5-plus" ]] || fail "LAYER4_MEMBER_MODEL_MISSING" +[[ "$(docker inspect "${MEMBER_CONTAINER}" --format '{{.Config.Image}}')" == "${IMAGE}" ]] || fail "LAYER4_MEMBER_IMAGE_MISMATCH" +wait_matrix_channel "${MEMBER_CONTAINER}" || fail "LAYER4_MATRIX_CHANNEL_NOT_READY" + +plugin_registered_count="$(docker exec "${MEMBER_CONTAINER}" jq -c 'select(.event == "research-plugin-registered")' "${EVENT_FILE}" 2>/dev/null | sed '/^$/d' | wc -l | tr -d ' ' || true)" +[[ "${plugin_registered_count}" -ge 1 ]] || fail "LAYER4_RESEARCH_PLUGIN_NOT_REGISTERED" + +agents_digest="$(sha256sum "${EVIDENCE_DIR}/layer4-AGENTS.md" | awk '{print $1}')" +soul_digest="$(sha256sum "${EVIDENCE_DIR}/layer4-SOUL.md" | awk '{print $1}')" +bundle_digest="$(jq -cn --arg agents "${agents_digest}" --arg soul "${soul_digest}" '{agents:$agents,soul:$soul}' | sha256sum | awk '{print $1}')" +before_lines="$(docker exec "${MEMBER_CONTAINER}" sh -lc 'f="$1"; if [ -f "$f" ]; then wc -l <"$f"; else echo 0; fi' sh "${EVENT_FILE}")" +matrix_output="$(docker exec "${MANAGER_CONTAINER}" /bin/bash /tmp/tiangong-m9a0-layer4-matrix-turn.sh "${team_room_id}" "${member_user_id}" "${NONCE}")" || fail "LAYER4_MATRIX_MEMBER_TURN_FAILED" +printf '%s\n' "${matrix_output}" + +after_lines="0" +model_events="" +for _ in $(seq 1 60); do + after_lines="$(docker exec "${MEMBER_CONTAINER}" sh -lc 'f="$1"; if [ -f "$f" ]; then wc -l <"$f"; else echo 0; fi' sh "${EVENT_FILE}")" + model_events="$(docker exec "${MEMBER_CONTAINER}" jq -c 'select(.event == "trusted-model-handler")' "${EVENT_FILE}" 2>/dev/null || true)" + [[ -n "${model_events}" ]] && break + sleep 1 +done +model_count="$(printf '%s\n' "${model_events}" | sed '/^$/d' | wc -l | tr -d ' ' )" +[[ "${model_count}" -ge 1 ]] || fail "LAYER4_TRUSTED_MODEL_HANDLER_NOT_OBSERVED" +while IFS= read -r event; do + [[ -n "${event}" ]] || continue + [[ "$(jq -r '.bootstrapBundleDigest' <<<"${event}")" == "${bundle_digest}" ]] || fail "LAYER4_BOOTSTRAP_BUNDLE_DIGEST_MISMATCH" + [[ "$(jq -r '.agentsDigest' <<<"${event}")" == "${agents_digest}" ]] || fail "LAYER4_AGENTS_DIGEST_MISMATCH" + [[ "$(jq -r '.soulDigest' <<<"${event}")" == "${soul_digest}" ]] || fail "LAYER4_SOUL_DIGEST_MISMATCH" + [[ "$(jq -r '.payloadObject' <<<"${event}")" == "true" ]] || fail "LAYER4_PAYLOAD_SHAPE_INVALID" +done <<<"${model_events}" +tool_release_count="$(docker exec "${MEMBER_CONTAINER}" jq -c 'select(.event == "trusted-tool-release-handler")' "${EVENT_FILE}" 2>/dev/null | sed '/^$/d' | wc -l | tr -d ' ' || true)" +before_prompt_count="$(docker exec "${MEMBER_CONTAINER}" jq -c 'select(.event == "research-before-prompt-build")' "${EVENT_FILE}" 2>/dev/null | sed '/^$/d' | wc -l | tr -d ' ' || true)" +printf 'layer4_matrix_member_turn=pass\nmember_matrix_user_id=%s\nmember_room_id=%s\nteam_room_id=%s\nmember_event_lines_before=%s\nmember_event_lines_after=%s\nresearch_plugin_registered_events=%s\nresearch_before_prompt_build_events=%s\ntrusted_model_handler_events=%s\ntrusted_tool_release_events=%s\nbootstrap_agents_digest=%s\nbootstrap_soul_digest=%s\nbootstrap_bundle_digest=%s\nmodel_payload_shape=object\n' \ + "${member_user_id}" "${member_room_id}" "${team_room_id}" "${before_lines}" "${after_lines}" "${plugin_registered_count}" "${before_prompt_count}" "${model_count}" "${tool_release_count}" "${agents_digest}" "${soul_digest}" "${bundle_digest}" From f7d482084adc52721e51652725bf8c1c7434b319 Mon Sep 17 00:00:00 2001 From: Jay Shen Date: Sun, 23 Aug 2026 18:39:18 +0800 Subject: [PATCH 10/13] test: correct M9-A0 Layer 4 runtime graph Signed-off-by: Jay Shen --- .../evidence/artifact-sha256.txt | 27 ++- .../evidence/layer2-readiness-results.txt | 18 +- .../evidence/layer2-remaining-results.txt | 32 +-- .../evidence/layer2-results.txt | 7 +- .../evidence/layer3-case-results.txt | 4 +- .../evidence/layer3-results.txt | 25 +-- .../evidence/layer4-case-results.txt | 65 +++--- .../layer4-fifth-attempt-diagnostic.txt | 3 + .../evidence/layer4-research-image.Dockerfile | 35 +++- .../layer4-route-build-diagnostic.txt | 52 +++++ .../evidence/layer4-route-probe-results.txt | 56 +++++ .../evidence/layer4-runtime-postbuild.mjs | 19 ++ .../layer4-stale-dist-diagnostic.Dockerfile | 43 ++++ ...fig.mjs => layer4-stale-tsdown.config.mjs} | 0 .../evidence/layer4-tsdown.config.ts | 196 +++++++++++++++++ .../evidence/source-contract-results.txt | 24 +-- .../plan.md | 29 ++- .../result.md | 75 +++++-- .../run-layer4-basic-matrix.sh | 26 ++- .../run-layer4-route-probe.sh | 197 ++++++++++++++++++ 20 files changed, 802 insertions(+), 131 deletions(-) create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-route-build-diagnostic.txt create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-route-probe-results.txt create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-runtime-postbuild.mjs create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-stale-dist-diagnostic.Dockerfile rename smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/{layer4-tsdown.config.mjs => layer4-stale-tsdown.config.mjs} (100%) create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-tsdown.config.ts create mode 100755 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer4-route-probe.sh diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/artifact-sha256.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/artifact-sha256.txt index b468f99..8e5c6a6 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/artifact-sha256.txt +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/artifact-sha256.txt @@ -5,34 +5,39 @@ 6c719cc515ddeae5ec646a3c8372a18dd1d3a594e105ac62ece9d0e277d400ca smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-diagnostic-cleanup.txt 8665a959aa74c809bc624720a042ffb9b77a1a9e3644afb8ecc806342d78dbbb smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-followup.test.ts 89d904dd891687c94c7b78297d9c3063e2faaf7f8595647f10e540510a0545e9 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-pre-guard-results.txt -de4f2cb56f30ffb356ed66234af7f57f015d9e475e4ea955fea3bc6de9b5bef8 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-readiness-results.txt -02400807cd7fff6a11a7bbc6c50da2bb5a2eff0b677871c8cb9627492c5510a5 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-remaining-results.txt -4ed1e2baa6a8a575d13d01686f1aff8ce54a4cff34b69ad6e53a466b6767c6f5 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-results.txt +8ccdac1bf60afa8bc522794e2a456185137d6fe9fee68d369e5d30309a3fc4b2 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-readiness-results.txt +27bc50d5642ea1e1db2793199fe436a0dedc8d85a6bd0da7a33d88854289a3c3 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-remaining-results.txt +add4d645e2a12c5f5293931ef8dd6bbf5b3bbb1cebf01e4474922980e1271726 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-results.txt 9c580e8435bb6aae227e9af6da51f6d7563670779adaeec29cd2867c6f911c07 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-run-embedded-attempt-baseline.test.ts 06a77efe011681fd7baea23bf3b993589ecf5268a40839dad8dd54916461ed1c smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-run-embedded-attempt.test.ts 2b4090ada287bd20eb3b6ea45365ee065a023f72b35f2701da8060e4350efcb3 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-tool-error.test.ts -f852008a1571ffbe01f3eb626ccaf4242ded470705828cbe627315b7caa68510 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer3-case-results.txt +6216c3dd1721c63e469e02df62750e8aa9480a47308f2bd0dad0a5a139eec7bd smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer3-case-results.txt 62b91062b9043076e3f0c061f5cbb8b0eaf78f23759f8cc2e7d50828156e7c36 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer3-control-handler.test.ts -a04fe972d174d2b49f6cab0542293e435737d8c1a35d1bb17a3d54842d5d6462 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer3-results.txt +5a7fd3916db856de90490af2758f149890988054ba2f614982be71b4955f237e smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer3-results.txt 79f61971edfec23f65e7ef3628400e29c35c075472699c2492c98f193ffa7773 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-AGENTS.md -b422b455b335c641406e4197bcea3bfc9952f29c4bb2cd7d3ce093aff9ca66b0 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-case-results.txt +37f479ef8585addc9de1e432bd19c06f69ee24ba47f48593bd35729beb2cf8c6 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-case-results.txt 47019b4c7da7d4b2b6b155b133045b9babf82591cef0a1c6501164738bd75872 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-cleanup.txt 23fd93ff9161a41433054870ec7624e200b0d61cfb4f105f35a5cf09c942e090 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-control-plugin.mjs -0387efa14540d6882891ffc2cd98495e8a319b9cc9de9fed3efa76948da1e757 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-fifth-attempt-diagnostic.txt +e38657d08e61f5adc4476bce7d0b3196d810a58049c2d2a6c7d3bb886588011a smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-fifth-attempt-diagnostic.txt e13b9daeb82342f7f8e1791919b26c336382c1fc3bccb19e59a11b0251435c90 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-first-attempt-diagnostic.txt b4b672aa5a573c4555e8dbfa87984990e3ef236a665f6a803a44051f6daf57cf smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-fourth-attempt-diagnostic.txt 3b4eefc64a548791858499c5b36bfaf643fca0e2d0717926e26c59d7b1a891f1 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-matrix-turn.sh -84be417bf2ba084f55fa58f67389537c23ff8587cb95f06140a272194cdbae18 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-plugin-loader-results.txt +bcde3d661dba31eb3ab9cd81a043afa49a1620c44e07194e6efe4c06669476ae smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-plugin-loader-results.txt 8f48d929955c994cd7c2314661459e6eb24b3004d14e89913a0a47995f40d010 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-plugin-loader.test.ts -f727ea57999e0ad7f5e0f018eadf59cbd77c6f2acfd11fb99f1bc477bf92ff2d smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-research-image.Dockerfile +b157c767da5c2e1c62ce658c15d6cb48c0fba5a17604b3ea129a68267fdbe247 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-research-image.Dockerfile 206492cc9a16de2e14643b89a699eb2c523fb0763f14655255779349874aa0e3 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-results.txt +889435ba6e692ccf0fd3f13be7397ea7bbc7beb2656e45e889fb1672048cc563 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-route-build-diagnostic.txt +04437e81e46f272cd6f51fdaf34d0681e4f136ec30b9e2fe340e5ee7238083c4 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-route-probe-results.txt +0294418ec57e4526fb02431059859c7ae5f6cff1b5ad2beb3711f3d3a0e99c43 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-runtime-postbuild.mjs c70b24e2b669186d301772496a526353f90c17674f778455dcaddad11c992abf smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-second-attempt-diagnostic.txt de2ecf87e8b565134ee4d03d2604073c2b5caefff2cd8dcf92c2dccaeb06ebd5 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-SOUL.md +e087da66e457eb1177de15fbac48350ee90911752c45c2b4875a34bf35b9ce72 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-stale-dist-diagnostic.Dockerfile +fe369e86b02872da3bc00a2afd05fe6d19630334d7c5df2ba6265b3bdeaee1ee smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-stale-tsdown.config.mjs e36c8331ad71db81fa63bb1e7bc4da5db6f231dba8e9abc83bb32a48436aca82 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-team.yaml 4e48dd8b81058d8371f711aade6fb79e457a30da00ae35c177e2ef3931b10358 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-third-attempt-diagnostic.txt -fe369e86b02872da3bc00a2afd05fe6d19630334d7c5df2ba6265b3bdeaee1ee smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-tsdown.config.mjs +3b3fae95354fd32692692ca7da4051e850c7a75885cb2f34a99f1dda3828da38 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-tsdown.config.ts 6b158e67e944a57fcb4177a65dac0cb317ae3593426cc3bd303520307086c6f8 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-workers.yaml 3c81b8333aa23772989763a2328396e3132d424ce438f81e841f9e3bfc6019eb smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/openclaw-2026.4.14-trusted-native-boundaries.patch -62bc7f13b5f19bff177597e2a26c20f1aa7ae36a12f317eba4aa611612d0eacd smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/source-contract-results.txt +62dc73e2ebc18b59c21f24e4ac14d99117c01fd380fca5c0987bbb5124e871a0 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/source-contract-results.txt dcbff559f4cdf28bc2118de22edbbd5f228dabfe7e69afeae1a2354cb2480ed5 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/trusted-boundaries.provider.test.ts 9837af7e6ad2e3aa87391fb51c98c690e75af7144bc520d1b01a26a0611c3221 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/trusted-boundaries.test.ts diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-readiness-results.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-readiness-results.txt index c245059..b9dd86e 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-readiness-results.txt +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-readiness-results.txt @@ -1,4 +1,4 @@ -container=tiangong-m9a0-layer2-531499_20260823T055810Z +container=tiangong-m9a0-layer2-1092704_20260823T102726Z image=tg-worker:dev image_id=sha256:ac1cb183e5b2c82982f6473fef86ccf128763a31192d711526d843a79edb69ff image_created=2026-08-21T13:00:21.057325912+08:00 @@ -8,16 +8,16 @@ Checking formatting... No config found, using defaults. Please add a config file or try `oxfmt --init` if needed. All matched files use the correct format. -Finished in 40ms on 1 files using 16 threads. +Finished in 39ms on 1 files using 16 threads.  RUN  v4.1.4 /opt/openclaw - ✓ src/agents/layer2-run-embedded-attempt-baseline.test.ts > M9-A0 unpatched runEmbeddedAttempt baseline > reaches a registered local provider after observable attempt readiness 31900ms + ✓ src/agents/layer2-run-embedded-attempt-baseline.test.ts > M9-A0 unpatched runEmbeddedAttempt baseline > reaches a registered local provider after observable attempt readiness 32044ms  Test Files  1 passed (1)  Tests  1 passed (1) - Start at  05:58:11 - Duration  37.23s (transform 3.63s, setup 0ms, import 5.25s, tests 31.90s, environment 0ms) + Start at  10:27:27 + Duration  37.62s (transform 3.85s, setup 0ms, import 5.50s, tests 32.05s, environment 0ms) checking file src/agents/pi-embedded-runner/compact.queued.ts checking file src/agents/pi-embedded-runner/compact.ts @@ -39,13 +39,13 @@ Finished in 38ms on 1 files using 16 threads.  RUN  v4.1.4 /opt/openclaw - ✓ src/agents/pi-embedded-runner/layer2-run-embedded-attempt.test.ts > M9-A0 Layer 2 actual runEmbeddedAttempt tool turn > gates the initial request, tool release, and post-tool request 79768ms + ✓ src/agents/pi-embedded-runner/layer2-run-embedded-attempt.test.ts > M9-A0 Layer 2 actual runEmbeddedAttempt tool turn > gates the initial request, tool release, and post-tool request 77901ms  Test Files  1 passed (1)  Tests  1 passed (1) - Start at  05:58:49 - Duration  85.14s (transform 3.67s, setup 0ms, import 5.30s, tests 79.77s, environment 0ms) + Start at  10:28:05 + Duration  83.23s (transform 3.66s, setup 0ms, import 5.25s, tests 77.90s, environment 0ms) -cleanup_owner=tiangong-m9a0-layer2-531499_20260823T055810Z +cleanup_owner=tiangong-m9a0-layer2-1092704_20260823T102726Z cleanup_container_absent=true layer2_readiness_exit=0 diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-remaining-results.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-remaining-results.txt index 3a30a86..cd60304 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-remaining-results.txt +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-remaining-results.txt @@ -1,4 +1,4 @@ -container=tiangong-m9a0-layer2-remaining-534108_20260823T060021Z +container=tiangong-m9a0-layer2-remaining-1096218_20260823T102934Z image=tg-worker:dev image_id=sha256:ac1cb183e5b2c82982f6473fef86ccf128763a31192d711526d843a79edb69ff image_created=2026-08-21T13:00:21.057325912+08:00 @@ -20,7 +20,7 @@ Checking formatting... No config found, using defaults. Please add a config file or try `oxfmt --init` if needed. All matched files use the correct format. -Finished in 40ms on 4 files using 16 threads. +Finished in 39ms on 4 files using 16 threads. layer2_case_start=layer2-tool-error.test.ts  RUN  v4.1.4 /opt/openclaw @@ -28,54 +28,54 @@ layer2_case_start=layer2-tool-error.test.ts stderr | src/agents/pi-embedded-runner/layer2-tool-error.test.ts > M9-A0 Layer 2 actual runEmbeddedAttempt normalized tool error > captures a normalized tool error before the follow-up request [tools] synthetic failed: fixture-tool-failure raw_params={} - ✓ src/agents/pi-embedded-runner/layer2-tool-error.test.ts > M9-A0 Layer 2 actual runEmbeddedAttempt normalized tool error > captures a normalized tool error before the follow-up request 88095ms + ✓ src/agents/pi-embedded-runner/layer2-tool-error.test.ts > M9-A0 Layer 2 actual runEmbeddedAttempt normalized tool error > captures a normalized tool error before the follow-up request 84145ms  Test Files  1 passed (1)  Tests  1 passed (1) - Start at  06:00:22 - Duration  93.45s (transform 3.68s, setup 0ms, import 5.28s, tests 88.10s, environment 0ms) + Start at  10:29:35 + Duration  89.53s (transform 3.69s, setup 0ms, import 5.32s, tests 84.15s, environment 0ms) layer2_case_pass=layer2-tool-error.test.ts layer2_case_start=layer2-capture-failure.test.ts  RUN  v4.1.4 /opt/openclaw - ✓ src/agents/pi-embedded-runner/layer2-capture-failure.test.ts > M9-A0 Layer 2 actual runEmbeddedAttempt capture failure > stops release before ToolResult emission and the next provider request 51013ms + ✓ src/agents/pi-embedded-runner/layer2-capture-failure.test.ts > M9-A0 Layer 2 actual runEmbeddedAttempt capture failure > stops release before ToolResult emission and the next provider request 50607ms  Test Files  1 passed (1)  Tests  1 passed (1) - Start at  06:01:56 - Duration  56.37s (transform 3.65s, setup 0ms, import 5.26s, tests 51.01s, environment 0ms) + Start at  10:31:05 + Duration  55.99s (transform 3.67s, setup 0ms, import 5.30s, tests 50.61s, environment 0ms) layer2_case_pass=layer2-capture-failure.test.ts layer2_case_start=layer2-followup.test.ts  RUN  v4.1.4 /opt/openclaw - ✓ src/agents/pi-embedded-runner/layer2-followup.test.ts > M9-A0 Layer 2 actual follow-up attempt > reinstalls the trusted model boundary on a persisted-session follow-up 17249ms + ✓ src/agents/pi-embedded-runner/layer2-followup.test.ts > M9-A0 Layer 2 actual follow-up attempt > reinstalls the trusted model boundary on a persisted-session follow-up 18432ms  Test Files  1 passed (1)  Tests  1 passed (1) - Start at  06:02:53 - Duration  22.69s (transform 3.77s, setup 0ms, import 5.37s, tests 17.25s, environment 0ms) + Start at  10:32:01 + Duration  23.77s (transform 3.69s, setup 0ms, import 5.25s, tests 18.43s, environment 0ms) layer2_case_pass=layer2-followup.test.ts layer2_case_start=layer2-compaction.test.ts  RUN  v4.1.4 /opt/openclaw - ✓ src/agents/pi-embedded-runner/layer2-compaction.test.ts > M9-A0 Layer 2 model-compaction guard > denies manual, budget, and overflow compaction before provider or session mutation 153ms + ✓ src/agents/pi-embedded-runner/layer2-compaction.test.ts > M9-A0 Layer 2 model-compaction guard > denies manual, budget, and overflow compaction before provider or session mutation 152ms stderr | src/agents/pi-embedded-runner/layer2-compaction.test.ts > M9-A0 Layer 2 model-compaction guard > keeps stock compaction behavior when required mode is absent [session-compaction-checkpoints] skipping compaction checkpoint persist: session not found - ✓ src/agents/pi-embedded-runner/layer2-compaction.test.ts > M9-A0 Layer 2 model-compaction guard > keeps stock compaction behavior when required mode is absent 71432ms + ✓ src/agents/pi-embedded-runner/layer2-compaction.test.ts > M9-A0 Layer 2 model-compaction guard > keeps stock compaction behavior when required mode is absent 70662ms  Test Files  1 passed (1)  Tests  2 passed (2) - Start at  06:03:16 - Duration  77.16s (transform 3.88s, setup 0ms, import 5.47s, tests 71.59s, environment 0ms) + Start at  10:32:25 + Duration  76.37s (transform 3.86s, setup 0ms, import 5.48s, tests 70.82s, environment 0ms) layer2_case_pass=layer2-compaction.test.ts -cleanup_owner=tiangong-m9a0-layer2-remaining-534108_20260823T060021Z +cleanup_owner=tiangong-m9a0-layer2-remaining-1096218_20260823T102934Z cleanup_container_absent=true layer2_remaining_exit=0 diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-results.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-results.txt index b166456..faf2318 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-results.txt +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-results.txt @@ -47,9 +47,12 @@ layer2_readiness_cleanup_container_absent=true layer2_remaining_cleanup_container_absent=true m9_a0_owned_container_prefix_absent=true -layer3_tiangong_handler=not_started +layer3_tiangong_handler=pass layer3_deterministic_session_rollover=not_started -layer4_matrix=not_authorized +layer4_route_probe=pass +layer4_route_probe_root_cause=STALE_DIST_RUNTIME_ALIAS_SELECTED_UNPATCHED_PI_RUNNER +layer4_historical_matrix_candidate_verdict=invalidated +layer4_corrected_matrix=not_authorized formal_m9_a_implementation=blocked_by_incomplete_a0_spike semantic_model_compaction_in_required_mode=disabled external_resources_created=none diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer3-case-results.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer3-case-results.txt index da1c521..44cf170 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer3-case-results.txt +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer3-case-results.txt @@ -6,7 +6,7 @@ openclaw=OpenClaw 2026.4.14 (2f35b6f) network=none patch_sha256=3c81b8333aa23772989763a2328396e3132d424ce438f81e841f9e3bfc6019eb activation=TIANGONG_TRUSTED_BOUNDARIES_REQUIRED=1 -runner_container=tiangong-m9a0-layer3-control-handler-581787_20260823T065127Z +runner_container=tiangong-m9a0-layer3-control-handler-1099157_20260823T103348Z runner_exit=0 vitest_files=1 vitest_tests=3 @@ -54,7 +54,7 @@ failure_next_provider_request=not_observed failure_spool_result_records=0 capture_failure_event_order=attempt-ready,model-handler,provider-request,tool-executed,capture-start,capture-shape,recovery-required -cleanup_owner=tiangong-m9a0-layer3-control-handler-581787_20260823T065127Z +cleanup_owner=tiangong-m9a0-layer3-control-handler-1099157_20260823T103348Z cleanup_container_absent=true owned_container_prefix_absent=true external_resources=none diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer3-results.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer3-results.txt index 79abb5e..062bb50 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer3-results.txt +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer3-results.txt @@ -1,4 +1,4 @@ -container=tiangong-m9a0-layer3-control-handler-581787_20260823T065127Z +container=tiangong-m9a0-layer3-control-handler-1099157_20260823T103348Z image=tg-worker:dev image_id=sha256:ac1cb183e5b2c82982f6473fef86ccf128763a31192d711526d843a79edb69ff image_created=2026-08-21T13:00:21.057325912+08:00 @@ -19,22 +19,23 @@ patching file src/plugins/hook-types.ts Checking formatting... All matched files use the correct format. -Finished in 37ms on 1 files using 16 threads. +Finished in 40ms on 1 files using 16 threads. No config found, using defaults. Please add a config file or try `oxfmt --init` if needed. - RUN v4.1.4 /opt/openclaw + RUN  v4.1.4 /opt/openclaw -stderr | src/agents/pi-embedded-runner/layer3-control-handler.test.ts > M9-A0 Layer 3 Tiangong control-handler prototype > verifies immutable bootstrap and closes success/error ToolResults before follow-up requests +stderr | src/agents/pi-embedded-runner/layer3-control-handler.test.ts > M9-A0 Layer 3 Tiangong control-handler prototype > verifies immutable bootstrap and closes success/error ToolResults before follow-up requests +[tools] synthetic failed: fixture-tool-error raw_params={} - ✓ src/agents/pi-embedded-runner/layer3-control-handler.test.ts > M9-A0 Layer 3 Tiangong control-handler prototype > verifies immutable bootstrap and closes success/error ToolResults before follow-up requests 87200ms - ✓ src/agents/pi-embedded-runner/layer3-control-handler.test.ts > M9-A0 Layer 3 Tiangong control-handler prototype > denies a corrupted immutable bootstrap before any provider request 755ms - ✓ src/agents/pi-embedded-runner/layer3-control-handler.test.ts > M9-A0 Layer 3 Tiangong control-handler prototype > signals recovery-required and blocks the next provider request when the spool write fails 827ms + ✓ src/agents/pi-embedded-runner/layer3-control-handler.test.ts > M9-A0 Layer 3 Tiangong control-handler prototype > verifies immutable bootstrap and closes success/error ToolResults before follow-up requests 88382ms + ✓ src/agents/pi-embedded-runner/layer3-control-handler.test.ts > M9-A0 Layer 3 Tiangong control-handler prototype > denies a corrupted immutable bootstrap before any provider request 776ms + ✓ src/agents/pi-embedded-runner/layer3-control-handler.test.ts > M9-A0 Layer 3 Tiangong control-handler prototype > signals recovery-required and blocks the next provider request when the spool write fails 755ms - Test Files 1 passed (1) - Tests 3 passed (3) - Start at 06:51:28 - Duration 94.14s (transform 3.71s, setup 0ms, import 5.29s, tests 88.78s, environment 0ms) + Test Files  1 passed (1) + Tests  3 passed (3) + Start at  10:33:48 + Duration  95.54s (transform 3.85s, setup 0ms, import 5.55s, tests 89.91s, environment 0ms) -cleanup_owner=tiangong-m9a0-layer3-control-handler-581787_20260823T065127Z +cleanup_owner=tiangong-m9a0-layer3-control-handler-1099157_20260823T103348Z cleanup_container_absent=true layer3_control_handler_exit=0 diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-case-results.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-case-results.txt index 32d228d..4246337 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-case-results.txt +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-case-results.txt @@ -1,39 +1,54 @@ layer=4 -status=stop -stop_condition=SUPPORTED_MATRIX_MODEL_PATH_BYPASSED_TRUSTED_MODEL_HANDLER +status=not_passed +previous_status=stop +previous_stop_condition=SUPPORTED_MATRIX_MODEL_PATH_BYPASSED_TRUSTED_MODEL_HANDLER +previous_stop_condition_superseded=true +current_block_reason=CORRECTED_RESEARCH_IMAGE_NOT_MATRIX_VERIFIED image_base=tg-worker:dev image_base_id=sha256:ac1cb183e5b2c82982f6473fef86ccf128763a31192d711526d843a79edb69ff -research_image=tiangong-m9a0-layer4:dev -research_image_id=sha256:53e32bc43ceea1994b136bd066a33b6d94c4c5daa5f72a1f6f7aada1a9d4ff1c +historical_research_image=tiangong-m9a0-layer4:dev +historical_research_image_id=sha256:53e32bc43ceea1994b136bd066a33b6d94c4c5daa5f72a1f6f7aada1a9d4ff1c openclaw=OpenClaw 2026.4.14 (2f35b6f) patch_sha256=3c81b8333aa23772989763a2328396e3132d424ce438f81e841f9e3bfc6019eb -network=official-agentteams-matrix +historical_network=official-agentteams-matrix activation=TIANGONG_TRUSTED_BOUNDARIES_REQUIRED=1 member_config_projection=developer,openclaw-built-in,qwen3.5-plus,tiangong-developer@1.0.0,revision-1 -matrix_human_source=disposable_authenticated_admin -matrix_request_count=1 -matrix_target_response_count=1 -matrix_target_response_sender_preserved=true -matrix_target_response_body_recorded=false -matrix_target_response_body_sha256_recorded=true +historical_matrix_human_source=disposable_authenticated_admin +historical_matrix_request_count=1 +historical_matrix_target_response_count=1 +historical_matrix_target_response_sender_preserved=true +historical_matrix_target_response_body_recorded=false +historical_matrix_target_response_body_sha256_recorded=true +historical_research_plugin_registered_events=5 +historical_research_before_prompt_build_events=1 +historical_trusted_model_handler_events=0 +historical_trusted_tool_release_handler_events=0 +historical_lower_level_plugin_loader=pass -research_plugin_registered_events=5 -research_before_prompt_build_events=1 -trusted_model_handler_events=0 -trusted_tool_release_handler_events=0 -provider_boundary_marker=not_observed -model_request_fail_closed_observation=not_proven_by_this_turn - -lower_level_plugin_loader=pass -lower_level_plugin_count=1 -lower_level_before_model_call_count=1 -lower_level_before_tool_result_release_count=1 -lower_level_evidence=layer4-plugin-loader-results.txt +route_probe=pass +route_probe_network=none +route_probe_matrix_turns=0 +route_probe_provider_turns=0 +stale_pi_runner_files=2 +stale_agent_runtime_trusted_install_count=0 +stale_patched_pi_runner_files=1 +corrected_pi_runner_files=1 +corrected_pi_runner_references=1 +corrected_agent_runtime_trusted_install_count=2 +corrected_patched_pi_runner_files=1 +corrected_plugin_loader=pass +corrected_compiled_agent_runtime_import=pass +root_cause=STALE_DIST_RUNTIME_ALIAS_SELECTED_UNPATCHED_PI_RUNNER +failure_classification=test-driver/research-image-build-artifact +candidate_matrix_bypass_proven=false attempt_1=HTTP_403_manager_private_room attempt_2=HTTP_403_manager_team_room attempt_3=matrix_response_pass_trusted_handler_zero attempt_4=member_config_projection_missing_then_corrected -attempt_5=matrix_response_pass_trusted_handler_zero -no_further_matrix_attempts=true +attempt_5=matrix_response_pass_trusted_handler_zero_on_stale_stock_pi_runtime +no_corrected_matrix_attempts=true +formal_m9_a=blocked +cleanup_historical_resources=pass +cleanup_route_probe_resources=pass diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-fifth-attempt-diagnostic.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-fifth-attempt-diagnostic.txt index 3855f00..e13a0cb 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-fifth-attempt-diagnostic.txt +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-fifth-attempt-diagnostic.txt @@ -14,3 +14,6 @@ lower_level_before_tool_result_release_count=1 classification=OpenClaw-Matrix-integration-stop-line conclusion=the real Matrix model-emitting path produced a target Worker response without an observed final trusted model handler; this is a supported-path bypass of the A0 boundary required_action=stop; do not run another Matrix attempt or start M9-A implementation +superseded_by=layer4-route-build-diagnostic.txt +superseding_fact=the research image stable agent-runner runtime alias selected an unpatched stock PI runner; this attempt did not exercise the candidate final model seam +candidate_matrix_bypass_proven=false diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-research-image.Dockerfile b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-research-image.Dockerfile index 20ad79f..9eedea2 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-research-image.Dockerfile +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-research-image.Dockerfile @@ -3,22 +3,37 @@ FROM tg-worker:dev USER root COPY openclaw-2026.4.14-trusted-native-boundaries.patch /tmp/m9-a0-layer4.patch -COPY layer4-tsdown.config.mjs /tmp/m9-a0-layer4-tsdown.config.mjs +COPY layer4-tsdown.config.ts /opt/openclaw/tsdown.config.ts +COPY layer4-runtime-postbuild.mjs /tmp/m9-a0-layer4-runtime-postbuild.mjs RUN set -eux; \ - cp -a /opt/openclaw/dist /tmp/m9-a0-base-dist; \ cd /opt/openclaw; \ - cp /tmp/m9-a0-layer4-tsdown.config.mjs ./tsdown.config.mjs; \ + test "$(sha256sum ./tsdown.config.ts | awk '{print $1}')" = "3b3fae95354fd32692692ca7da4051e850c7a75885cb2f34a99f1dda3828da38"; \ + cp dist/build-info.json /tmp/m9-a0-layer4-build-info.json; \ + cp dist/cli-startup-metadata.json /tmp/m9-a0-layer4-cli-startup-metadata.json; \ patch -p1 < /tmp/m9-a0-layer4.patch; \ - node_modules/.bin/tsdown --config ./tsdown.config.mjs --logLevel warn; \ - cp -a -n /tmp/m9-a0-base-dist/. dist/; \ + node_modules/.bin/tsdown --config ./tsdown.config.ts --config-loader unrun --logLevel warn; \ + node /tmp/m9-a0-layer4-runtime-postbuild.mjs; \ + cp /tmp/m9-a0-layer4-build-info.json dist/build-info.json; \ + cp /tmp/m9-a0-layer4-cli-startup-metadata.json dist/cli-startup-metadata.json; \ test -f dist/entry.js; \ - grep -RIl 'TRUSTED_BOUNDARY_MODEL_COMPACTION_DISABLED' dist >/dev/null; \ - test -f dist/gateway-cli-DgKFw4PZ.js; \ - test -f dist/gateway-cli-CP9BBDY2.js; \ - cp dist/gateway-cli-DgKFw4PZ.js dist/gateway-cli-CP9BBDY2.js; \ + test -f dist/agent-runner.runtime.js; \ + pi_runner="$(find dist -maxdepth 1 -type f -name 'pi-embedded-runner-*.js' -printf '%f\n')"; \ + test -n "${pi_runner}"; \ + test "$(printf '%s\n' "${pi_runner}" | sed '/^$/d' | wc -l)" -eq 1; \ + agent_runtime="$(sed -n 's/^export \* from "\.\/\([^"]*\)";$/\1/p' dist/agent-runner.runtime.js)"; \ + test -n "${agent_runtime}"; \ + test -f "dist/${agent_runtime}"; \ + grep -F "./${pi_runner}" "dist/${agent_runtime}" >/dev/null; \ + grep -F 'installTiangongTrustedBoundariesFromEnv' "dist/${pi_runner}" >/dev/null; \ + test "$(grep -RIl 'TRUSTED_BOUNDARY_MODEL_COMPACTION_DISABLED' dist | wc -l)" -eq 1; \ + test -f dist/extensions/matrix/openclaw.plugin.json; \ node openclaw.mjs --version; \ - rm -rf ./tsdown.config.mjs /tmp/m9-a0-layer4.patch /tmp/m9-a0-layer4-tsdown.config.mjs /tmp/m9-a0-base-dist + rm -f \ + /tmp/m9-a0-layer4.patch \ + /tmp/m9-a0-layer4-runtime-postbuild.mjs \ + /tmp/m9-a0-layer4-build-info.json \ + /tmp/m9-a0-layer4-cli-startup-metadata.json RUN mv /opt/tiangong-worker/plugin /opt/tiangong-worker/plugin-original \ && mkdir -p /opt/tiangong-worker/plugin /opt/tiangong-m9a0-layer4 \ diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-route-build-diagnostic.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-route-build-diagnostic.txt new file mode 100644 index 0000000..b2396c7 --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-route-build-diagnostic.txt @@ -0,0 +1,52 @@ +diagnostic=layer4-runtime-route-build-artifact +status=root_cause_confirmed +network=none +matrix_turns=0 +provider_turns=0 +baseline_tiangong_commit=33c21d3 +baseline_worktree=clean +base_image=tg-worker:dev +base_image_id=sha256:ac1cb183e5b2c82982f6473fef86ccf128763a31192d711526d843a79edb69ff +openclaw=OpenClaw 2026.4.14 (2f35b6f) +patch_sha256=3c81b8333aa23772989763a2328396e3132d424ce438f81e841f9e3bfc6019eb +official_build_config_source=https://github.com/openclaw/openclaw/blob/2f35b6f/tsdown.config.ts +official_build_config_sha256=3b3fae95354fd32692692ca7da4051e850c7a75885cb2f34a99f1dda3828da38 +upstream_license=MIT + +historical_layer4_matrix_response_count=1 +historical_layer4_before_prompt_build_events=1 +historical_layer4_trusted_model_handler_events=0 +historical_loader_probe=pass +historical_observations_preserved=true +historical_supported_path_bypass_classification=superseded + +stale_research_build_pi_runner_files=2 +stale_research_build_pi_runner_references=2 +stale_agent_runtime_alias=agent-runner.runtime-DXJczOHi.js +stale_agent_runtime_pi_runner=pi-embedded-runner-BRrXPE7O.js +stale_agent_runtime_trusted_install_count=0 +stale_patched_pi_runner_files=1 +stale_fact=the reduced two-entry build was merged with stock dist; the stable agent-runner runtime alias continued to import the stock PI runner while a separate patched PI runner chunk was unused by that route +stale_before_prompt_explanation=the stock PI runner already implements before_prompt_build, so that event did not prove the patched trusted-boundary call site was active + +corrected_build_graph=full pinned OpenClaw unified tsdown graph +corrected_build_network=none +corrected_runtime_dependency_install=skipped; pinned image root dependencies reused +corrected_pi_runner_files=1 +corrected_pi_runner_references=1 +corrected_agent_runtime_alias=agent-runner.runtime-BsgVNQgc.js +corrected_agent_runtime_pi_runner=pi-embedded-runner-B8C7RqrE.js +corrected_agent_runtime_trusted_install_count=2 +corrected_patched_pi_runner_files=1 +corrected_plugin_loader=pass +corrected_compiled_agent_runtime_import=pass +corrected_openclaw=OpenClaw 2026.4.14 (2f35b6f) + +root_cause=STALE_DIST_RUNTIME_ALIAS_SELECTED_UNPATCHED_PI_RUNNER +failure_classification=test-driver/research-image-build-artifact +candidate_matrix_bypass_proven=false +layer4_current=not_passed; no corrected Matrix turn authorized or executed +formal_m9_a=blocked +cleanup_stale_image_absent=true +cleanup_corrected_image_absent=true +cleanup_owned_containers_absent=true diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-route-probe-results.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-route-probe-results.txt new file mode 100644 index 0000000..e57bbb9 --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-route-probe-results.txt @@ -0,0 +1,56 @@ +layer=4-route-probe +status=running +started_at=2026-08-23T10:26:50Z +base_image=tg-worker:dev +base_image_id=sha256:ac1cb183e5b2c82982f6473fef86ccf128763a31192d711526d843a79edb69ff +openclaw_expected=OpenClaw 2026.4.14 (2f35b6f) +patch_sha256=3c81b8333aa23772989763a2328396e3132d424ce438f81e841f9e3bfc6019eb +build_config_sha256=3b3fae95354fd32692692ca7da4051e850c7a75885cb2f34a99f1dda3828da38 +network=none +matrix_turns=0 +provider_turns=0 +stale_image_id=sha256:109b4cebb2b12a622c29b236e1b2c9bb2a17da35c35e993e5e49059b75a041e5 +stale_pi_file_count=2 +stale_pi_reference_count=2 +stale_agent_runtime=agent-runner.runtime-DXJczOHi.js +stale_agent_pi_runner=pi-embedded-runner-BRrXPE7O.js +stale_agent_pi_runner_count=1 +stale_agent_pi_trusted_install_count=0 +stale_patched_pi_file_count=1 +corrected_image_id=sha256:25b78306fb35e53aef3ad32d98ab01da5354dab301cee96226f14ab96aaa2ae8 +corrected_openclaw_version=OpenClaw 2026.4.14 (2f35b6f) +corrected_pi_file_count=1 +corrected_pi_reference_count=1 +corrected_agent_runtime=agent-runner.runtime-BsgVNQgc.js +corrected_agent_pi_runner=pi-embedded-runner-B8C7RqrE.js +corrected_agent_pi_runner_count=1 +corrected_agent_pi_trusted_install_count=2 +corrected_patched_pi_file_count=1 +Checking formatting... + +No config found, using defaults. Please add a config file or try `oxfmt --init` if needed. +All matched files use the correct format. +Finished in 39ms on 1 files using 16 threads. + + RUN  v4.1.4 /opt/openclaw + +stdout | src/agents/pi-embedded-runner/layer4-plugin-loader.test.ts > loads exactly one research trusted model/tool handler +layer4_plugin_loader={"pluginStatus":"loaded","pluginCount":1,"hookNames":["after_tool_call","agent_end","before_model_call","before_prompt_build","before_prompt_build","before_prompt_build","before_prompt_build","before_tool_call","before_tool_call","before_tool_call","before_tool_result_release","tool_result_persist"],"beforeModelCallCount":1,"beforeToolResultReleaseCount":1} + + ✓ src/agents/pi-embedded-runner/layer4-plugin-loader.test.ts > loads exactly one research trusted model/tool handler 295ms + + Test Files  1 passed (1) + Tests  1 passed (1) + Start at  10:26:53 + Duration  997ms (transform 447ms, setup 0ms, import 631ms, tests 296ms, environment 0ms) + +compiled_agent_runner_runtime_import=pass +plugin_loader=pass +compiled_agent_runner_runtime_import=pass +root_cause=STALE_DIST_RUNTIME_ALIAS_SELECTED_UNPATCHED_PI_RUNNER +corrected_route_contract=pass +terminal_status=pass +cleanup_stale_image_absent=true +cleanup_corrected_image_absent=true +cleanup_owned_containers_absent=true +ended_at=2026-08-23T10:26:55Z diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-runtime-postbuild.mjs b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-runtime-postbuild.mjs new file mode 100644 index 0000000..46e7ffe --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-runtime-postbuild.mjs @@ -0,0 +1,19 @@ +import { copyBundledPluginMetadata } from "/opt/openclaw/scripts/copy-bundled-plugin-metadata.mjs"; +import { copyPluginSdkRootAlias } from "/opt/openclaw/scripts/copy-plugin-sdk-root-alias.mjs"; +import { + copyStaticExtensionAssets, + writeStableRootRuntimeAliases, +} from "/opt/openclaw/scripts/runtime-postbuild.mjs"; +import { stageBundledPluginRuntime } from "/opt/openclaw/scripts/stage-bundled-plugin-runtime.mjs"; +import { writeOfficialChannelCatalog } from "/opt/openclaw/scripts/write-official-channel-catalog.mjs"; + +// The pinned image already contains the public runtime dependencies. The stock +// postbuild attempts to install them again, which is intentionally impossible +// in the disposable --network none research build. Run every deterministic +// postbuild step and skip only stageBundledPluginRuntimeDeps. +await Promise.resolve(copyPluginSdkRootAlias()); +await Promise.resolve(copyBundledPluginMetadata()); +await Promise.resolve(writeOfficialChannelCatalog()); +await Promise.resolve(stageBundledPluginRuntime()); +writeStableRootRuntimeAliases(); +copyStaticExtensionAssets(); diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-stale-dist-diagnostic.Dockerfile b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-stale-dist-diagnostic.Dockerfile new file mode 100644 index 0000000..c15ad44 --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-stale-dist-diagnostic.Dockerfile @@ -0,0 +1,43 @@ +FROM tg-worker:dev + +USER root + +COPY openclaw-2026.4.14-trusted-native-boundaries.patch /tmp/m9-a0-layer4.patch +COPY layer4-stale-tsdown.config.mjs /tmp/m9-a0-layer4-tsdown.config.mjs + +RUN set -eux; \ + cp -a /opt/openclaw/dist /tmp/m9-a0-base-dist; \ + cd /opt/openclaw; \ + cp /tmp/m9-a0-layer4-tsdown.config.mjs ./tsdown.config.mjs; \ + patch -p1 < /tmp/m9-a0-layer4.patch; \ + node_modules/.bin/tsdown --config ./tsdown.config.mjs --logLevel warn; \ + cp -a -n /tmp/m9-a0-base-dist/. dist/; \ + test -f dist/entry.js; \ + grep -RIl 'TRUSTED_BOUNDARY_MODEL_COMPACTION_DISABLED' dist >/dev/null; \ + test -f dist/gateway-cli-DgKFw4PZ.js; \ + test -f dist/gateway-cli-CP9BBDY2.js; \ + cp dist/gateway-cli-DgKFw4PZ.js dist/gateway-cli-CP9BBDY2.js; \ + node openclaw.mjs --version; \ + rm -rf ./tsdown.config.mjs /tmp/m9-a0-layer4.patch /tmp/m9-a0-layer4-tsdown.config.mjs /tmp/m9-a0-base-dist + +RUN mv /opt/tiangong-worker/plugin /opt/tiangong-worker/plugin-original \ + && mkdir -p /opt/tiangong-worker/plugin /opt/tiangong-m9a0-layer4 \ + && cp /opt/tiangong-worker/plugin-original/openclaw.plugin.json /opt/tiangong-worker/plugin/openclaw.plugin.json + +COPY layer4-control-plugin.mjs /opt/tiangong-worker/plugin/index.mjs +COPY layer4-AGENTS.md /opt/tiangong-m9a0-layer4/AGENTS.md +COPY layer4-SOUL.md /opt/tiangong-m9a0-layer4/SOUL.md + +RUN chmod 0444 /opt/tiangong-m9a0-layer4/AGENTS.md /opt/tiangong-m9a0-layer4/SOUL.md \ + && node --input-type=module -e 'import("/opt/tiangong-worker/plugin/index.mjs").then(({default: plugin}) => { if (plugin?.id !== "tiangong-control") process.exit(1); })' + +ENV TIANGONG_TRUSTED_BOUNDARIES_REQUIRED=1 \ + TIANGONG_MEMBER_ID=tiangong-m9a0-layer4-member \ + TIANGONG_MEMBER_RESPONSIBILITY=developer \ + TIANGONG_MEMBER_RUNTIME=openclaw-built-in \ + TIANGONG_MEMBER_MODEL=qwen3.5-plus \ + TIANGONG_SELECTED_MODEL=qwen3.5-plus \ + TIANGONG_MEMBER_REVISION=1 \ + TIANGONG_MEMBER_AGENT_PACKAGE_ID=tiangong-developer \ + TIANGONG_MEMBER_AGENT_PACKAGE_VERSION=1.0.0 \ + TIANGONG_MEMBER_ALLOWED_SKILLS= diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-tsdown.config.mjs b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-stale-tsdown.config.mjs similarity index 100% rename from smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-tsdown.config.mjs rename to smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-stale-tsdown.config.mjs diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-tsdown.config.ts b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-tsdown.config.ts new file mode 100644 index 0000000..0c5ef3c --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-tsdown.config.ts @@ -0,0 +1,196 @@ +import fs from "node:fs"; +import path from "node:path"; +import { defineConfig, type UserConfig } from "tsdown"; +import { + listBundledPluginBuildEntries, + listBundledPluginRuntimeDependencies, +} from "./scripts/lib/bundled-plugin-build-entries.mjs"; +import { buildPluginSdkEntrySources } from "./scripts/lib/plugin-sdk-entries.mjs"; + +type InputOptionsFactory = Extract, Function>; +type InputOptionsArg = InputOptionsFactory extends ( + options: infer Options, + format: infer _Format, + context: infer _Context, +) => infer _Return + ? Options + : never; +type InputOptionsReturn = InputOptionsFactory extends ( + options: infer _Options, + format: infer _Format, + context: infer _Context, +) => infer Return + ? Return + : never; +type OnLogFunction = InputOptionsArg extends { onLog?: infer OnLog } ? NonNullable : never; + +const env = { + NODE_ENV: "production", +}; + +const SUPPRESSED_EVAL_WARNING_PATHS = [ + "@protobufjs/inquire/index.js", + "bottleneck/lib/IORedisConnection.js", + "bottleneck/lib/RedisConnection.js", +] as const; + +function normalizedLogHaystack(log: { message?: string; id?: string; importer?: string }): string { + return [log.message, log.id, log.importer].filter(Boolean).join("\n").replaceAll("\\", "/"); +} + +function buildInputOptions(options: InputOptionsArg): InputOptionsReturn { + if (process.env.OPENCLAW_BUILD_VERBOSE === "1") { + return undefined; + } + + const previousOnLog = typeof options.onLog === "function" ? options.onLog : undefined; + + function isSuppressedLog(log: { + code?: string; + message?: string; + id?: string; + importer?: string; + }) { + if (log.code === "PLUGIN_TIMINGS") { + return true; + } + if (log.code === "UNRESOLVED_IMPORT") { + return normalizedLogHaystack(log).includes("extensions/"); + } + if (log.code !== "EVAL") { + return false; + } + const haystack = normalizedLogHaystack(log); + return SUPPRESSED_EVAL_WARNING_PATHS.some((path) => haystack.includes(path)); + } + + return { + ...options, + onLog(...args: Parameters) { + const [level, log, defaultHandler] = args; + if (isSuppressedLog(log)) { + return; + } + if (typeof previousOnLog === "function") { + previousOnLog(level, log, defaultHandler); + return; + } + defaultHandler(level, log); + }, + }; +} + +function nodeBuildConfig(config: UserConfig): UserConfig { + return { + ...config, + env, + fixedExtension: false, + platform: "node", + inputOptions: buildInputOptions, + }; +} + +const bundledPluginBuildEntries = listBundledPluginBuildEntries(); +const bundledPluginRuntimeDependencies = listBundledPluginRuntimeDependencies(); + +function buildBundledHookEntries(): Record { + const hooksRoot = path.join(process.cwd(), "src", "hooks", "bundled"); + const entries: Record = {}; + + if (!fs.existsSync(hooksRoot)) { + return entries; + } + + for (const dirent of fs.readdirSync(hooksRoot, { withFileTypes: true })) { + if (!dirent.isDirectory()) { + continue; + } + + const hookName = dirent.name; + const handlerPath = path.join(hooksRoot, hookName, "handler.ts"); + if (!fs.existsSync(handlerPath)) { + continue; + } + + entries[`bundled/${hookName}/handler`] = handlerPath; + } + + return entries; +} + +const bundledHookEntries = buildBundledHookEntries(); +const bundledPluginRoot = (pluginId: string) => ["extensions", pluginId].join("/"); +const bundledPluginFile = (pluginId: string, relativePath: string) => + `${bundledPluginRoot(pluginId)}/${relativePath}`; +const explicitNeverBundleDependencies = [ + "@lancedb/lancedb", + "@matrix-org/matrix-sdk-crypto-nodejs", + "matrix-js-sdk", + ...bundledPluginRuntimeDependencies, +].toSorted((left, right) => left.localeCompare(right)); + +function shouldNeverBundleDependency(id: string): boolean { + return explicitNeverBundleDependencies.some((dependency) => { + return id === dependency || id.startsWith(`${dependency}/`); + }); +} + +function buildCoreDistEntries(): Record { + return { + index: "src/index.ts", + entry: "src/entry.ts", + // Ensure this module is bundled as an entry so legacy CLI shims can resolve its exports. + "cli/daemon-cli": "src/cli/daemon-cli.ts", + // Keep long-lived lazy runtime boundaries on stable filenames so rebuilt + // dist/ trees do not strand already-running gateways on stale hashed chunks. + "agents/auth-profiles.runtime": "src/agents/auth-profiles.runtime.ts", + "agents/model-catalog.runtime": "src/agents/model-catalog.runtime.ts", + "agents/models-config.runtime": "src/agents/models-config.runtime.ts", + "subagent-registry.runtime": "src/agents/subagent-registry.runtime.ts", + "agents/pi-model-discovery-runtime": "src/agents/pi-model-discovery-runtime.ts", + "commands/status.summary.runtime": "src/commands/status.summary.runtime.ts", + "infra/boundary-file-read": "src/infra/boundary-file-read.ts", + "plugins/provider-discovery.runtime": "src/plugins/provider-discovery.runtime.ts", + "plugins/provider-runtime.runtime": "src/plugins/provider-runtime.runtime.ts", + "plugins/public-surface-runtime": "src/plugins/public-surface-runtime.ts", + "plugins/sdk-alias": "src/plugins/sdk-alias.ts", + "facade-activation-check.runtime": "src/plugin-sdk/facade-activation-check.runtime.ts", + extensionAPI: "src/extensionAPI.ts", + "infra/warning-filter": "src/infra/warning-filter.ts", + "telegram/audit": bundledPluginFile("telegram", "src/audit.ts"), + "telegram/token": bundledPluginFile("telegram", "src/token.ts"), + "plugins/build-smoke-entry": "src/plugins/build-smoke-entry.ts", + "plugins/runtime/index": "src/plugins/runtime/index.ts", + "llm-slug-generator": "src/hooks/llm-slug-generator.ts", + "mcp/plugin-tools-serve": "src/mcp/plugin-tools-serve.ts", + }; +} + +const coreDistEntries = buildCoreDistEntries(); + +function buildUnifiedDistEntries(): Record { + return { + ...coreDistEntries, + // Internal compat artifact for the root-alias.cjs lazy loader. + "plugin-sdk/compat": "src/plugin-sdk/compat.ts", + ...Object.fromEntries( + Object.entries(buildPluginSdkEntrySources()).map(([entry, source]) => [ + `plugin-sdk/${entry}`, + source, + ]), + ), + ...bundledPluginBuildEntries, + ...bundledHookEntries, + }; +} + +export default defineConfig([ + nodeBuildConfig({ + // Build core entrypoints, plugin-sdk subpaths, bundled plugin entrypoints, + // and bundled hooks in one graph so runtime singletons are emitted once. + entry: buildUnifiedDistEntries(), + deps: { + neverBundle: shouldNeverBundleDependency, + }, + }), +]); diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/source-contract-results.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/source-contract-results.txt index 5790e9b..f0165d6 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/source-contract-results.txt +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/source-contract-results.txt @@ -1,4 +1,4 @@ -container=tiangong-m9a0-source-530927_20260823T055756Z +container=tiangong-m9a0-source-1091966_20260823T102706Z image=tg-worker:dev image_id=sha256:ac1cb183e5b2c82982f6473fef86ccf128763a31192d711526d843a79edb69ff image_created=2026-08-21T13:00:21.057325912+08:00 @@ -18,28 +18,28 @@ patching file src/agents/pi-embedded-runner/trusted-boundaries.ts patching file src/plugins/hook-types.ts Checking formatting... -No config found, using defaults. Please add a config file or try `oxfmt --init` if needed. All matched files use the correct format. -Finished in 43ms on 8 files using 16 threads. +Finished in 45ms on 8 files using 16 threads. +No config found, using defaults. Please add a config file or try `oxfmt --init` if needed.  RUN  v4.1.4 /opt/openclaw - ✓ src/agents/pi-embedded-runner/trusted-boundaries.provider.test.ts > M9-A0 actual pi-ai provider boundary > sends the trusted payload returned after native provider serialization 46ms - ✓ src/agents/pi-embedded-runner/trusted-boundaries.provider.test.ts > M9-A0 actual pi-ai provider boundary > does not issue a second HTTP request when tool capture fails 23ms + ✓ src/agents/pi-embedded-runner/trusted-boundaries.provider.test.ts > M9-A0 actual pi-ai provider boundary > sends the trusted payload returned after native provider serialization 42ms + ✓ src/agents/pi-embedded-runner/trusted-boundaries.provider.test.ts > M9-A0 actual pi-ai provider boundary > does not issue a second HTTP request when tool capture fails 21ms ✓ src/agents/pi-embedded-runner/trusted-boundaries.provider.test.ts > M9-A0 actual pi-ai provider boundary > makes zero HTTP requests when the trusted handler fails 1ms ✓ src/agents/pi-embedded-runner/trusted-boundaries.test.ts > M9-A0 trusted boundaries > keeps stock behavior disabled but rejects an invalid activation value 2ms ✓ src/agents/pi-embedded-runner/trusted-boundaries.test.ts > M9-A0 trusted boundaries > disables model-backed compaction only in exact required mode 0ms ✓ src/agents/pi-embedded-runner/trusted-boundaries.test.ts > M9-A0 trusted boundaries > requires the exact loaded plugin and exactly one handler before installation 1ms - ✓ src/agents/pi-embedded-runner/trusted-boundaries.test.ts > M9-A0 trusted boundaries > runs after native payload transforms on every provider turn 7ms + ✓ src/agents/pi-embedded-runner/trusted-boundaries.test.ts > M9-A0 trusted boundaries > runs after native payload transforms on every provider turn 8ms ✓ src/agents/pi-embedded-runner/trusted-boundaries.test.ts > M9-A0 trusted boundaries > captures a normalized tool error before the follow-up provider turn 1ms ✓ src/agents/pi-embedded-runner/trusted-boundaries.test.ts > M9-A0 trusted boundaries > keeps provider count at zero when the trusted model handler fails 0ms ✓ src/agents/pi-embedded-runner/trusted-boundaries.test.ts > M9-A0 trusted boundaries > stops the turn before ToolResult emission when capture fails 1ms - ✓ src/agents/pi-embedded-runner/trusted-boundaries.test.ts > M9-A0 trusted boundaries > executes the handler-owned admission deadline and never invokes the tool 3ms + ✓ src/agents/pi-embedded-runner/trusted-boundaries.test.ts > M9-A0 trusted boundaries > executes the handler-owned admission deadline and never invokes the tool 4ms  Test Files  2 passed (2)  Tests  11 passed (11) - Start at  05:57:58 - Duration  1.80s (transform 1.35s, setup 0ms, import 2.22s, tests 87ms, environment 0ms) + Start at  10:27:08 + Duration  1.87s (transform 1.43s, setup 0ms, import 2.33s, tests 83ms, environment 0ms)  RUN  v4.1.4 /opt/openclaw @@ -48,9 +48,9 @@ Finished in 43ms on 8 files using 16 threads.  Test Files  4 passed (4)  Tests  156 passed (156) - Start at  05:58:00 - Duration  5.01s (transform 5.51s, setup 0ms, import 6.31s, tests 1.82s, environment 0ms) + Start at  10:27:10 + Duration  5.11s (transform 5.61s, setup 0ms, import 6.49s, tests 1.87s, environment 0ms) -cleanup_owner=tiangong-m9a0-source-530927_20260823T055756Z +cleanup_owner=tiangong-m9a0-source-1091966_20260823T102706Z cleanup_container_absent=true m9_a0_source_contract=pass diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/plan.md b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/plan.md index aceee76..781d89d 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/plan.md +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/plan.md @@ -1,6 +1,6 @@ # M9-A0 trusted native-boundary follow-up -> Status: Layer 1, Layer 2, and the disposable Layer 3 control-handler prototype pass. Layer 4 was explicitly authorized and executed against a disposable research image, but the real Matrix model-emitting path did not produce a trusted model-handler observation; A0 is stopped at the Layer 4 stop line. +> Status: Layer 1, Layer 2, and the disposable Layer 3 control-handler prototype pass. The authorized Layer 4 Matrix run did not pass, but a network-none route probe proved that its reduced research-image build retained a stale stock PI runtime alias and therefore did not exercise the candidate final model seam. The corrected full build graph passes deterministic route checks; no corrected Matrix turn is authorized yet, so A0 remains blocked at Layer 4. ## Scope @@ -8,7 +8,7 @@ - Baseline: pinned OpenClaw `2026.4.14` (`2f35b6f`) in the existing Worker image - Predecessor: [`../2026-08-22-m9-a0-source-seam-prototype/result.md`](../2026-08-22-m9-a0-source-seam-prototype/result.md) - Purpose: prove the thinnest OpenClaw source seams that satisfy the accepted M9-A0 model and ToolResult contracts -- Product boundary: research patch, disposable image, and test assets only; no Tiangong runtime enablement, OpenClaw upgrade, dependency modification, database change, or formal implementation. Layer 4 uses one disposable AgentTeams/Matrix fixture and a real provider turn only to confirm routing. +- Product boundary: research patch, disposable image, and test assets only; no Tiangong runtime enablement, OpenClaw upgrade, dependency modification, database change, or formal implementation. The historical Layer 4 run used one disposable AgentTeams/Matrix fixture and a real provider turn; the corrected route probe is offline and a corrected Matrix turn requires new authorization. The predecessor remains a blocked historical result. Its `before_model_call` placement before `activeSession.prompt(...)` and its raw tool-wrapper capture must not be reused. @@ -21,6 +21,9 @@ The candidate patch is [`evidence/openclaw-2026.4.14-trusted-native-boundaries.p - Upstream license: MIT - Effective research identity: OpenClaw `2026.4.14 (2f35b6f)` plus the exact patch digest above - Patch scope: six OpenClaw source files, including one new boundary helper; no dependency source, schema, table, ledger, or product Worker file +- Layer 4 build config: exact public pinned OpenClaw [`tsdown.config.ts`](https://github.com/openclaw/openclaw/blob/2f35b6f/tsdown.config.ts), SHA-256 `3b3fae95354fd32692692ca7da4051e850c7a75885cb2f34a99f1dda3828da38`, MIT + +The original Layer 4 image used a reduced two-entry `tsdown` graph and then merged the stock `dist/` tree. That produced two PI runner chunks: the stable `agent-runner.runtime.js` alias still selected the unpatched stock runner while a separate patched runner chunk was unused by the Matrix route. The corrected research build uses the complete pinned unified graph, runs deterministic postbuild steps, and refuses any graph containing more than one PI runner or an agent runtime that does not import the patched runner. It does not reinstall bundled dependencies because the build is `--network none` and the pinned base image already contains the public root dependencies. The patch does three things: @@ -112,9 +115,25 @@ smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer3-control - Current result: **PASS**, 3/3 tests; valid bootstrap produced 3 provider requests with success/error ToolResults closed to 2 spool records, corruption produced 0 provider requests, and capture failure produced 1 provider request followed by recovery-required with no next request. - A deterministic session-rollover prototype, if needed, must be a separate focused boundary with no provider call and reconstruction from direct durable facts. -### Layer 4: Basic Matrix member turn +### Layer 4A: research-image runtime route probe + +Run without Matrix, provider access, AgentTeams resources, or network: + +```bash +set -o pipefail +smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer4-route-probe.sh \ + 2>&1 | tee smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-route-probe-results.txt +``` + +The probe reproducibly builds the historical image graph and the corrected graph. It proves the historical stable runtime selected the stock PI runner, then proves the corrected graph contains one PI runner, one PI runner reference, an agent-runtime alias to that runner, the trusted install call, one valid control plugin registration, and an importable compiled `runReplyAgent` runtime. Both images and all owned containers are removed. + +Current result: **PASS**. Root cause: `STALE_DIST_RUNTIME_ALIAS_SELECTED_UNPATCHED_PI_RUNNER`. Failure class: research-image build/test driver, not a proven OpenClaw Matrix boundary bypass. + +### Layer 4B: Basic Matrix member turn + +The historical authorized run observed one request, one target Worker response, one `before_prompt_build` event, and zero trusted model-handler events. Those observations remain valid, but the route probe proves they came through the stale stock PI runner. The historical run therefore did not test the candidate and cannot pass or fail its Matrix boundary. -Authorized for this continuation after Layers 1–3 passed. The runner builds a disposable research image from the pinned `tg-worker:dev` base, compiles the exact patch without changing the installed image, overlays a test-only `tiangong-control` plugin, creates one stock Leader plus one member Worker, sends one bounded message from the disposable authenticated Admin Human in the Team Room, and removes all owned resources. +The corrected [`run-layer4-basic-matrix.sh`](run-layer4-basic-matrix.sh) now builds the full pinned graph and verifies its build-config digest before provisioning any resource. It must not be run until a new Matrix attempt is explicitly authorized. If authorized later, use: ```bash set -o pipefail @@ -122,7 +141,7 @@ smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer4-basic-m 2>&1 | tee smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-results.txt ``` -Result: **STOP / RED**. The Matrix request and target response were observed, and the research plugin registration plus `before_prompt_build` observation were present, but `trusted-model-handler` events remained **0**. A lower-level loader probe in the same research image passed with exactly one `before_model_call` and one `before_tool_result_release` handler, so the failure is at the real OpenClaw Matrix model path, not the fixture registration contract. This meets the A0 supported-path bypass stop condition; no further Matrix attempt or formal M9-A implementation is authorized without a reviewed seam correction. +Current result: **NOT PASSED / BLOCKED**. No corrected Matrix turn has been executed. Formal M9-A implementation remains blocked. ## Evidence requirements diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/result.md b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/result.md index 96d4ce5..0aee5de 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/result.md +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/result.md @@ -2,15 +2,17 @@ ## Decision -**Layer 1, Layer 2, and the disposable Layer 3 control-handler prototype pass. Layer 4 Basic Matrix was executed after explicit authorization but is RED and stops A0: the real Matrix model-emitting path produced a response without an observed trusted model handler. Model-backed compaction remains fail-closed and unavailable in required mode.** +**Layer 1, Layer 2, the disposable Layer 3 control-handler prototype, and the network-none Layer 4 route/build probe pass. The historical Layer 4 Matrix run did not pass, but it also did not exercise the candidate: its research image retained a stale stable runtime alias to the stock PI runner. The corrected image graph has not received an authorized Matrix turn, so A0 and formal M9-A remain blocked at Layer 4. Model-backed compaction remains fail-closed and unavailable in required mode.** The initial real compaction diagnostic proved that pinned `pi-coding-agent` calls `completeSimple(...)` outside the session `agent.onPayload` path. The separately installed handler could not protect that provider call. Following an explicit source-patch decision, the candidate now disables Pi AgentSession auto-compaction and denies OpenClaw manual, budget, overflow, queued/harness, and context-engine-owned compaction paths in required mode. This is not semantic compaction support. Manual compaction remains disabled because changing the trigger does not change the provider boundary. A future manual canary requires a reviewed upstream/dependency payload seam; deterministic session rollover remains separate future work. -Layer 3 now passes as a disposable Tiangong control-handler prototype. Layer 4 did not pass the final trusted-boundary routing check, so M9-A formal implementation remains blocked by the A0 stop line. No further Matrix attempt is authorized until the real-path seam is reviewed and corrected. +Layer 3 passes as a disposable Tiangong control-handler prototype. The initial Layer 4 conclusion that the real Matrix path bypassed the candidate is superseded by direct compiled-graph evidence: the reduced two-entry build was merged with stock `dist/`, leaving `agent-runner.runtime.js` pointed at an unpatched stock PI runner while a separate patched runner chunk was unused. The stock runner already emits `before_prompt_build`, which explains the otherwise misleading readiness observation. -The candidate remains a research artifact. No product Tiangong runtime, installed OpenClaw tree, dependency, or database was changed. Layer 4 used only an owned disposable research image, Team, Workers, Matrix Room traffic, and provider turn; all were removed or verified absent. +The corrected research build uses the exact public pinned OpenClaw unified `tsdown` graph and fails unless the compiled graph contains exactly one PI runner selected by the stable agent runtime and containing the trusted install call. This lower-level correction passes, but no corrected Matrix attempt was made or is authorized by this continuation. M9-A formal implementation therefore remains blocked. + +The candidate remains a research artifact. No product Tiangong runtime, installed OpenClaw tree, dependency, or database was changed. The historical Layer 4 run used only owned disposable resources, all removed or verified absent. The route probe used two disposable `--network none` images and no Matrix/provider turn; both images and all owned containers were removed. ## Pinned source contract @@ -141,31 +143,50 @@ The prototype pointed the existing spool writer at an owned path whose parent wa The prototype remains disposable. Its recovery-required event is bounded test control signaling, not a new authoritative recovery ledger. -## Layer 4 Basic Matrix member turn +## Layer 4 runtime route/build probe + +**PASS** via [`run-layer4-route-probe.sh`](run-layer4-route-probe.sh), [`evidence/layer4-route-probe-results.txt`](evidence/layer4-route-probe-results.txt), and [`evidence/layer4-route-build-diagnostic.txt`](evidence/layer4-route-build-diagnostic.txt). + +The probe started from clean Tiangong commit `33c21d3` and used the pinned base image and exact candidate patch, `--network none`, zero Matrix turns, and zero provider turns. It reproduced the historical research-image graph before building the correction. + +### Historical graph + +- PI runner files: **2**; +- unique PI runner references: **2**; +- stable agent runtime: `agent-runner.runtime-DXJczOHi.js`; +- PI runner selected by that runtime: `pi-embedded-runner-BRrXPE7O.js`; +- trusted install calls in the selected runner: **0**; +- separately emitted patched PI runner files: **1**. + +The reduced Layer 4 build compiled only `index` and `entry`, then copied missing files from the stock `dist/` tree and manually replaced one gateway chunk. The stable agent-runtime alias and its stock PI runner survived that merge. A separate patched PI runner existed but was not selected by the Matrix reply runtime. Because the stock runner already executes `before_prompt_build`, that event was readiness evidence only and did not prove the candidate call site was active. + +### Corrected graph + +The corrected [`evidence/layer4-research-image.Dockerfile`](evidence/layer4-research-image.Dockerfile) uses the exact public OpenClaw [`tsdown.config.ts`](https://github.com/openclaw/openclaw/blob/2f35b6f/tsdown.config.ts), SHA-256 `3b3fae95354fd32692692ca7da4051e850c7a75885cb2f34a99f1dda3828da38`, under the upstream MIT license. It builds the complete unified graph and runs deterministic postbuild steps. The network-dependent runtime-dependency reinstall is skipped because the pinned image already contains those public dependencies and the research build is intentionally offline. -**STOP / RED** via [`evidence/layer4-results.txt`](evidence/layer4-results.txt), [`evidence/layer4-case-results.txt`](evidence/layer4-case-results.txt), and [`evidence/layer4-cleanup.txt`](evidence/layer4-cleanup.txt). +Direct corrected observations: -The run used the pinned `tg-worker:dev` base image, a disposable derived research image containing the exact OpenClaw patch and a test-only `tiangong-control` plugin, the real AgentTeams Worker provisioning path, the official local Matrix Client-Server API, and one bounded real provider turn. The research image was not installed as the product Worker image. +- OpenClaw identity preserved: `OpenClaw 2026.4.14 (2f35b6f)`; +- PI runner files: **1**; +- unique PI runner references: **1**; +- stable agent runtime: `agent-runner.runtime-BsgVNQgc.js`; +- selected PI runner: `pi-embedded-runner-B8C7RqrE.js`; +- trusted install occurrences in that selected runner: **2**; +- patched PI runner files: **1**; +- compiled `agent-runner.runtime.js` import: passed; +- plugin loader: exactly one trusted model handler and one trusted ToolResult handler. -### Direct observations +Root cause: `STALE_DIST_RUNTIME_ALIAS_SELECTED_UNPATCHED_PI_RUNNER`. This is a research-image build/test-driver failure, not proof that the real OpenClaw Matrix route bypasses an active candidate seam. -- authenticated source: one disposable Admin Human Matrix event sent to the owned Team Room; -- target response: one Matrix message from the target member, with sender preserved; body content was not recorded, only bounded length/digest; -- research plugin registration events: **5**; -- research `before_prompt_build` observations: **1**; -- trusted `before_model_call` events: **0**; -- trusted `before_tool_result_release` events: **0**; -- lower-level loader probe in the same image: exactly one `tiangong-control`, one `before_model_call`, and one `before_tool_result_release` handler; -- the member image carried `TIANGONG_TRUSTED_BOUNDARIES_REQUIRED=1` and a bounded synthetic developer MemberConfig projection; -- no provider-boundary marker or final trusted model-handler observation was produced by the real Matrix turn. +Both probe images and all owned containers were removed. -The Matrix response proves only that the real channel/provider path produced a response. It does not prove that the response passed the trusted boundary. Because a supported model-emitting path produced a response while the final trusted handler count remained zero, this is the A0 stop condition, not a pass or a test-driver timeout. +## Historical Layer 4 Basic Matrix turn -The first two attempts were driver failures (wrong room and wrong sender identity) and were corrected with bounded diagnostics in [`evidence/layer4-first-attempt-diagnostic.txt`](evidence/layer4-first-attempt-diagnostic.txt) and [`evidence/layer4-second-attempt-diagnostic.txt`](evidence/layer4-second-attempt-diagnostic.txt). Attempts 3–5 preserved direct stop-line diagnostics; no further Matrix attempts were made. +**NOT PASSED; CANDIDATE RESULT INVALIDATED** via [`evidence/layer4-results.txt`](evidence/layer4-results.txt), [`evidence/layer4-case-results.txt`](evidence/layer4-case-results.txt), and [`evidence/layer4-cleanup.txt`](evidence/layer4-cleanup.txt). -### Cleanup +The historical machine observations remain unchanged: one authenticated Matrix request, one target Worker response, five plugin registration events, one `before_prompt_build` event, and zero trusted model-handler events. The route probe now proves that turn used the stale stock PI runner, so it cannot establish either candidate success or candidate bypass. -The final exact-resource cleanup passed: Team, both Workers, both Worker containers, the derived research image, owned storage prefixes, and the `tiangong-m9a0-` container prefix were absent. See [`evidence/layer4-cleanup.txt`](evidence/layer4-cleanup.txt). +The first two attempts were driver failures involving room and sender selection. Attempts 3–5 preserved bounded diagnostics. All historical Team, Worker, container, image, Room-membership, and storage resources were removed. No corrected Matrix attempt has been authorized or executed. ## Historical compaction bypass @@ -182,8 +203,9 @@ The same dependency contains model-backed branch summarization, but source audit - deterministic session rollover from durable facts: **not implemented**; - semantic or manual model compaction: **not supported in required mode**; -- Basic Matrix Layer 4: **RED; trusted handler bypass stop condition**; -- M9-A formal implementation: **still blocked by the A0 stop line**. +- corrected Basic Matrix Layer 4: **not authorized or executed**; +- historical Layer 4 candidate verdict: **invalidated by stale stock runtime routing**; +- M9-A formal implementation: **still blocked until corrected Layer 4 passes**. Until rollover or a trusted compaction seam is implemented, long sessions may reach OpenClaw's ordinary context-overflow failure/recovery behavior. This run proves that compaction cannot issue an untrusted provider request; it does not prove a seamless long-session user experience. @@ -201,6 +223,12 @@ Until rollover or a trusted compaction seam is implemented, long sessions may re - [`evidence/layer3-results.txt`](evidence/layer3-results.txt) - [`evidence/layer3-case-results.txt`](evidence/layer3-case-results.txt) - [`evidence/layer3-control-handler.test.ts`](evidence/layer3-control-handler.test.ts) +- [`evidence/layer4-route-probe-results.txt`](evidence/layer4-route-probe-results.txt) +- [`evidence/layer4-route-build-diagnostic.txt`](evidence/layer4-route-build-diagnostic.txt) +- [`evidence/layer4-research-image.Dockerfile`](evidence/layer4-research-image.Dockerfile) +- [`evidence/layer4-stale-dist-diagnostic.Dockerfile`](evidence/layer4-stale-dist-diagnostic.Dockerfile) +- [`evidence/layer4-tsdown.config.ts`](evidence/layer4-tsdown.config.ts) +- [`evidence/layer4-runtime-postbuild.mjs`](evidence/layer4-runtime-postbuild.mjs) - [`evidence/layer4-results.txt`](evidence/layer4-results.txt) - [`evidence/layer4-case-results.txt`](evidence/layer4-case-results.txt) - [`evidence/layer4-cleanup.txt`](evidence/layer4-cleanup.txt) @@ -213,8 +241,9 @@ Until rollover or a trusted compaction seam is implemented, long sessions may re - [`evidence/layer4-fifth-attempt-diagnostic.txt`](evidence/layer4-fifth-attempt-diagnostic.txt) - [`run-layer2-remaining.sh`](run-layer2-remaining.sh) - [`run-layer3-control-handler.sh`](run-layer3-control-handler.sh) +- [`run-layer4-route-probe.sh`](run-layer4-route-probe.sh) - [`run-layer4-basic-matrix.sh`](run-layer4-basic-matrix.sh) ## Cleanup -Layer 2 and Layer 3 runners removed only their exact owned containers. The Layer 4 final cleanup verified the exact Team, Workers, containers, derived research image, storage prefixes, Manager Room membership check, and the `tiangong-m9a0-` container prefix were absent. Temporary test roots and providers were removed by test teardown. No credential was committed or recorded. +Layer 2 and Layer 3 runners removed only their exact owned containers. The historical Layer 4 final cleanup verified the exact Team, Workers, containers, derived research image, storage prefixes, Manager Room membership check, and the `tiangong-m9a0-` container prefix were absent. The route probe removed both exact owned images and all four exact owned containers. Temporary test roots and providers were removed by test teardown. No credential was committed or recorded. diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer4-basic-matrix.sh b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer4-basic-matrix.sh index 2330732..0891840 100755 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer4-basic-matrix.sh +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer4-basic-matrix.sh @@ -9,6 +9,9 @@ readonly EXPECTED_BASE_IMAGE_ID="sha256:ac1cb183e5b2c82982f6473fef86ccf128763a31 readonly IMAGE="tiangong-m9a0-layer4:dev" readonly PATCH="${EVIDENCE_DIR}/openclaw-2026.4.14-trusted-native-boundaries.patch" readonly DOCKERFILE="${EVIDENCE_DIR}/layer4-research-image.Dockerfile" +readonly BUILD_CONFIG="${EVIDENCE_DIR}/layer4-tsdown.config.ts" +readonly EXPECTED_BUILD_CONFIG_SHA256="3b3fae95354fd32692692ca7da4051e850c7a75885cb2f34a99f1dda3828da38" +readonly RUNTIME_POSTBUILD="${EVIDENCE_DIR}/layer4-runtime-postbuild.mjs" readonly TEAM_FIXTURE="${EVIDENCE_DIR}/layer4-team.yaml" readonly WORKER_FIXTURE="${EVIDENCE_DIR}/layer4-workers.yaml" readonly MATRIX_TURN="${EVIDENCE_DIR}/layer4-matrix-turn.sh" @@ -22,6 +25,7 @@ readonly MANAGER_CONTAINER="agentteams-manager" readonly OPENCLAW_VERSION="OpenClaw 2026.4.14 (2f35b6f)" readonly CONTAINER_PREFIX="tiangong-m9a0-" readonly NONCE="$(printf '%s' "$$-$(date -u +%Y%m%dT%H%M%SZ)" | sha256sum | cut -c1-24)" +readonly IMAGE_PROBE_CONTAINER="tiangong-m9a0-layer4-image-probe-${NONCE}" readonly EVENT_FILE="/root/agentteams-fs/agents/${MEMBER_NAME}/.tiangong/runtime/m9-a0-layer4/events.ndjson" resources_reserved=0 @@ -187,6 +191,7 @@ cleanup() { wait_absent || cleanup_failed=1 purge_storage || cleanup_failed=1 fi + docker rm -f "${IMAGE_PROBE_CONTAINER}" >/dev/null 2>&1 || true if ((image_owned == 1)); then docker image rm "${IMAGE}" >/dev/null 2>&1 || cleanup_failed=1 fi @@ -202,11 +207,19 @@ cleanup() { } trap cleanup EXIT INT TERM -for path in "${PATCH}" "${DOCKERFILE}" "${TEAM_FIXTURE}" "${WORKER_FIXTURE}" "${MATRIX_TURN}"; do +for path in \ + "${PATCH}" \ + "${DOCKERFILE}" \ + "${BUILD_CONFIG}" \ + "${RUNTIME_POSTBUILD}" \ + "${TEAM_FIXTURE}" \ + "${WORKER_FIXTURE}" \ + "${MATRIX_TURN}"; do [[ -f "${path}" && ! -L "${path}" ]] || fail "LAYER4_ASSET_MISSING" done [[ -x "${MATRIX_TURN}" ]] || fail "LAYER4_MATRIX_SCRIPT_NOT_EXECUTABLE" [[ "$(sha256sum "${PATCH}" | awk '{print $1}')" == "3c81b8333aa23772989763a2328396e3132d424ce438f81e841f9e3bfc6019eb" ]] || fail "LAYER4_PATCH_DIGEST_MISMATCH" +[[ "$(sha256sum "${BUILD_CONFIG}" | awk '{print $1}')" == "${EXPECTED_BUILD_CONFIG_SHA256}" ]] || fail "LAYER4_BUILD_CONFIG_DIGEST_MISMATCH" base_image_id="$(docker image inspect --format '{{.Id}}' "${BASE_IMAGE}" 2>/dev/null || true)" [[ "${base_image_id}" == "${EXPECTED_BASE_IMAGE_ID}" ]] || fail "LAYER4_BASE_IMAGE_MISMATCH" for name in "${TEAM_NAME}" "${LEADER_NAME}" "${MEMBER_NAME}"; do @@ -218,9 +231,13 @@ done if docker image inspect "${IMAGE}" >/dev/null 2>&1; then fail "LAYER4_IMAGE_ALREADY_EXISTS" fi +if docker container inspect "${IMAGE_PROBE_CONTAINER}" >/dev/null 2>&1; then + fail "LAYER4_PROBE_CONTAINER_ALREADY_EXISTS" +fi -printf 'layer=4\nstatus=running\nbase_image=%s\nbase_image_id=%s\npatch_sha256=%s\nopenclaw_expected=%s\nnetwork=official-agentteams-matrix\nnonce=%s\n' \ - "${BASE_IMAGE}" "${base_image_id}" "$(sha256sum "${PATCH}" | awk '{print $1}')" "${OPENCLAW_VERSION}" "${NONCE}" +printf 'layer=4\nstatus=running\nbase_image=%s\nbase_image_id=%s\npatch_sha256=%s\nbuild_config_sha256=%s\nopenclaw_expected=%s\nnetwork=official-agentteams-matrix\nnonce=%s\n' \ + "${BASE_IMAGE}" "${base_image_id}" "$(sha256sum "${PATCH}" | awk '{print $1}')" \ + "$(sha256sum "${BUILD_CONFIG}" | awk '{print $1}')" "${OPENCLAW_VERSION}" "${NONCE}" build_log="$(mktemp)" if ! docker build --network none --file "${DOCKERFILE}" --tag "${IMAGE}" "${EVIDENCE_DIR}" >"${build_log}" 2>&1; then @@ -232,7 +249,8 @@ fi rm -f "${build_log}" image_owned=1 derived_image_id="$(docker image inspect --format '{{.Id}}' "${IMAGE}")" -derived_version="$(docker run --rm --network none --entrypoint openclaw "${IMAGE}" --version)" +derived_version="$(docker run --rm --name "${IMAGE_PROBE_CONTAINER}" --network none \ + --entrypoint openclaw "${IMAGE}" --version)" [[ "${derived_version}" == "${OPENCLAW_VERSION}" ]] || fail "LAYER4_OPENCLAW_VERSION_MISMATCH" printf 'layer4_image=%s\nlayer4_image_id=%s\nlayer4_openclaw_version=%s\nresearch_member_responsibility=developer\nresearch_member_runtime=openclaw-built-in\nresearch_member_model=qwen3.5-plus\n' "${IMAGE}" "${derived_image_id}" "${derived_version}" diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer4-route-probe.sh b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer4-route-probe.sh new file mode 100755 index 0000000..fb98ee2 --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer4-route-probe.sh @@ -0,0 +1,197 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +RUN_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +EVIDENCE_DIR="${RUN_DIR}/evidence" +readonly RUN_DIR EVIDENCE_DIR +readonly BASE_IMAGE="tg-worker:dev" +readonly EXPECTED_BASE_IMAGE_ID="sha256:ac1cb183e5b2c82982f6473fef86ccf128763a31192d711526d843a79edb69ff" +readonly EXPECTED_VERSION="OpenClaw 2026.4.14 (2f35b6f)" +readonly PATCH="${EVIDENCE_DIR}/openclaw-2026.4.14-trusted-native-boundaries.patch" +readonly EXPECTED_PATCH_SHA256="3c81b8333aa23772989763a2328396e3132d424ce438f81e841f9e3bfc6019eb" +readonly BUILD_CONFIG="${EVIDENCE_DIR}/layer4-tsdown.config.ts" +readonly EXPECTED_BUILD_CONFIG_SHA256="3b3fae95354fd32692692ca7da4051e850c7a75885cb2f34a99f1dda3828da38" +readonly STALE_DOCKERFILE="${EVIDENCE_DIR}/layer4-stale-dist-diagnostic.Dockerfile" +readonly CORRECTED_DOCKERFILE="${EVIDENCE_DIR}/layer4-research-image.Dockerfile" +readonly STALE_IMAGE="tiangong-m9a0-layer4-stale-route-probe:dev" +readonly CORRECTED_IMAGE="tiangong-m9a0-layer4-corrected-route-probe:dev" +readonly STALE_CONTAINER="tiangong-m9a0-layer4-stale-route-probe-$$" +readonly CORRECTED_CONTAINER="tiangong-m9a0-layer4-corrected-route-probe-$$" +readonly VERSION_CONTAINER="tiangong-m9a0-layer4-version-route-probe-$$" +readonly LOADER_CONTAINER="tiangong-m9a0-layer4-loader-route-probe-$$" +readonly STARTED_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + +STALE_BUILD_LOG="$(mktemp)" +CORRECTED_BUILD_LOG="$(mktemp)" +cleanup_failed=0 + +cleanup() { + local status=$? + trap - EXIT INT TERM + for container in \ + "${STALE_CONTAINER}" \ + "${CORRECTED_CONTAINER}" \ + "${VERSION_CONTAINER}" \ + "${LOADER_CONTAINER}"; do + docker rm -f "${container}" >/dev/null 2>&1 || true + done + for image in "${STALE_IMAGE}" "${CORRECTED_IMAGE}"; do + if docker image inspect "${image}" >/dev/null 2>&1; then + docker image rm "${image}" >/dev/null 2>&1 || cleanup_failed=1 + fi + done + rm -f "${STALE_BUILD_LOG}" "${CORRECTED_BUILD_LOG}" + for container in \ + "${STALE_CONTAINER}" \ + "${CORRECTED_CONTAINER}" \ + "${VERSION_CONTAINER}" \ + "${LOADER_CONTAINER}"; do + if docker container inspect "${container}" >/dev/null 2>&1; then + cleanup_failed=1 + fi + done + for image in "${STALE_IMAGE}" "${CORRECTED_IMAGE}"; do + if docker image inspect "${image}" >/dev/null 2>&1; then + cleanup_failed=1 + fi + done + printf 'cleanup_stale_image_absent=%s\n' "$(! docker image inspect "${STALE_IMAGE}" >/dev/null 2>&1 && echo true || echo false)" + printf 'cleanup_corrected_image_absent=%s\n' "$(! docker image inspect "${CORRECTED_IMAGE}" >/dev/null 2>&1 && echo true || echo false)" + printf 'cleanup_owned_containers_absent=%s\n' "$([[ ${cleanup_failed} -eq 0 ]] && echo true || echo false)" + printf 'ended_at=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" + if ((status == 0 && cleanup_failed != 0)); then + status=1 + fi + exit "${status}" +} +trap cleanup EXIT INT TERM + +fail() { + printf 'route_probe_failure=%s\n' "$1" >&2 + exit 1 +} + +field() { + local payload="$1" key="$2" + awk -F= -v key="${key}" '$1 == key { print substr($0, length(key) + 2); exit }' <<<"${payload}" +} + +build_image() { + local image="$1" dockerfile="$2" log="$3" + if ! docker build --network none --pull=false --file "${dockerfile}" --tag "${image}" \ + "${EVIDENCE_DIR}" >"${log}" 2>&1; then + tail -40 "${log}" >&2 + return 1 + fi +} + +probe_graph() { + local image="$1" container="$2" + docker run --name "${container}" --network none --entrypoint /bin/sh "${image}" -c ' + set -eu + cd /opt/openclaw/dist + pi_files="$(find . -maxdepth 1 -type f -name "pi-embedded-runner-*.js" -printf "%f\n" | sort)" + pi_file_count="$(printf "%s\n" "${pi_files}" | sed "/^$/d" | wc -l | tr -d " ")" + pi_reference_files="$(grep -RohE "pi-embedded-runner-[A-Za-z0-9_-]+[.]js" . --include="*.js" | sort -u)" + pi_reference_count="$(printf "%s\n" "${pi_reference_files}" | sed "/^$/d" | wc -l | tr -d " ")" + agent_runtime="$(sed -n "s/^export [*] from \"[.]\\/\\([^\"]*\\)\";$/\\1/p" agent-runner.runtime.js)" + test -n "${agent_runtime}" + test -f "${agent_runtime}" + agent_pi_runner="$(grep -oE "pi-embedded-runner-[A-Za-z0-9_-]+[.]js" "${agent_runtime}" | sort -u)" + agent_pi_runner_count="$(printf "%s\n" "${agent_pi_runner}" | sed "/^$/d" | wc -l | tr -d " ")" + agent_pi_trusted_install_count=0 + if [ "${agent_pi_runner_count}" -eq 1 ]; then + agent_pi_trusted_install_count="$(grep -c "installTiangongTrustedBoundariesFromEnv" "${agent_pi_runner}" || true)" + fi + patched_pi_file_count=0 + for file in ${pi_files}; do + if grep -q "installTiangongTrustedBoundariesFromEnv" "${file}"; then + patched_pi_file_count=$((patched_pi_file_count + 1)) + fi + done + printf "pi_file_count=%s\n" "${pi_file_count}" + printf "pi_reference_count=%s\n" "${pi_reference_count}" + printf "agent_runtime=%s\n" "${agent_runtime}" + printf "agent_pi_runner=%s\n" "${agent_pi_runner}" + printf "agent_pi_runner_count=%s\n" "${agent_pi_runner_count}" + printf "agent_pi_trusted_install_count=%s\n" "${agent_pi_trusted_install_count}" + printf "patched_pi_file_count=%s\n" "${patched_pi_file_count}" + ' +} + +for path in \ + "${PATCH}" \ + "${BUILD_CONFIG}" \ + "${STALE_DOCKERFILE}" \ + "${CORRECTED_DOCKERFILE}" \ + "${EVIDENCE_DIR}/layer4-runtime-postbuild.mjs" \ + "${EVIDENCE_DIR}/layer4-stale-tsdown.config.mjs" \ + "${EVIDENCE_DIR}/layer4-plugin-loader.test.ts"; do + [[ -f "${path}" && ! -L "${path}" ]] || fail "ROUTE_PROBE_ASSET_MISSING" +done +[[ "$(sha256sum "${PATCH}" | awk '{print $1}')" == "${EXPECTED_PATCH_SHA256}" ]] || fail "ROUTE_PROBE_PATCH_DIGEST_MISMATCH" +[[ "$(sha256sum "${BUILD_CONFIG}" | awk '{print $1}')" == "${EXPECTED_BUILD_CONFIG_SHA256}" ]] || fail "ROUTE_PROBE_BUILD_CONFIG_DIGEST_MISMATCH" +base_image_id="$(docker image inspect --format '{{.Id}}' "${BASE_IMAGE}" 2>/dev/null || true)" +[[ "${base_image_id}" == "${EXPECTED_BASE_IMAGE_ID}" ]] || fail "ROUTE_PROBE_BASE_IMAGE_MISMATCH" +for image in "${STALE_IMAGE}" "${CORRECTED_IMAGE}"; do + ! docker image inspect "${image}" >/dev/null 2>&1 || fail "ROUTE_PROBE_IMAGE_ALREADY_EXISTS" +done +for container in \ + "${STALE_CONTAINER}" \ + "${CORRECTED_CONTAINER}" \ + "${VERSION_CONTAINER}" \ + "${LOADER_CONTAINER}"; do + ! docker container inspect "${container}" >/dev/null 2>&1 || fail "ROUTE_PROBE_CONTAINER_ALREADY_EXISTS" +done + +printf 'layer=4-route-probe\nstatus=running\nstarted_at=%s\nbase_image=%s\nbase_image_id=%s\nopenclaw_expected=%s\npatch_sha256=%s\nbuild_config_sha256=%s\nnetwork=none\nmatrix_turns=0\nprovider_turns=0\n' \ + "${STARTED_AT}" "${BASE_IMAGE}" "${base_image_id}" "${EXPECTED_VERSION}" \ + "${EXPECTED_PATCH_SHA256}" "${EXPECTED_BUILD_CONFIG_SHA256}" + +build_image "${STALE_IMAGE}" "${STALE_DOCKERFILE}" "${STALE_BUILD_LOG}" || fail "ROUTE_PROBE_STALE_BUILD_FAILED" +stale_image_id="$(docker image inspect --format '{{.Id}}' "${STALE_IMAGE}")" +stale_graph="$(probe_graph "${STALE_IMAGE}" "${STALE_CONTAINER}")" +docker rm "${STALE_CONTAINER}" >/dev/null +[[ "$(field "${stale_graph}" pi_file_count)" == "2" ]] || fail "ROUTE_PROBE_STALE_PI_COUNT_UNEXPECTED" +[[ "$(field "${stale_graph}" pi_reference_count)" == "2" ]] || fail "ROUTE_PROBE_STALE_PI_REFERENCE_COUNT_UNEXPECTED" +[[ "$(field "${stale_graph}" agent_pi_runner_count)" == "1" ]] || fail "ROUTE_PROBE_STALE_AGENT_ROUTE_AMBIGUOUS" +[[ "$(field "${stale_graph}" agent_pi_trusted_install_count)" == "0" ]] || fail "ROUTE_PROBE_STALE_ALIAS_NOT_STALE" +[[ "$(field "${stale_graph}" patched_pi_file_count)" == "1" ]] || fail "ROUTE_PROBE_STALE_PATCHED_CHUNK_COUNT_UNEXPECTED" +printf 'stale_image_id=%s\n%s\n' "${stale_image_id}" "$(sed 's/^/stale_/' <<<"${stale_graph}")" + +build_image "${CORRECTED_IMAGE}" "${CORRECTED_DOCKERFILE}" "${CORRECTED_BUILD_LOG}" || fail "ROUTE_PROBE_CORRECTED_BUILD_FAILED" +corrected_image_id="$(docker image inspect --format '{{.Id}}' "${CORRECTED_IMAGE}")" +corrected_version="$(docker run --rm --name "${VERSION_CONTAINER}" --network none \ + --entrypoint openclaw "${CORRECTED_IMAGE}" --version)" +[[ "${corrected_version}" == "${EXPECTED_VERSION}" ]] || fail "ROUTE_PROBE_CORRECTED_VERSION_MISMATCH" +corrected_graph="$(probe_graph "${CORRECTED_IMAGE}" "${CORRECTED_CONTAINER}")" +docker rm "${CORRECTED_CONTAINER}" >/dev/null +[[ "$(field "${corrected_graph}" pi_file_count)" == "1" ]] || fail "ROUTE_PROBE_CORRECTED_PI_COUNT_UNEXPECTED" +[[ "$(field "${corrected_graph}" pi_reference_count)" == "1" ]] || fail "ROUTE_PROBE_CORRECTED_PI_REFERENCE_COUNT_UNEXPECTED" +[[ "$(field "${corrected_graph}" agent_pi_runner_count)" == "1" ]] || fail "ROUTE_PROBE_CORRECTED_AGENT_ROUTE_AMBIGUOUS" +[[ "$(field "${corrected_graph}" agent_pi_trusted_install_count)" -ge 1 ]] || fail "ROUTE_PROBE_CORRECTED_TRUSTED_INSTALL_MISSING" +[[ "$(field "${corrected_graph}" patched_pi_file_count)" == "1" ]] || fail "ROUTE_PROBE_CORRECTED_PATCHED_CHUNK_COUNT_UNEXPECTED" +printf 'corrected_image_id=%s\ncorrected_openclaw_version=%s\n%s\n' \ + "${corrected_image_id}" "${corrected_version}" "$(sed 's/^/corrected_/' <<<"${corrected_graph}")" + +docker run --name "${LOADER_CONTAINER}" --network none \ + --mount "type=bind,src=${RUN_DIR},dst=/m9-a0,readonly" \ + --entrypoint /bin/sh "${CORRECTED_IMAGE}" -c ' + set -eu + cd /opt/openclaw + cp /m9-a0/evidence/layer4-plugin-loader.test.ts \ + src/agents/pi-embedded-runner/layer4-plugin-loader.test.ts + ./node_modules/.bin/oxfmt --check \ + src/agents/pi-embedded-runner/layer4-plugin-loader.test.ts + ./node_modules/.bin/vitest run \ + src/agents/pi-embedded-runner/layer4-plugin-loader.test.ts \ + --reporter=verbose + node --input-type=module -e '\'' + const runtime = await import("./dist/agent-runner.runtime.js"); + if (typeof runtime.runReplyAgent !== "function") process.exit(1); + console.log("compiled_agent_runner_runtime_import=pass"); + '\'' + ' +docker rm "${LOADER_CONTAINER}" >/dev/null + +printf 'plugin_loader=pass\ncompiled_agent_runner_runtime_import=pass\nroot_cause=STALE_DIST_RUNTIME_ALIAS_SELECTED_UNPATCHED_PI_RUNNER\ncorrected_route_contract=pass\nterminal_status=pass\n' From 4e8a9babe34694592abc324bb3058629c1441bbf Mon Sep 17 00:00:00 2001 From: Jay Shen Date: Sun, 23 Aug 2026 18:57:41 +0800 Subject: [PATCH 11/13] test: fix Layer 4 bootstrap digest oracle Signed-off-by: Jay Shen --- ...er4-corrected-first-attempt-diagnostic.txt | 20 +++++++++++++++++++ .../evidence/layer4-results.txt | 15 +++++++------- .../run-layer4-basic-matrix.sh | 2 +- 3 files changed, 28 insertions(+), 9 deletions(-) create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-corrected-first-attempt-diagnostic.txt diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-corrected-first-attempt-diagnostic.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-corrected-first-attempt-diagnostic.txt new file mode 100644 index 0000000..c26d87f --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-corrected-first-attempt-diagnostic.txt @@ -0,0 +1,20 @@ +attempt=corrected_1 +matrix_response=pass +trusted_handler_observed=true +trusted_model_handler_records= +{"event":"trusted-model-handler","provider":"agentteams-gateway","model":"qwen3.5-plus","bootstrapBundleDigest":"2ee41285d5e2b703f167860ffd19e9c4446196be7caa3b3d3e610a05d4efb98b","agentsDigest":"79f61971edfec23f65e7ef3628400e29c35c075472699c2492c98f193ffa7773","soulDigest":"de2ecf87e8b565134ee4d03d2604073c2b5caefff2cd8dcf92c2dccaeb06ebd5","payloadObject":true,"sessionKeyPresent":true} +research_plugin_registered_events=5 + +research_before_prompt_build_events=1 + +trusted_model_handler_events=1 + +trusted_tool_release_events=0 + +agents_digest=79f61971edfec23f65e7ef3628400e29c35c075472699c2492c98f193ffa7773 +soul_digest=de2ecf87e8b565134ee4d03d2604073c2b5caefff2cd8dcf92c2dccaeb06ebd5 +expected_bundle_digest_without_newline=2ee41285d5e2b703f167860ffd19e9c4446196be7caa3b3d3e610a05d4efb98b +classification=test-driver-oracle +failure_code=LAYER4_BOOTSTRAP_BUNDLE_DIGEST_MISMATCH +hypothesis=runner_jq_digest_included_trailing_newline +next_action=correct_digest_calculation_then_retry_once diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-results.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-results.txt index d26ab8c..022be6b 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-results.txt +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-results.txt @@ -3,24 +3,23 @@ status=running base_image=tg-worker:dev base_image_id=sha256:ac1cb183e5b2c82982f6473fef86ccf128763a31192d711526d843a79edb69ff patch_sha256=3c81b8333aa23772989763a2328396e3132d424ce438f81e841f9e3bfc6019eb +build_config_sha256=3b3fae95354fd32692692ca7da4051e850c7a75885cb2f34a99f1dda3828da38 openclaw_expected=OpenClaw 2026.4.14 (2f35b6f) network=official-agentteams-matrix -nonce=fa1d281fcc61c91710a63192 +nonce=b196cdb6ce79c93b5610846b layer4_image=tiangong-m9a0-layer4:dev -layer4_image_id=sha256:53e32bc43ceea1994b136bd066a33b6d94c4c5daa5f72a1f6f7aada1a9d4ff1c +layer4_image_id=sha256:dac895fbfec5eb1c48f3da6237b9ee4b7678d58e0158179d68f3a634a4b5bcc2 layer4_openclaw_version=OpenClaw 2026.4.14 (2f35b6f) research_member_responsibility=developer research_member_runtime=openclaw-built-in research_member_model=qwen3.5-plus -matrix_request_event_id=$hFZcopLRI-7m8bMRmNODVNERGFSl9HMT735pHR-lq3I -matrix_response_event_id=$TK4x0MyQi_gt0IXbow7nrjyvg-PdPGTeUQHte4IDv0I +matrix_request_event_id=$UcY72HOVWa5O2LGLw6TTrCAesqYkJ4bemmVpcOPfTNw +matrix_response_event_id=$1rAyR2g35kx8qvEkPYrazUiqVxD5GeMl2j7P0pUA5-A matrix_response_sender=@tiangong-m9a0-layer4-member:matrix-local.agentteams.io:18080 matrix_response_body_length=42 -matrix_response_body_sha256=6a46b7f6b9737c0f56cc891dcfc75f256243c7d824fb03da979bf7e5db2dceba +matrix_response_body_sha256=30f39bf16211139fa9b658771941c55de776e07bd6bfb9016c205ae10c8ce423 matrix_response=pass -layer4_failure_code=LAYER4_TRUSTED_MODEL_HANDLER_NOT_OBSERVED +layer4_failure_code=LAYER4_BOOTSTRAP_BUNDLE_DIGEST_MISMATCH cleanup_team_absent=false cleanup_image_absent=false cleanup_owned_container_prefix_absent=true -terminal_status=stop -final_cleanup_evidence=layer4-cleanup.txt diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer4-basic-matrix.sh b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer4-basic-matrix.sh index 0891840..d237fc8 100755 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer4-basic-matrix.sh +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer4-basic-matrix.sh @@ -296,7 +296,7 @@ plugin_registered_count="$(docker exec "${MEMBER_CONTAINER}" jq -c 'select(.even agents_digest="$(sha256sum "${EVIDENCE_DIR}/layer4-AGENTS.md" | awk '{print $1}')" soul_digest="$(sha256sum "${EVIDENCE_DIR}/layer4-SOUL.md" | awk '{print $1}')" -bundle_digest="$(jq -cn --arg agents "${agents_digest}" --arg soul "${soul_digest}" '{agents:$agents,soul:$soul}' | sha256sum | awk '{print $1}')" +bundle_digest="$(printf '%s' "$(jq -cn --arg agents "${agents_digest}" --arg soul "${soul_digest}" '{agents:$agents,soul:$soul}')" | sha256sum | awk '{print $1}')" before_lines="$(docker exec "${MEMBER_CONTAINER}" sh -lc 'f="$1"; if [ -f "$f" ]; then wc -l <"$f"; else echo 0; fi' sh "${EVENT_FILE}")" matrix_output="$(docker exec "${MANAGER_CONTAINER}" /bin/bash /tmp/tiangong-m9a0-layer4-matrix-turn.sh "${team_room_id}" "${member_user_id}" "${NONCE}")" || fail "LAYER4_MATRIX_MEMBER_TURN_FAILED" printf '%s\n' "${matrix_output}" From d5c218a3b33974bcf1a22c41bd200cc936a4db2c Mon Sep 17 00:00:00 2001 From: Jay Shen Date: Sun, 23 Aug 2026 19:08:29 +0800 Subject: [PATCH 12/13] test: harden Layer 4 cleanup Signed-off-by: Jay Shen --- .../evidence/layer4-cleanup.txt | 5 +++- ...r4-corrected-second-attempt-diagnostic.txt | 21 ++++++++++++++++ .../evidence/layer4-results.txt | 25 ++++++++++++++----- .../run-layer4-basic-matrix.sh | 14 ++++++++--- 4 files changed, 55 insertions(+), 10 deletions(-) create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-corrected-second-attempt-diagnostic.txt diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-cleanup.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-cleanup.txt index e329578..24b9a42 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-cleanup.txt +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-cleanup.txt @@ -1,6 +1,9 @@ layer=4 -final_cleanup=pass cleanup_scope=tiangong-m9a0-layer4 +corrected_canary_runner_cleanup_team_absent=false +corrected_canary_runner_cleanup_image_absent=false +corrected_canary_runner_cleanup_owned_container_prefix_absent=true +manual_recovery_cleanup=pass team_absent=true leader_worker_absent=true member_worker_absent=true diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-corrected-second-attempt-diagnostic.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-corrected-second-attempt-diagnostic.txt new file mode 100644 index 0000000..814e8a6 --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-corrected-second-attempt-diagnostic.txt @@ -0,0 +1,21 @@ +attempt=corrected_2 +status=matrix_boundary_pass_cleanup_runner_failed +image=tiangong-m9a0-layer4:dev +image_id=sha256:3e075aace419c3d91aa05717a0722343258ce5582f2aa76142964d2f608e1b1e +openclaw=OpenClaw 2026.4.14 (2f35b6f) +patch_sha256=3c81b8333aa23772989763a2328396e3132d424ce438f81e841f9e3bfc6019eb +build_config_sha256=3b3fae95354fd32692692ca7da4051e850c7a75885cb2f34a99f1dda3828da38 +network=official-agentteams-matrix +matrix_response=pass +trusted_model_handler_events=1 +trusted_tool_release_events=0 +model_payload_shape=object +bootstrap_agents_digest=79f61971edfec23f65e7ef3628400e29c35c075472699c2492c98f193ffa7773 +bootstrap_soul_digest=de2ecf87e8b565134ee4d03d2604073c2b5caefff2cd8dcf92c2dccaeb06ebd5 +bootstrap_bundle_digest=2ee41285d5e2b703f167860ffd19e9c4446196be7caa3b3d3e610a05d4efb98b +runner_cleanup_team_absent=false +runner_cleanup_image_absent=false +runner_cleanup_owned_container_prefix_absent=true +classification=cleanup-driver +manual_exact_resource_cleanup=pass +next_action=record_pass_and_keep_cleanup_retry_fix diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-results.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-results.txt index 022be6b..ae0ae0c 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-results.txt +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-results.txt @@ -6,20 +6,33 @@ patch_sha256=3c81b8333aa23772989763a2328396e3132d424ce438f81e841f9e3bfc6019eb build_config_sha256=3b3fae95354fd32692692ca7da4051e850c7a75885cb2f34a99f1dda3828da38 openclaw_expected=OpenClaw 2026.4.14 (2f35b6f) network=official-agentteams-matrix -nonce=b196cdb6ce79c93b5610846b +nonce=5ed99b29dd68ebee6446683f layer4_image=tiangong-m9a0-layer4:dev -layer4_image_id=sha256:dac895fbfec5eb1c48f3da6237b9ee4b7678d58e0158179d68f3a634a4b5bcc2 +layer4_image_id=sha256:3e075aace419c3d91aa05717a0722343258ce5582f2aa76142964d2f608e1b1e layer4_openclaw_version=OpenClaw 2026.4.14 (2f35b6f) research_member_responsibility=developer research_member_runtime=openclaw-built-in research_member_model=qwen3.5-plus -matrix_request_event_id=$UcY72HOVWa5O2LGLw6TTrCAesqYkJ4bemmVpcOPfTNw -matrix_response_event_id=$1rAyR2g35kx8qvEkPYrazUiqVxD5GeMl2j7P0pUA5-A +matrix_request_event_id=$AveRMjvDc0jn88VF5JuDoJKTga5rDFrKgVDNQi_j9eQ +matrix_response_event_id=$An97aCweJS2Tut5DCW0S7kHz_4yGr6P22SoRnqF-QPU matrix_response_sender=@tiangong-m9a0-layer4-member:matrix-local.agentteams.io:18080 matrix_response_body_length=42 -matrix_response_body_sha256=30f39bf16211139fa9b658771941c55de776e07bd6bfb9016c205ae10c8ce423 +matrix_response_body_sha256=5e8aaa3f1fe34512221273a6b18900c8495f80e6e9b3396e3aadb7933a059ee1 matrix_response=pass -layer4_failure_code=LAYER4_BOOTSTRAP_BUNDLE_DIGEST_MISMATCH +layer4_matrix_member_turn=pass +member_matrix_user_id=@tiangong-m9a0-layer4-member:matrix-local.agentteams.io:18080 +member_room_id=!rZOBAtjIoznZkVfHI3:matrix-local.agentteams.io:18080 +team_room_id=!0E0KkMGicKK4Me7Vkr:matrix-local.agentteams.io:18080 +member_event_lines_before=3 +member_event_lines_after=7 +research_plugin_registered_events=3 +research_before_prompt_build_events=1 +trusted_model_handler_events=1 +trusted_tool_release_events=0 +bootstrap_agents_digest=79f61971edfec23f65e7ef3628400e29c35c075472699c2492c98f193ffa7773 +bootstrap_soul_digest=de2ecf87e8b565134ee4d03d2604073c2b5caefff2cd8dcf92c2dccaeb06ebd5 +bootstrap_bundle_digest=2ee41285d5e2b703f167860ffd19e9c4446196be7caa3b3d3e610a05d4efb98b +model_payload_shape=object cleanup_team_absent=false cleanup_image_absent=false cleanup_owned_container_prefix_absent=true diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer4-basic-matrix.sh b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer4-basic-matrix.sh index d237fc8..1cb8efe 100755 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer4-basic-matrix.sh +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer4-basic-matrix.sh @@ -139,8 +139,14 @@ purge_storage() { return "${failed}" } +delete_owned_workers() { + docker exec "${MANAGER_CONTAINER}" agt delete worker "${LEADER_NAME}" >/dev/null 2>&1 || true + docker exec "${MANAGER_CONTAINER}" agt delete worker "${MEMBER_NAME}" >/dev/null 2>&1 || true +} + wait_absent() { - for _ in $(seq 1 180); do + local attempt + for attempt in $(seq 1 180); do if ! team_json >/dev/null 2>&1 && \ ! member_json "${LEADER_NAME}" >/dev/null 2>&1 && \ ! member_json "${MEMBER_NAME}" >/dev/null 2>&1 && \ @@ -148,6 +154,9 @@ wait_absent() { ! container_exists "${MEMBER_CONTAINER}"; then return 0 fi + if ((attempt % 10 == 0)); then + delete_owned_workers + fi sleep 2 done return 1 @@ -186,8 +195,7 @@ cleanup() { leave_room "${leader_room_id}" || cleanup_failed=1 leave_room "${member_room_id}" || cleanup_failed=1 docker exec "${MANAGER_CONTAINER}" agt delete team "${TEAM_NAME}" >/dev/null 2>&1 || true - docker exec "${MANAGER_CONTAINER}" agt delete worker "${LEADER_NAME}" >/dev/null 2>&1 || true - docker exec "${MANAGER_CONTAINER}" agt delete worker "${MEMBER_NAME}" >/dev/null 2>&1 || true + delete_owned_workers wait_absent || cleanup_failed=1 purge_storage || cleanup_failed=1 fi From 14a56e635a88acbcf6134fa4b2e9cf99950a80b3 Mon Sep 17 00:00:00 2001 From: Jay Shen Date: Sun, 23 Aug 2026 19:16:04 +0800 Subject: [PATCH 13/13] test: record corrected Layer 4 canary pass Signed-off-by: Jay Shen --- .../evidence/artifact-sha256.txt | 9 +- .../evidence/layer4-case-results.txt | 85 +++++++++---------- .../evidence/layer4-cleanup.txt | 7 +- ...er4-corrected-third-attempt-diagnostic.txt | 22 +++++ .../evidence/layer4-results.txt | 20 +++-- .../plan.md | 10 ++- .../result.md | 37 ++++++-- 7 files changed, 117 insertions(+), 73 deletions(-) create mode 100644 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-corrected-third-attempt-diagnostic.txt diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/artifact-sha256.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/artifact-sha256.txt index 8e5c6a6..ca617f0 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/artifact-sha256.txt +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/artifact-sha256.txt @@ -15,9 +15,12 @@ add4d645e2a12c5f5293931ef8dd6bbf5b3bbb1cebf01e4474922980e1271726 smoke-testing/ 62b91062b9043076e3f0c061f5cbb8b0eaf78f23759f8cc2e7d50828156e7c36 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer3-control-handler.test.ts 5a7fd3916db856de90490af2758f149890988054ba2f614982be71b4955f237e smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer3-results.txt 79f61971edfec23f65e7ef3628400e29c35c075472699c2492c98f193ffa7773 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-AGENTS.md -37f479ef8585addc9de1e432bd19c06f69ee24ba47f48593bd35729beb2cf8c6 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-case-results.txt -47019b4c7da7d4b2b6b155b133045b9babf82591cef0a1c6501164738bd75872 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-cleanup.txt +c801ceae16a4a2a843d333ba0c4fc07eb16a917952d3be9841940dec6ec2a0d0 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-case-results.txt +683a8b2659ec33057a0dbfd1a0b90690c1515b411631820ff2cf8c9ee970f0f8 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-cleanup.txt 23fd93ff9161a41433054870ec7624e200b0d61cfb4f105f35a5cf09c942e090 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-control-plugin.mjs +7986cad3188d84898464659c8a1a0cb8b1d177a2c3a35d4bb943f3c0eb0db807 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-corrected-first-attempt-diagnostic.txt +df6660d56d16494dda2836372499a6a6a73f23bf94efb9a2b28e2506d8fd8bae smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-corrected-second-attempt-diagnostic.txt +2086d8a40f6dbbc3ae035c779858fbac285e1bcff7e7c334e2040727b9f6af7f smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-corrected-third-attempt-diagnostic.txt e38657d08e61f5adc4476bce7d0b3196d810a58049c2d2a6c7d3bb886588011a smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-fifth-attempt-diagnostic.txt e13b9daeb82342f7f8e1791919b26c336382c1fc3bccb19e59a11b0251435c90 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-first-attempt-diagnostic.txt b4b672aa5a573c4555e8dbfa87984990e3ef236a665f6a803a44051f6daf57cf smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-fourth-attempt-diagnostic.txt @@ -25,7 +28,7 @@ b4b672aa5a573c4555e8dbfa87984990e3ef236a665f6a803a44051f6daf57cf smoke-testing/ bcde3d661dba31eb3ab9cd81a043afa49a1620c44e07194e6efe4c06669476ae smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-plugin-loader-results.txt 8f48d929955c994cd7c2314661459e6eb24b3004d14e89913a0a47995f40d010 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-plugin-loader.test.ts b157c767da5c2e1c62ce658c15d6cb48c0fba5a17604b3ea129a68267fdbe247 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-research-image.Dockerfile -206492cc9a16de2e14643b89a699eb2c523fb0763f14655255779349874aa0e3 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-results.txt +373802316af25a4377b3add5edba44adceff34adefa87637002afb21529e4d32 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-results.txt 889435ba6e692ccf0fd3f13be7397ea7bbc7beb2656e45e889fb1672048cc563 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-route-build-diagnostic.txt 04437e81e46f272cd6f51fdaf34d0681e4f136ec30b9e2fe340e5ee7238083c4 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-route-probe-results.txt 0294418ec57e4526fb02431059859c7ae5f6cff1b5ad2beb3711f3d3a0e99c43 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-runtime-postbuild.mjs diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-case-results.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-case-results.txt index 4246337..6e2bdd2 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-case-results.txt +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-case-results.txt @@ -1,54 +1,45 @@ layer=4 -status=not_passed -previous_status=stop -previous_stop_condition=SUPPORTED_MATRIX_MODEL_PATH_BYPASSED_TRUSTED_MODEL_HANDLER -previous_stop_condition_superseded=true -current_block_reason=CORRECTED_RESEARCH_IMAGE_NOT_MATRIX_VERIFIED -image_base=tg-worker:dev -image_base_id=sha256:ac1cb183e5b2c82982f6473fef86ccf128763a31192d711526d843a79edb69ff -historical_research_image=tiangong-m9a0-layer4:dev -historical_research_image_id=sha256:53e32bc43ceea1994b136bd066a33b6d94c4c5daa5f72a1f6f7aada1a9d4ff1c +status=pass +historical_candidate_verdict=invalidated_by_stale_stock_runtime_routing +route_probe=pass +route_probe_network=none +route_probe_matrix_turns=0 +route_probe_provider_turns=0 +root_cause_historical=STALE_DIST_RUNTIME_ALIAS_SELECTED_UNPATCHED_PI_RUNNER + +base_image=tg-worker:dev +base_image_id=sha256:ac1cb183e5b2c82982f6473fef86ccf128763a31192d711526d843a79edb69ff +research_image=tiangong-m9a0-layer4:dev +research_image_id=sha256:b77bb1771f6a0e181aedf4f62206ad826aa37c7e6aaecd2ae5b5dc9e90ab7137 openclaw=OpenClaw 2026.4.14 (2f35b6f) patch_sha256=3c81b8333aa23772989763a2328396e3132d424ce438f81e841f9e3bfc6019eb -historical_network=official-agentteams-matrix +build_config_sha256=3b3fae95354fd32692692ca7da4051e850c7a75885cb2f34a99f1dda3828da38 activation=TIANGONG_TRUSTED_BOUNDARIES_REQUIRED=1 member_config_projection=developer,openclaw-built-in,qwen3.5-plus,tiangong-developer@1.0.0,revision-1 -historical_matrix_human_source=disposable_authenticated_admin -historical_matrix_request_count=1 -historical_matrix_target_response_count=1 -historical_matrix_target_response_sender_preserved=true -historical_matrix_target_response_body_recorded=false -historical_matrix_target_response_body_sha256_recorded=true -historical_research_plugin_registered_events=5 -historical_research_before_prompt_build_events=1 -historical_trusted_model_handler_events=0 -historical_trusted_tool_release_handler_events=0 -historical_lower_level_plugin_loader=pass - -route_probe=pass -route_probe_network=none -route_probe_matrix_turns=0 -route_probe_provider_turns=0 -stale_pi_runner_files=2 -stale_agent_runtime_trusted_install_count=0 -stale_patched_pi_runner_files=1 -corrected_pi_runner_files=1 -corrected_pi_runner_references=1 -corrected_agent_runtime_trusted_install_count=2 -corrected_patched_pi_runner_files=1 -corrected_plugin_loader=pass -corrected_compiled_agent_runtime_import=pass -root_cause=STALE_DIST_RUNTIME_ALIAS_SELECTED_UNPATCHED_PI_RUNNER -failure_classification=test-driver/research-image-build-artifact -candidate_matrix_bypass_proven=false +corrected_attempt_count=3 +corrected_attempt_matrix_request_count_each=1 +corrected_attempt_matrix_response_count_each=1 +corrected_total_matrix_requests=3 +corrected_total_matrix_responses=3 +corrected_canary_attempt=3 +accepted_canary_matrix_request_count=1 +accepted_canary_matrix_response_count=1 +matrix_response_sender_preserved=true +matrix_response_body_recorded=false +matrix_response_body_sha256_recorded=true +model_turn_count=1 +trusted_model_handler_events=1 +trusted_tool_release_events=0 +model_payload_shape=object +bootstrap_agents_digest=79f61971edfec23f65e7ef3628400e29c35c075472699c2492c98f193ffa7773 +bootstrap_soul_digest=de2ecf87e8b565134ee4d03d2604073c2b5caefff2cd8dcf92c2dccaeb06ebd5 +bootstrap_bundle_digest=2ee41285d5e2b703f167860ffd19e9c4446196be7caa3b3d3e610a05d4efb98b +runner_exit=0 +cleanup=pass -attempt_1=HTTP_403_manager_private_room -attempt_2=HTTP_403_manager_team_room -attempt_3=matrix_response_pass_trusted_handler_zero -attempt_4=member_config_projection_missing_then_corrected -attempt_5=matrix_response_pass_trusted_handler_zero_on_stale_stock_pi_runtime -no_corrected_matrix_attempts=true -formal_m9_a=blocked -cleanup_historical_resources=pass -cleanup_route_probe_resources=pass +corrected_attempt_1=matrix_boundary_reached_test_driver_digest_oracle_failed +corrected_attempt_2=matrix_boundary_passed_cleanup_driver_failed_manual_cleanup_passed +corrected_attempt_3=matrix_boundary_passed_runner_cleanup_passed +no_further_matrix_attempts=true +formal_m9_a_implementation=not_started diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-cleanup.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-cleanup.txt index 24b9a42..7331ac3 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-cleanup.txt +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-cleanup.txt @@ -1,9 +1,10 @@ layer=4 cleanup_scope=tiangong-m9a0-layer4 -corrected_canary_runner_cleanup_team_absent=false -corrected_canary_runner_cleanup_image_absent=false +corrected_canary_attempt=3 +corrected_canary_runner_exit=0 +corrected_canary_runner_cleanup_team_absent=true +corrected_canary_runner_cleanup_image_absent=true corrected_canary_runner_cleanup_owned_container_prefix_absent=true -manual_recovery_cleanup=pass team_absent=true leader_worker_absent=true member_worker_absent=true diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-corrected-third-attempt-diagnostic.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-corrected-third-attempt-diagnostic.txt new file mode 100644 index 0000000..128c74d --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-corrected-third-attempt-diagnostic.txt @@ -0,0 +1,22 @@ +attempt=corrected_3 +status=pass +image=tiangong-m9a0-layer4:dev +image_id=sha256:b77bb1771f6a0e181aedf4f62206ad826aa37c7e6aaecd2ae5b5dc9e90ab7137 +openclaw=OpenClaw 2026.4.14 (2f35b6f) +patch_sha256=3c81b8333aa23772989763a2328396e3132d424ce438f81e841f9e3bfc6019eb +build_config_sha256=3b3fae95354fd32692692ca7da4051e850c7a75885cb2f34a99f1dda3828da38 +network=official-agentteams-matrix +matrix_request_count=1 +matrix_response_count=1 +matrix_response_body_recorded=false +matrix_response_body_digest_recorded=true +model_turn_count=1 +trusted_model_handler_events=1 +trusted_tool_release_events=0 +model_payload_shape=object +bootstrap_agents_digest=79f61971edfec23f65e7ef3628400e29c35c075472699c2492c98f193ffa7773 +bootstrap_soul_digest=de2ecf87e8b565134ee4d03d2604073c2b5caefff2cd8dcf92c2dccaeb06ebd5 +bootstrap_bundle_digest=2ee41285d5e2b703f167860ffd19e9c4446196be7caa3b3d3e610a05d4efb98b +runner_exit=0 +cleanup=pass +classification=corrected-layer4-boundary-pass diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-results.txt b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-results.txt index ae0ae0c..bc5ea58 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-results.txt +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-results.txt @@ -6,23 +6,23 @@ patch_sha256=3c81b8333aa23772989763a2328396e3132d424ce438f81e841f9e3bfc6019eb build_config_sha256=3b3fae95354fd32692692ca7da4051e850c7a75885cb2f34a99f1dda3828da38 openclaw_expected=OpenClaw 2026.4.14 (2f35b6f) network=official-agentteams-matrix -nonce=5ed99b29dd68ebee6446683f +nonce=952ef28af5c6c5c8ed9ebdc5 layer4_image=tiangong-m9a0-layer4:dev -layer4_image_id=sha256:3e075aace419c3d91aa05717a0722343258ce5582f2aa76142964d2f608e1b1e +layer4_image_id=sha256:b77bb1771f6a0e181aedf4f62206ad826aa37c7e6aaecd2ae5b5dc9e90ab7137 layer4_openclaw_version=OpenClaw 2026.4.14 (2f35b6f) research_member_responsibility=developer research_member_runtime=openclaw-built-in research_member_model=qwen3.5-plus -matrix_request_event_id=$AveRMjvDc0jn88VF5JuDoJKTga5rDFrKgVDNQi_j9eQ -matrix_response_event_id=$An97aCweJS2Tut5DCW0S7kHz_4yGr6P22SoRnqF-QPU +matrix_request_event_id=$YY-0S2sj3OmHiU3xsLqMARXuhOSc-u1TvHv7U0WhzjU +matrix_response_event_id=$TLmHoFcHEUlnnuVyrDjHlYxfuHItql5Kko_FjUcOUWk matrix_response_sender=@tiangong-m9a0-layer4-member:matrix-local.agentteams.io:18080 matrix_response_body_length=42 -matrix_response_body_sha256=5e8aaa3f1fe34512221273a6b18900c8495f80e6e9b3396e3aadb7933a059ee1 +matrix_response_body_sha256=0e88c184cee46e7fbab6e4de271f66fd333924c462025f831001a3b7a6fbbf3c matrix_response=pass layer4_matrix_member_turn=pass member_matrix_user_id=@tiangong-m9a0-layer4-member:matrix-local.agentteams.io:18080 -member_room_id=!rZOBAtjIoznZkVfHI3:matrix-local.agentteams.io:18080 -team_room_id=!0E0KkMGicKK4Me7Vkr:matrix-local.agentteams.io:18080 +member_room_id=!rYGcjn8VPXVkDlR3cQ:matrix-local.agentteams.io:18080 +team_room_id=!KkPzKzCWAhlM95VPCE:matrix-local.agentteams.io:18080 member_event_lines_before=3 member_event_lines_after=7 research_plugin_registered_events=3 @@ -33,6 +33,8 @@ bootstrap_agents_digest=79f61971edfec23f65e7ef3628400e29c35c075472699c2492c98f19 bootstrap_soul_digest=de2ecf87e8b565134ee4d03d2604073c2b5caefff2cd8dcf92c2dccaeb06ebd5 bootstrap_bundle_digest=2ee41285d5e2b703f167860ffd19e9c4446196be7caa3b3d3e610a05d4efb98b model_payload_shape=object -cleanup_team_absent=false -cleanup_image_absent=false +cleanup_team_absent=true +cleanup_image_absent=true cleanup_owned_container_prefix_absent=true +runner_exit=0 +terminal_status=pass diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/plan.md b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/plan.md index 781d89d..8ae08ff 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/plan.md +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/plan.md @@ -1,6 +1,6 @@ # M9-A0 trusted native-boundary follow-up -> Status: Layer 1, Layer 2, and the disposable Layer 3 control-handler prototype pass. The authorized Layer 4 Matrix run did not pass, but a network-none route probe proved that its reduced research-image build retained a stale stock PI runtime alias and therefore did not exercise the candidate final model seam. The corrected full build graph passes deterministic route checks; no corrected Matrix turn is authorized yet, so A0 remains blocked at Layer 4. +> Status: Layer 1, Layer 2, the disposable Layer 3 control-handler prototype, and the corrected Layer 4 Basic Matrix canary pass. The historical Layer 4 verdict was invalidated by a stale stock runtime graph; the corrected full graph exercised the candidate and observed the trusted model handler. A0 is complete for review; formal M9-A implementation remains separate and has not started. ## Scope @@ -8,7 +8,7 @@ - Baseline: pinned OpenClaw `2026.4.14` (`2f35b6f`) in the existing Worker image - Predecessor: [`../2026-08-22-m9-a0-source-seam-prototype/result.md`](../2026-08-22-m9-a0-source-seam-prototype/result.md) - Purpose: prove the thinnest OpenClaw source seams that satisfy the accepted M9-A0 model and ToolResult contracts -- Product boundary: research patch, disposable image, and test assets only; no Tiangong runtime enablement, OpenClaw upgrade, dependency modification, database change, or formal implementation. The historical Layer 4 run used one disposable AgentTeams/Matrix fixture and a real provider turn; the corrected route probe is offline and a corrected Matrix turn requires new authorization. +- Product boundary: research patch, disposable image, and test assets only; no Tiangong runtime enablement, OpenClaw upgrade, dependency modification, database change, or formal implementation. Three bounded diagnostic Matrix/provider turns used an owned disposable AgentTeams fixture; the final accepted canary was one turn confirming the compiled route and trusted boundary. The predecessor remains a blocked historical result. Its `before_model_call` placement before `activeSession.prompt(...)` and its raw tool-wrapper capture must not be reused. @@ -133,7 +133,7 @@ Current result: **PASS**. Root cause: `STALE_DIST_RUNTIME_ALIAS_SELECTED_UNPATCH The historical authorized run observed one request, one target Worker response, one `before_prompt_build` event, and zero trusted model-handler events. Those observations remain valid, but the route probe proves they came through the stale stock PI runner. The historical run therefore did not test the candidate and cannot pass or fail its Matrix boundary. -The corrected [`run-layer4-basic-matrix.sh`](run-layer4-basic-matrix.sh) now builds the full pinned graph and verifies its build-config digest before provisioning any resource. It must not be run until a new Matrix attempt is explicitly authorized. If authorized later, use: +The corrected [`run-layer4-basic-matrix.sh`](run-layer4-basic-matrix.sh) built the full pinned graph and verified its build-config digest before provisioning resources. This continuation explicitly authorized and executed the canary with: ```bash set -o pipefail @@ -141,7 +141,9 @@ smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer4-basic-m 2>&1 | tee smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-results.txt ``` -Current result: **NOT PASSED / BLOCKED**. No corrected Matrix turn has been executed. Formal M9-A implementation remains blocked. +Current result: **PASS**. The corrected canary observed one Matrix request, one target response, one trusted model-handler event with the exact immutable bootstrap digests, and an object-shaped payload. The explicit prompt prohibited tools, so zero ToolResult-release events is expected. The final runner exit was 0 and all exact resources were cleaned. + +The first corrected attempt exposed a test-driver digest-oracle newline bug; the second reached the boundary but exposed a cleanup retry bug; the third was the accepted canary. Both defects were fixed with direct evidence before the final canary. No further Matrix attempt is authorized or needed. A0 is ready for review; formal M9-A implementation remains separate and has not started. ## Evidence requirements diff --git a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/result.md b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/result.md index 0aee5de..2e3a6af 100644 --- a/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/result.md +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/result.md @@ -2,7 +2,7 @@ ## Decision -**Layer 1, Layer 2, the disposable Layer 3 control-handler prototype, and the network-none Layer 4 route/build probe pass. The historical Layer 4 Matrix run did not pass, but it also did not exercise the candidate: its research image retained a stale stable runtime alias to the stock PI runner. The corrected image graph has not received an authorized Matrix turn, so A0 and formal M9-A remain blocked at Layer 4. Model-backed compaction remains fail-closed and unavailable in required mode.** +**Layer 1, Layer 2, the disposable Layer 3 control-handler prototype, the corrected Layer 4 route/build probe, and the corrected Layer 4 Basic Matrix canary pass. The historical Layer 4 verdict is invalidated because its image retained a stale stable runtime alias to the stock PI runner. The corrected canary exercised the candidate trusted model seam and completed with exact cleanup. A0 is ready for review; formal M9-A implementation remains separate and has not started. Model-backed compaction remains fail-closed and unavailable in required mode.** The initial real compaction diagnostic proved that pinned `pi-coding-agent` calls `completeSimple(...)` outside the session `agent.onPayload` path. The separately installed handler could not protect that provider call. Following an explicit source-patch decision, the candidate now disables Pi AgentSession auto-compaction and denies OpenClaw manual, budget, overflow, queued/harness, and context-engine-owned compaction paths in required mode. @@ -10,9 +10,9 @@ This is not semantic compaction support. Manual compaction remains disabled beca Layer 3 passes as a disposable Tiangong control-handler prototype. The initial Layer 4 conclusion that the real Matrix path bypassed the candidate is superseded by direct compiled-graph evidence: the reduced two-entry build was merged with stock `dist/`, leaving `agent-runner.runtime.js` pointed at an unpatched stock PI runner while a separate patched runner chunk was unused. The stock runner already emits `before_prompt_build`, which explains the otherwise misleading readiness observation. -The corrected research build uses the exact public pinned OpenClaw unified `tsdown` graph and fails unless the compiled graph contains exactly one PI runner selected by the stable agent runtime and containing the trusted install call. This lower-level correction passes, but no corrected Matrix attempt was made or is authorized by this continuation. M9-A formal implementation therefore remains blocked. +The corrected research build uses the exact public pinned OpenClaw unified `tsdown` graph and fails unless the compiled graph contains exactly one PI runner selected by the stable agent runtime and containing the trusted install call. This lower-level correction passed, and the corrected Matrix canary below exercised that graph. M9-A formal implementation has not started; it remains a separate review and authorization step. -The candidate remains a research artifact. No product Tiangong runtime, installed OpenClaw tree, dependency, or database was changed. The historical Layer 4 run used only owned disposable resources, all removed or verified absent. The route probe used two disposable `--network none` images and no Matrix/provider turn; both images and all owned containers were removed. +The candidate remains a research artifact. No product Tiangong runtime, installed OpenClaw tree, dependency, or database was changed. The historical and corrected Layer 4 runs used only owned disposable resources, all removed or verified absent. The route probe used two disposable `--network none` images and no Matrix/provider turn; the corrected canary used one disposable Matrix/provider turn and its Team, Workers, image, containers, and storage were cleaned by the final successful runner. ## Pinned source contract @@ -186,7 +186,27 @@ Both probe images and all owned containers were removed. The historical machine observations remain unchanged: one authenticated Matrix request, one target Worker response, five plugin registration events, one `before_prompt_build` event, and zero trusted model-handler events. The route probe now proves that turn used the stale stock PI runner, so it cannot establish either candidate success or candidate bypass. -The first two attempts were driver failures involving room and sender selection. Attempts 3–5 preserved bounded diagnostics. All historical Team, Worker, container, image, Room-membership, and storage resources were removed. No corrected Matrix attempt has been authorized or executed. +The first two attempts were driver failures involving room and sender selection. Attempts 3–5 preserved bounded diagnostics. All historical Team, Worker, container, image, Room-membership, and storage resources were removed. At the time of this historical result, no corrected Matrix attempt had yet been authorized or executed. + +## Corrected Layer 4 Basic Matrix canary + +**PASS** via [`evidence/layer4-results.txt`](evidence/layer4-results.txt), [`evidence/layer4-case-results.txt`](evidence/layer4-case-results.txt), [`evidence/layer4-corrected-third-attempt-diagnostic.txt`](evidence/layer4-corrected-third-attempt-diagnostic.txt), and [`evidence/layer4-cleanup.txt`](evidence/layer4-cleanup.txt). + +The user-authorized continuation made three bounded corrected diagnostic attempts, each provisioning only the owned disposable fixture and sending one authenticated Admin Human Matrix event. The third and accepted canary built the full pinned OpenClaw graph, provisioned one disposable stock Leader and one corrected OpenClaw member Worker, and observed the target response. + +Direct boundary facts: + +- OpenClaw identity: `2026.4.14 (2f35b6f)`; +- corrected research image: one pinned image ID, one PI runner, one stable runtime reference, and the trusted install call in the selected runner; +- Matrix requests: **1**; target responses: **1**; response body retained only as length/digest; +- trusted `before_model_call` events: **1**; +- bootstrap provenance: exact immutable `AGENTS.md` and `SOUL.md` digests plus matching bundle digest; +- model payload shape: object; +- trusted ToolResult-release events: **0**, expected because the bounded prompt explicitly prohibited tools; +- runner exit: **0**; +- cleanup: Team, Workers, containers, research image, storage prefixes, and owned container prefix all absent. + +The first corrected attempt reached the trusted handler but exposed a test-driver digest-oracle newline bug. After that fix, the second attempt passed the Matrix boundary but exposed a cleanup retry bug; manual cleanup passed and the runner cleanup retry was hardened. The third corrected canary passed with runner exit 0. These were test-driver/cleanup findings, not trusted-boundary failures. No further Matrix attempt is needed. ## Historical compaction bypass @@ -203,9 +223,9 @@ The same dependency contains model-backed branch summarization, but source audit - deterministic session rollover from durable facts: **not implemented**; - semantic or manual model compaction: **not supported in required mode**; -- corrected Basic Matrix Layer 4: **not authorized or executed**; +- corrected Basic Matrix Layer 4: **PASS**; - historical Layer 4 candidate verdict: **invalidated by stale stock runtime routing**; -- M9-A formal implementation: **still blocked until corrected Layer 4 passes**. +- M9-A formal implementation: **not started; separate review/authorization remains required**. Until rollover or a trusted compaction seam is implemented, long sessions may reach OpenClaw's ordinary context-overflow failure/recovery behavior. This run proves that compaction cannot issue an untrusted provider request; it does not prove a seamless long-session user experience. @@ -239,6 +259,9 @@ Until rollover or a trusted compaction seam is implemented, long sessions may re - [`evidence/layer4-third-attempt-diagnostic.txt`](evidence/layer4-third-attempt-diagnostic.txt) - [`evidence/layer4-fourth-attempt-diagnostic.txt`](evidence/layer4-fourth-attempt-diagnostic.txt) - [`evidence/layer4-fifth-attempt-diagnostic.txt`](evidence/layer4-fifth-attempt-diagnostic.txt) +- [`evidence/layer4-corrected-first-attempt-diagnostic.txt`](evidence/layer4-corrected-first-attempt-diagnostic.txt) +- [`evidence/layer4-corrected-second-attempt-diagnostic.txt`](evidence/layer4-corrected-second-attempt-diagnostic.txt) +- [`evidence/layer4-corrected-third-attempt-diagnostic.txt`](evidence/layer4-corrected-third-attempt-diagnostic.txt) - [`run-layer2-remaining.sh`](run-layer2-remaining.sh) - [`run-layer3-control-handler.sh`](run-layer3-control-handler.sh) - [`run-layer4-route-probe.sh`](run-layer4-route-probe.sh) @@ -246,4 +269,4 @@ Until rollover or a trusted compaction seam is implemented, long sessions may re ## Cleanup -Layer 2 and Layer 3 runners removed only their exact owned containers. The historical Layer 4 final cleanup verified the exact Team, Workers, containers, derived research image, storage prefixes, Manager Room membership check, and the `tiangong-m9a0-` container prefix were absent. The route probe removed both exact owned images and all four exact owned containers. Temporary test roots and providers were removed by test teardown. No credential was committed or recorded. +Layer 2 and Layer 3 runners removed only their exact owned containers. The historical Layer 4 cleanup and the corrected final Layer 4 runner verified the exact Team, Workers, containers, derived research image, storage prefixes, Manager Room membership check, and the `tiangong-m9a0-` container prefix were absent. The route probe removed both exact owned images and all four exact owned containers. Temporary test roots and providers were removed by test teardown. No credential was committed or recorded.