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 的协调; 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..dd9be53 --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-source-seam-prototype/result.md @@ -0,0 +1,74 @@ +# 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. + +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. 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..ca617f0 --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/artifact-sha256.txt @@ -0,0 +1,46 @@ +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-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 +89d904dd891687c94c7b78297d9c3063e2faaf7f8595647f10e540510a0545e9 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-pre-guard-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 +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 +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 +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 +3b4eefc64a548791858499c5b36bfaf643fca0e2d0717926e26c59d7b1a891f1 smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-matrix-turn.sh +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 +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 +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 +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 +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-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-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-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 new file mode 100644 index 0000000..3f3787a --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-compaction.test.ts @@ -0,0 +1,276 @@ +import fs from "node:fs/promises"; +import http from "node:http"; +import os from "node:os"; +import path from "node:path"; +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[] = []; + +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>; +}> { + 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, + }; +} + +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); +} + +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(); + 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 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 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 { 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 }); + 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-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..109b1be --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-diagnostic-cleanup.txt @@ -0,0 +1,2 @@ +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-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-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 new file mode 100644 index 0000000..b9dd86e --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-readiness-results.txt @@ -0,0 +1,51 @@ +container=tiangong-m9a0-layer2-1092704_20260823T102726Z +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 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 + + ✓ 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  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 +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 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 77901ms + + Test Files  1 passed (1) + Tests  1 passed (1) + 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-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 new file mode 100644 index 0000000..cd60304 --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-remaining-results.txt @@ -0,0 +1,81 @@ +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 +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 39ms 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 84145ms + + Test Files  1 passed (1) + Tests  1 passed (1) + 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 50607ms + + Test Files  1 passed (1) + Tests  1 passed (1) + 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 18432ms + + Test Files  1 passed (1) + Tests  1 passed (1) + 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 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 70662ms + + Test Files  1 passed (1) + Tests  2 passed (2) + 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-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 new file mode 100644 index 0000000..faf2318 --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-results.txt @@ -0,0 +1,59 @@ +layer=2 +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 +patch_sha256=3c81b8333aa23772989763a2328396e3132d424ce438f81e841f9e3bfc6019eb + +unpatched_registered_provider_baseline=pass +unpatched_registered_provider_requests=1 +patched_runEmbeddedAttempt_main_post_tool=pass +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=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 + +layer3_tiangong_handler=pass +layer3_deterministic_session_rollover=not_started +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 +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 new file mode 100644 index 0000000..c48a2ad --- /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,193 @@ +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 "./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 { + 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 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())); + } + while (roots.length > 0) await fs.rm(roots.pop()!, { recursive: true, force: true }); +}); + +describe("M9-A0 unpatched runEmbeddedAttempt baseline", () => { + 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; + 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 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(); + 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 new file mode 100644 index 0000000..1e349b0 --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer2-run-embedded-attempt.test.ts @@ -0,0 +1,365 @@ +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 }); + 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 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"); + 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 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, + 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 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", + "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; + } + }, 180_000); +}); 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/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..44cf170 --- /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-1099157_20260823T103348Z +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-1099157_20260823T103348Z +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..062bb50 --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer3-results.txt @@ -0,0 +1,41 @@ +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 +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 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 + +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 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  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-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-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..6e2bdd2 --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-case-results.txt @@ -0,0 +1,45 @@ +layer=4 +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 +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 + +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 + +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 new file mode 100644 index 0000000..7331ac3 --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-cleanup.txt @@ -0,0 +1,20 @@ +layer=4 +cleanup_scope=tiangong-m9a0-layer4 +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 +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-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-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-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-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..e13a0cb --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-fifth-attempt-diagnostic.txt @@ -0,0 +1,19 @@ +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 +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-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..9eedea2 --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-research-image.Dockerfile @@ -0,0 +1,58 @@ +FROM tg-worker:dev + +USER root + +COPY openclaw-2026.4.14-trusted-native-boundaries.patch /tmp/m9-a0-layer4.patch +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; \ + cd /opt/openclaw; \ + 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.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; \ + 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 -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 \ + && 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..bc5ea58 --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-results.txt @@ -0,0 +1,40 @@ +layer=4 +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=952ef28af5c6c5c8ed9ebdc5 +layer4_image=tiangong-m9a0-layer4:dev +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=$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=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=!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 +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=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/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-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-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-stale-tsdown.config.mjs b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/layer4-stale-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-stale-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-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.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/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/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..0b41747 --- /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,422 @@ +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..a0a10ce 100644 +--- a/src/agents/pi-embedded-runner/compact.ts ++++ b/src/agents/pi-embedded-runner/compact.ts +@@ -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..d9e90d9 100644 +--- a/src/agents/pi-embedded-runner/run/attempt.ts ++++ b/src/agents/pi-embedded-runner/run/attempt.ts +@@ -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"; + 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); ++ } + 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: { ++ runId: params.runId, ++ agentId: sessionAgentId, ++ sessionId: activeSession.sessionId, ++ workspaceDir: effectiveWorkspace, ++ ...(params.sessionKey ? { sessionKey: params.sessionKey } : {}), ++ ...(params.trigger ? { trigger: params.trigger } : {}), ++ }, ++ 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..abc3393 +--- /dev/null ++++ b/src/agents/pi-embedded-runner/trusted-boundaries.ts +@@ -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"; ++import type { ++ PluginHookAgentContext, ++ PluginHookBeforeModelCallResult, ++ 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"; ++export const TIANGONG_MODEL_COMPACTION_DISABLED_REASON = ++ "TRUSTED_BOUNDARY_MODEL_COMPACTION_DISABLED"; ++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) || isToolResultError(event.result), ++ ...(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; ++} ++ ++/** 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; ++ 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 +@@ -57,6 +57,7 @@ export type PluginHookName = + | "before_prompt_build" + | "before_agent_start" + | "before_agent_reply" ++ | "before_model_call" + | "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" + | "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", + "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", + "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; ++ provider: string; ++ model: string; ++ runId?: string; ++}; ++ ++/** Explicit success handshake; missing or malformed results fail closed. */ ++export type PluginHookBeforeModelCallResult = { ++ allow: true; ++ payload: unknown; ++}; ++ + 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; ++ params: unknown; ++ runId?: string; ++ toolCallId: string; ++ result: unknown; ++ isError: boolean; ++}; ++ ++/** Explicit success handshake; missing or malformed results fail closed. */ ++export type PluginHookBeforeToolResultReleaseResult = { ++ release: true; ++}; ++ + 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; + 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, ++ ) => ++ | 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 new file mode 100644 index 0000000..f0165d6 --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/source-contract-results.txt @@ -0,0 +1,56 @@ +container=tiangong-m9a0-source-1091966_20260823T102706Z +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 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 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 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 4ms + + Test Files  2 passed (2) + Tests  11 passed (11) + 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 + +···························································································································································· + + Test Files  4 passed (4) + Tests  156 passed (156) + 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-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/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..f142109 --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/evidence/trusted-boundaries.test.ts @@ -0,0 +1,501 @@ +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, + resolveTiangongModelCompactionGuard, + TIANGONG_MODEL_COMPACTION_DISABLED_REASON, +} 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("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: [] }), + 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..8ae08ff --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/plan.md @@ -0,0 +1,177 @@ +# M9-A0 trusted native-boundary follow-up + +> 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 + +- 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: 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. 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. + +## 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 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: 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: + +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. **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. + +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. +- 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 + +| 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 | +| 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 does 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 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. + +Current result: **PASS**, boundary/provider tests **11/11**, adjacent regressions **156/156**. + +### Layer 2: actual patched OpenClaw attempt and local fake provider + +Run both reproducible runners: + +```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 + +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 +``` + +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 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 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 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. + +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 +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 +``` + +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 + +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 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. + +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 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 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, 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 new file mode 100644 index 0000000..2e3a6af --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/result.md @@ -0,0 +1,272 @@ +# M9-A0 trusted native-boundaries result + +## Decision + +**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. + +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 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 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 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 + +**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 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** +- 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 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 + +**PASS** via [`evidence/layer2-readiness-results.txt`](evidence/layer2-readiness-results.txt). + +- 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. + +### Normalized tool error + +**PASS** via [`evidence/layer2-remaining-results.txt`](evidence/layer2-remaining-results.txt). + +- provider requests: **2**; +- tool executions: **1**; +- 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-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. + +The handler failure therefore stops release before ordinary ToolResult emission and before the next provider request. + +### Persisted follow-up + +**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: + +- provider requests: **2**; +- model handlers: **2**; +- event order: `attempt-ready → model-handler → attempt-ready → model-handler`; +- persisted user messages: **2**. + +This proves the final model seam is reinstalled on the persisted-session follow-up path. + +### Required-mode compaction guard + +**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). + +With `TIANGONG_TRUSTED_BOUNDARIES_REQUIRED=1`: + +- 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. + +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. + +## 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. + +Direct corrected observations: + +- 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. + +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. + +Both probe images and all owned containers were removed. + +## Historical Layer 4 Basic Matrix turn + +**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). + +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. 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 + +The pre-guard diagnostic is retained as bounded failure evidence: + +- [`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 + +- deterministic session rollover from durable facts: **not implemented**; +- semantic or manual model compaction: **not supported in required mode**; +- corrected Basic Matrix Layer 4: **PASS**; +- historical Layer 4 candidate verdict: **invalidated by stale stock runtime routing**; +- 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. + +## Evidence index + +- [`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/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) +- [`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) +- [`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) +- [`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) +- [`run-layer4-basic-matrix.sh`](run-layer4-basic-matrix.sh) + +## Cleanup + +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. 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..454b8c5 --- /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="3c81b8333aa23772989763a2328396e3132d424ce438f81e841f9e3bfc6019eb" +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" 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-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" 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..1cb8efe --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-layer4-basic-matrix.sh @@ -0,0 +1,332 @@ +#!/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 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" +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 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 +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}" +} + +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() { + 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 && \ + ! container_exists "${LEADER_CONTAINER}" && \ + ! container_exists "${MEMBER_CONTAINER}"; then + return 0 + fi + if ((attempt % 10 == 0)); then + delete_owned_workers + 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 + delete_owned_workers + 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 + 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}" \ + "${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 + 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 +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\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 + 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 --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}" + +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="$(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}" + +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}" 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' 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..6d052df --- /dev/null +++ b/smoke-testing/runs/2026-08-22-m9-a0-trusted-native-boundaries/run-source-contract.sh @@ -0,0 +1,91 @@ +#!/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-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/run.ts \ + src/agents/pi-embedded-runner/compact.ts \ + src/agents/pi-embedded-runner/compact.queued.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"