From 057859931c6f9472565e680528839fb7f276b4b7 Mon Sep 17 00:00:00 2001 From: Steven Welch Date: Sun, 27 Sep 2026 20:17:26 -0600 Subject: [PATCH 1/7] test(opencode): isolate two-arm init runtime feasibility probe --- .github/tests/opencode-init-runtime/README.md | 81 +++++ .github/tests/opencode-init-runtime/http.mjs | 55 +++ .../tests/opencode-init-runtime/prepare.sh | 26 ++ .github/tests/opencode-init-runtime/probe.mjs | 57 +++ .../tests/opencode-init-runtime/runtime.py | 325 ++++++++++++++++++ .github/workflows/opencode-init-runtime.yml | 22 ++ 6 files changed, 566 insertions(+) create mode 100644 .github/tests/opencode-init-runtime/README.md create mode 100644 .github/tests/opencode-init-runtime/http.mjs create mode 100644 .github/tests/opencode-init-runtime/prepare.sh create mode 100644 .github/tests/opencode-init-runtime/probe.mjs create mode 100644 .github/tests/opencode-init-runtime/runtime.py create mode 100644 .github/workflows/opencode-init-runtime.yml diff --git a/.github/tests/opencode-init-runtime/README.md b/.github/tests/opencode-init-runtime/README.md new file mode 100644 index 0000000..ca0c29c --- /dev/null +++ b/.github/tests/opencode-init-runtime/README.md @@ -0,0 +1,81 @@ +# Init Runtime Feasibility + +Owner: makeitworkcloud. Isolated prototype, not chart adoption or release approval. +The PR-only workflow uses hosted Docker and the unchanged upstream +`ghcr.io/anomalyco/opencode:1.18.29@sha256:ecc3bf96ee55dad226d9cde50d79aaa8a1215c47860c0fcdc71570461bf438b8` +on linux/amd64. No Dockerfile, image build, image push, registry login, custom +ONNX/glibc, provider fallback, production mount, or cluster operation exists. + +## Lifecycle and Gate + +One root init helper extracts official packages into a UUID runtime volume, +using the stock image's trusted public apk keys and strictly matched Alpine +3.24 repositories. Node >=22.13, gcompat, libgcc/libstdc++ and apk-resolved +dependencies are prepared once. Binutils lives in a separate inspection volume, +never the application. Applications mount runtime dependencies read-only. + +A uses PATH and LD_LIBRARY_PATH only. B adds +`LD_PRELOAD=/opt/runtime/lib/libgcompat.so.0`. Both use distinct fresh UUID HOME +volumes; application paths are identical. Each has its own 15-minute budget and +B runs even if A fails. Common provisioning failure prevents both and is red. +Control A is evidence, never the gate. Green requires B's complete cold and warm +sequence AND successful cleanup. Any B failure ends this experiment. + +Applications retain the upstream entrypoint, use numeric UID/GID 1000, read-only +rootfs, dropped capabilities and no-new-privileges. HOME/config/tmp are writable; +no host ports are published. Test-owned root helpers have only CHOWN added and +repair permissions only on fresh volumes, not persisted warm HOME trees. + +OpenCode automatically installs context-mode@1.0.169 and opencode-mem@2.26.0 +under HOME/.cache/opencode/packages//node_modules/, with lifecycle +scripts disabled by its upstream loader. No external npm install is added. +Provider list is empty, permissions deny, MCP empty, capture/extraction, +chat/compaction injection and profile features disabled. Context storage lives +under HOME/.local/share/context-mode. API authentication is a public synthetic +fixture, not a credential. Cold egress is needed for public packages/models; +this is not an egress allowlist or a malicious-dependency sandbox. + +Readiness requires exact configured plugins and native tool registration, not +context tool dispatch. B must create one synthetic memory through the real +localhost API and recall identical id/content with finite similarity >=0.6. +HTTP 200 with success=false fails. It then stops/removes the application, +discards config/tmp, seeds a fresh config volume, and starts the same stock image +with retained HOME/deps and network=none. The same memory must be recalled. +No model inference request to an LLM, custom plugin bridge, or remote provider is +used; the memory path does execute the default local CPU embedding model. + +## Evidence and Limits + +CI reports exact installed apk name/version pairs via +`apk list --installed --manifest`, skipped package script names when the apk +database exposes a script archive, provisioning/bootstrap/write/recall timings, +OpenCode and Node PT_INTERP, gcompat dynamic __vsnprintf_chk export, ONNX +DT_NEEDED, actual PID 1 gcompat mapping, security and Node SQLite FTS5 checks, +and detected absolute/broken dependency symlinks. No symlink, interpreter or +resource-path relocation is performed. A loader stub named by DT_NEEDED may not +be a loadable ELF library; presence or symbol export is not compatibility proof. +First-write latency includes remaining background model warmup, not just compute. +No warm success/timing is claimed when cold write fails. + +`--no-scripts` is deliberate fresh binary/library extraction, not a complete +bootable alternate root. Official 3.24 gcompat has no install hooks; Node's +post-upgrade script only warns about obsolete npm. Runtime dependencies may have +additional hooks; extraction success is not proof those are unnecessary. Actual +Node/native API execution is the gate; missing setup ends the experiment rather +than adding trust bypasses, chroot escapes, or undocumented relocation fixes. +See [apk options](https://github.com/alpinelinux/apk-tools/blob/master/doc/apk.8.scd), +[gcompat](https://github.com/alpinelinux/aports/blob/3.24-stable/main/gcompat/APKBUILD), +and [Node hook](https://github.com/alpinelinux/aports/blob/3.24-stable/main/nodejs/nodejs.post-upgrade). + +Package revisions/transitive npm ranges and model revision are not frozen. +Cold dependency resolution is not hermetic. Offline replacement tests reuse, +not cache immutability, Kubernetes storage, ARM64, concurrency, DR, context host +dispatch, real conversation learning, or production safety. No raw maps, auth +files, response bodies or log dumps are printed; only fixed diagnostic markers, +symbol names and bounded sanitized HTTP error fields are emitted. + +API/helper provenance: charts PR 113 at a34d8ef1646470c5d24275660c72d314f63b617b; +images PR 58 at 4aa256927c02577f253b2cbb8a09652c021e99ff. Prior run 36366947984 +failed first write on __vsnprintf_chk; it is not a passing baseline. CI is the +only validation environment. Parent reviews/opens the PR; no dispatch/rerun, +merge, publication, chart bump, GitOps rollout or production changes are implied. diff --git a/.github/tests/opencode-init-runtime/http.mjs b/.github/tests/opencode-init-runtime/http.mjs new file mode 100644 index 0000000..cf31675 --- /dev/null +++ b/.github/tests/opencode-init-runtime/http.mjs @@ -0,0 +1,55 @@ +// Adapted from images@4aa2569 tests/http.mjs. Never stream response bodies. +import fs from "node:fs"; + +function sanitize(value) { + return String(value).slice(0, 8192) + .replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "") + .replace(/\b(?:https?|ftp|file):\/\/[^\s<>"']+/gi, "[url]") + .replace(/\b(?:Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+/gi, "[credential]") + .replace(/\b(?:password|passwd|token|api[_-]?key|authorization|secret|cookie)["']?\s*[:=]\s*(?:"[^"]*"|'[^']*'|[^\s,;]+)/gi, "[credential]") + .replace(/\b(?:sk-|ghp_|github_pat_|hf_)[A-Za-z0-9_-]+/g, "[credential]") + .replace(/\b[^\s:@/]+:[^\s@/]+@[^\s/]+/g, "[credential]") + .replace(/\?[^\s<>"']+/g, "[query]") + .replace(/(?:[A-Za-z]:[\\/]|~\/|\.\.?\/|\/)[^\s<>"'()]+/g, "[path]") + .replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").slice(0, 768); +} + +function diagnostic(error, depth = 0) { + if (depth > 3 || error == null) return []; + if (typeof error === "string") return [{ message: sanitize(error) }]; + if (typeof error !== "object") return []; + const item = {}; + for (const field of ["name", "message", "code", "status"]) + if (["string", "number"].includes(typeof error[field])) item[field] = sanitize(error[field]); + return [item, ...diagnostic(error.error, depth + 1), ...diagnostic(error.cause, depth + 1)].slice(0, 6); +} + +let status = null; +try { + const input = JSON.parse(fs.readFileSync(0, "utf8")); + const route = input.route.split("?")[0]; + const allowed = input.port === 4096 ? ["/config", "/experimental/tool/ids"] + : input.port === 4747 ? ["/api/health", "/api/memories", "/api/search"] : []; + if (!allowed.includes(route)) throw new Error("unsupported fixture route"); + const response = await fetch(`http://127.0.0.1:${input.port}${input.route}`, { + method: input.post === null ? "GET" : "POST", headers: input.headers, + body: input.post === null ? undefined : JSON.stringify(input.post), + redirect: "error", signal: AbortSignal.timeout(input.timeout * 1000), + }); + status = response.status; + const chunks = []; + let bytes = 0; + for await (const chunk of response.body) { + bytes += chunk.length; + if (bytes > 1024 * 1024) throw new Error("response exceeds 1 MiB"); + chunks.push(chunk); + } + let body; + try { body = JSON.parse(Buffer.concat(chunks).toString("utf8")); } + catch { throw new Error("non-JSON response"); } + const failed = !response.ok || body?.success === false; + console.log(JSON.stringify({ status, body: failed ? null : body, + error: failed ? diagnostic(input.port === 4747 ? body : "request rejected") : null })); +} catch (error) { + console.log(JSON.stringify({ status, body: null, error: diagnostic(error) })); +} diff --git a/.github/tests/opencode-init-runtime/prepare.sh b/.github/tests/opencode-init-runtime/prepare.sh new file mode 100644 index 0000000..76b1d9c --- /dev/null +++ b/.github/tests/opencode-init-runtime/prepare.sh @@ -0,0 +1,26 @@ +#!/bin/sh +set -eu +test "$(cat /etc/alpine-release)" = 3.24.1 +test "$(apk --print-arch)" = x86_64 +count=0 +while IFS= read -r repo || [ -n "$repo" ]; do + case "$repo" in + ''|'#'*) continue ;; + https://dl-cdn.alpinelinux.org/alpine/v3.24/main|https://dl-cdn.alpinelinux.org/alpine/v3.24/community) ;; + *) printf '%s\n' 'unexpected repository; refusing substitution' >&2; exit 1 ;; + esac + count=$((count + 1)) +done < /etc/apk/repositories +test "$count" -gt 0 +for root in /opt/runtime /opt/inspect; do + mkdir -p "$root/etc/apk/keys" + cp /etc/apk/repositories "$root/etc/apk/repositories" + cp /etc/apk/keys/*.pub "$root/etc/apk/keys/" +done +# Fresh binary/library extraction, not a bootable root. Never run hooks outside +# their root; never rewrite absolute symlinks, interpreters, or resource paths. +apk --root /opt/runtime --initdb --no-cache --no-scripts add 'nodejs>=22.13' gcompat libgcc libstdc++ +apk --root /opt/inspect --initdb --no-cache --no-scripts add binutils +test -f /opt/runtime/lib/libgcompat.so.0 +test -x /opt/runtime/usr/bin/node +test -x /opt/inspect/usr/bin/readelf diff --git a/.github/tests/opencode-init-runtime/probe.mjs b/.github/tests/opencode-init-runtime/probe.mjs new file mode 100644 index 0000000..5615846 --- /dev/null +++ b/.github/tests/opencode-init-runtime/probe.mjs @@ -0,0 +1,57 @@ +import fs from "node:fs"; +import path from "node:path"; +import assert from "node:assert/strict"; +import { DatabaseSync } from "node:sqlite"; + +const home = process.env.HOME; +const status = fs.readFileSync("/proc/1/status", "utf8"); +assert.equal(process.getuid(), 1000); +assert.equal(process.getgid(), 1000); +for (const key of ["Uid", "Gid"]) + assert.match(status, new RegExp(`^${key}:\\s+1000\\s+1000\\s+1000\\s+1000$`, "m")); +assert.match(status, /^CapEff:\s+0+$/m); +assert.match(status, /^NoNewPrivs:\s+1$/m); +const mounts = fs.readFileSync("/proc/1/mountinfo", "utf8").split("\n").map(line => line.split(" ")); +for (const target of ["/", "/opt/runtime"]) + assert(mounts.some(fields => fields[4] === target && fields[5].split(",").includes("ro"))); +assert.throws(() => fs.writeFileSync("/rootfs-write-must-fail", "test")); +assert.throws(() => fs.writeFileSync("/opt/runtime/write-must-fail", "test")); +for (const dir of [home, `${home}/.config/opencode`, `${home}/.cache`, + `${home}/.local/share/context-mode`, `${home}/.local/state`, "/tmp"]) { + fs.mkdirSync(dir, { recursive: true }); + const file = path.join(dir, "init-runtime-write-test"); + fs.writeFileSync(file, "test"); + assert.equal(fs.readFileSync(file, "utf8"), "test"); + fs.unlinkSync(file); +} +const [major, minor] = process.versions.node.split(".").map(Number); +assert(major > 22 || (major === 22 && minor >= 13)); +const db = new DatabaseSync(":memory:"); +db.exec("CREATE VIRTUAL TABLE evidence USING fts5(content)"); +db.close(); +const mapped = fs.readFileSync("/proc/1/maps", "utf8").includes("/opt/runtime/lib/libgcompat.so.0"); +if (process.argv[2] === "B") assert(mapped, "candidate gcompat absent from host mappings"); +const packages = {}; +for (const [name, version] of [["context-mode", "1.0.169"], ["opencode-mem", "2.26.0"]]) { + const manifest = JSON.parse(fs.readFileSync( + `${home}/.cache/opencode/packages/${name}@${version}/node_modules/${name}/package.json`, "utf8")); + assert.equal(manifest.version, version); + packages[name] = version; +} +// Report relocation hazards without repairing them or reading arbitrary targets. +const links = []; +function walk(dir) { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const file = path.join(dir, entry.name); + if (entry.isDirectory()) walk(file); + else if (entry.isSymbolicLink()) { + const target = fs.readlinkSync(file); + if (target.startsWith("/") || !fs.existsSync(file)) + links.push({ name: path.relative("/opt/runtime", file), absolute: target.startsWith("/"), broken: !fs.existsSync(file) }); + } + } +} +walk("/opt/runtime"); +console.log(JSON.stringify({ security: true, node: process.versions.node, sqlite_fts5: true, + host_gcompat_mapped: mapped, packages, relocation_hazards: links.slice(0, 30), + relocation_hazard_count: links.length, context_host_dispatch: "not tested; registration only" })); diff --git a/.github/tests/opencode-init-runtime/runtime.py b/.github/tests/opencode-init-runtime/runtime.py new file mode 100644 index 0000000..406c5ce --- /dev/null +++ b/.github/tests/opencode-init-runtime/runtime.py @@ -0,0 +1,325 @@ +#!/usr/bin/env python3 +"""CI-only stock-image init-volume feasibility; candidate B is the sole runtime gate.""" + +import hashlib +import json +import math +import os +from pathlib import Path +import re +import signal +import subprocess +import sys +import time +from urllib.parse import quote +import uuid + +IMAGE = "ghcr.io/anomalyco/opencode:1.18.29@sha256:ecc3bf96ee55dad226d9cde50d79aaa8a1215c47860c0fcdc71570461bf438b8" +HOME = "/home/opencode" +CONFIG = HOME + "/.config/opencode" +PLUGINS = ["context-mode@1.0.169", "opencode-mem@2.26.0"] +TOOLS = {"memory", "ctx_execute", "ctx_batch_execute", "ctx_index", "ctx_search", "ctx_stats"} +TOKEN = "ci-fixture-not-a-secret" +SENTENCE = "The synthetic runtime probe stores amber-harbor-7f31d2 evidence." +TAG = "opencode_project_" + hashlib.sha256(("path:" + HOME).encode()).hexdigest()[:16] +PREFIX = "opencode-init-" + uuid.uuid4().hex +FIXTURES = str(Path(__file__).resolve().parent) +VOLUMES, CONTAINERS = [], [] +DEADLINE = time.monotonic() + 600 +REPORT = {"arms": {}, "context_host_dispatch": "not tested; registration only"} +HARDEN = ["--read-only", "--cap-drop=ALL", "--security-opt=no-new-privileges", + "--tmpfs=/tmp:rw,size=512m,mode=1777"] +ENV = ["-e", "HOME=" + HOME, "-e", "XDG_CONFIG_HOME=" + HOME + "/.config", + "-e", "XDG_CACHE_HOME=" + HOME + "/.cache", "-e", "XDG_DATA_HOME=" + HOME + "/.local/share", + "-e", "XDG_STATE_HOME=" + HOME + "/.local/state", + "-e", "CONTEXT_MODE_DIR=" + HOME + "/.local/share/context-mode", + "-e", "PATH=/opt/runtime/usr/bin:/opt/runtime/bin:/usr/local/bin:/usr/bin:/bin", + "-e", "LD_LIBRARY_PATH=/opt/runtime/lib:/opt/runtime/usr/lib"] +OPENCODE = {"plugin": PLUGINS, "enabled_providers": [], "permission": {"*": "deny"}, "mcp": {}} +MEMORY = { + "storagePath": HOME + "/.opencode-mem/data", "embeddingModel": "Xenova/nomic-embed-text-v1", + "embeddingDimensions": 768, "embeddingUseTaskPrefixes": True, + "autoCaptureEnabled": False, "autoCleanupEnabled": False, "injectProfile": False, + "userProfileAutoCleanupEnabled": False, "userProfileValidationEnabled": False, + "chatMessage": {"enabled": False}, "compaction": {"enabled": False}, + "containerTagPrefix": "opencode", "webServerEnabled": True, "webServerHost": "127.0.0.1", + "webServerPort": 4747, "webServerApiToken": TOKEN, "similarityThreshold": 0.6, +} + + +class Failure(Exception): + pass + + +def native_evidence(text): + # Fixed phrases plus symbol identifiers only, never raw logs/argv/environment. + text = text[:65536] + phrases = [p for p in ["error loading shared library", "undefined symbol", "symbol not found", + "invalid ELF", "Exec format error", "Permission denied", "Read-only file system", + "certificate", "ENOTFOUND", "ECONNREFUSED", "timed out"] if p.lower() in text.lower()] + symbols = re.findall(r"(?:undefined symbol[: ]+|Error relocating [^\n:]+: )([A-Za-z_][A-Za-z0-9_]{0,100})", text) + return {"markers": phrases, "unresolved_symbols": sorted(set(symbols))[:12]} + + +def run(args, timeout=60, check=True, cleanup=False, data=None): + budget = timeout if cleanup else min(timeout, DEADLINE - time.monotonic()) + if budget <= 0: + raise Failure("phase deadline exhausted") + try: + result = subprocess.run(["docker", *args], input=data, text=True, capture_output=True, timeout=budget) + except (subprocess.TimeoutExpired, OSError): + raise Failure("container command timeout or unavailable") from None + if check and result.returncode: + raise Failure("docker " + args[0] + " failed rc=" + str(result.returncode) + " " + + json.dumps(native_evidence(result.stderr + result.stdout))) + return result + + +def volume(suffix): + name = PREFIX + "-" + suffix + VOLUMES.append(name) + run(["volume", "create", name]) + return name + + +def helper(args, command, timeout=120, data=None, cleanup=False): + name = PREFIX + "-helper-" + str(len(CONTAINERS)) + CONTAINERS.append(name) + return run(["run", "--rm", "-i", "--name", name, "--pull=never", "--platform=linux/amd64", + *HARDEN, *args, "--entrypoint=/bin/sh", IMAGE, "-ec", command], + timeout=timeout, data=data, cleanup=cleanup) + + +def request(name, port, route, post=None, timeout=30): + headers = {"x-opencode-directory": HOME} + if port == 4747: + headers["Authorization"] = "Bearer " + TOKEN + if post is not None: + headers["Content-Type"] = "application/json" + result = run(["exec", "-i", name, "node", "/probe/http.mjs"], timeout=timeout + 10, + data=json.dumps({"port": port, "route": route, "post": post, + "headers": headers, "timeout": timeout})) + envelope = json.loads(result.stdout) + CURRENT["last_request"] = {"route": route.split("?")[0], "status": envelope["status"], + "error": envelope["error"]} + if envelope["error"] is not None or not isinstance(envelope["status"], int) or not 200 <= envelope["status"] < 300: + return None + return envelope["body"] + + +def wait(name, port, route, accept, window=300): + end = min(DEADLINE, time.monotonic() + window) + while time.monotonic() < end: + payload = request(name, port, route, timeout=max(1, min(25, int(end - time.monotonic())))) + if payload is not None and accept(payload): + return payload + if run(["inspect", "--format", "{{.State.Status}}", name]).stdout.strip() != "running": + raise Failure("application exited before readiness") + time.sleep(2) + raise Failure("readiness deadline exhausted") + + +def elf(runtime, inspect, home=None, cleanup=False): + mounts = ["--network=none", "--user=1000:1000", "-v", runtime + ":/opt/runtime:ro", + "-v", inspect + ":/opt/inspect:ro", "-e", "LD_LIBRARY_PATH=/opt/inspect/lib:/opt/inspect/usr/lib"] + if home is None: + command = ("/opt/inspect/usr/bin/readelf -l /usr/local/bin/opencode; printf 'NODE_ELF\n'; " + "/opt/inspect/usr/bin/readelf -l /opt/runtime/usr/bin/node; " + "/opt/inspect/usr/bin/readelf --dyn-syms --wide /opt/runtime/lib/libgcompat.so.0") + else: + mounts += ["-v", home + ":" + HOME + ":ro"] + command = ("find " + HOME + "/.cache/opencode/packages/opencode-mem@2.26.0/node_modules " + "-type f \\( -name 'libonnxruntime.so*' -o -name 'onnxruntime_binding.node' \\) " + "-exec /opt/inspect/usr/bin/readelf -d '{}' ';'") + output = helper(mounts, command, cleanup=cleanup).stdout + if home is not None: + return {"needed": sorted(set(re.findall(r"\(NEEDED\).*?\[(.*?)\]", output))), + "dynamic_sections": output.count("Dynamic section at offset")} + interpreters = [re.findall(r"Requesting program interpreter: ([^\]]+)", part) + for part in output.split("NODE_ELF\n", 1)] + exported = any("__vsnprintf_chk" in line and " UND " not in line and "GLOBAL" in line for line in output.splitlines()) + if len(interpreters) != 2 or not exported: + raise Failure("ELF inspection or gcompat dynamic symbol evidence absent") + return {"opencode_interpreter": interpreters[0] or "no PT_INTERP", + "node_interpreter": interpreters[1] or "no PT_INTERP", + "gcompat_exports___vsnprintf_chk": exported} + + +def prepare_home(home, config, cold): + mounts = ["-v", home + ":" + HOME, "-v", config + ":" + CONFIG] + helper(["--network=none", "--user=0:0", "--cap-add=CHOWN", *mounts], + "chown -R 1000:1000 " + (HOME if cold else CONFIG)) + seed = "mkdir -p " + HOME + "/.cache " + HOME + "/.local/share/context-mode " + HOME + "/.local/state; " + # stdin contains only synthetic config; no external installer or package scripts. + seed += "IFS= read -r config; printf '%s\\n' \"$config\" > " + CONFIG + "/opencode.json; " + seed += "IFS= read -r memory; printf '%s\\n' \"$memory\" > " + CONFIG + "/opencode-mem.jsonc" + helper(["--network=none", "--user=1000:1000", *mounts], seed, + data=json.dumps(OPENCODE) + "\n" + json.dumps(MEMORY) + "\n") + return mounts + + +def ready(name, arm): + def config_ok(value): + if not isinstance(value, dict): + return False + specs = [p[0] if isinstance(p, list) and p else p for p in value.get("plugin", [])] + return all(p in specs for p in PLUGINS) and value.get("enabled_providers") == [] and not value.get("mcp") + wait(name, 4096, "/config", config_ok, window=420) + ids = wait(name, 4096, "/experimental/tool/ids", lambda v: isinstance(v, list) and TOOLS.issubset(set(v))) + if any(ids.count(tool) != 1 for tool in TOOLS): + raise Failure("duplicate tool registration") + wait(name, 4747, "/api/health", lambda v: isinstance(v, dict) and v.get("success") is True and v.get("status") == "ok") + return json.loads(run(["exec", name, "node", "/probe/probe.mjs", arm]).stdout) + + +def recall(name, memory_id): + payload = request(name, 4747, "/api/search?q=" + quote(SENTENCE) + "&tag=" + TAG + "&pageSize=20", timeout=180) + if not isinstance(payload, dict) or payload.get("success") is not True: + raise Failure("real embedding recall failed") + hits = [item for item in (payload.get("data") or {}).get("items", []) + if item.get("type") == "memory" and item.get("id") == memory_id and item.get("content") == SENTENCE] + similarity = hits[0].get("similarity") if hits else None + if isinstance(similarity, bool) or not isinstance(similarity, (int, float)) or not math.isfinite(similarity) or similarity < 0.6: + raise Failure("same id/content/finite similarity >=0.6 not recalled") + return similarity + + +def arm_test(arm, runtime, inspect): + global DEADLINE, CURRENT + DEADLINE = time.monotonic() + 15 * 60 + CURRENT = {"status": "FAIL", "seconds": {}, "phase": "fresh HOME"} + REPORT["arms"][arm] = CURRENT + names = [] + home = None + try: + home = volume(arm + "-home") + memory_id = None + for lifecycle in ["cold", "warm"]: + CURRENT["phase"] = lifecycle + " config preparation" + config = volume(arm + "-config-" + lifecycle) + mounts = prepare_home(home, config, lifecycle == "cold") + name = PREFIX + "-" + arm + "-" + lifecycle + names.append(name) + CONTAINERS.append(name) + CURRENT["phase"] = lifecycle + " bootstrap and host registration" + start = time.monotonic() + preload = ["-e", "LD_PRELOAD=" + ("/opt/runtime/lib/libgcompat.so.0" if arm == "B" else "")] + run(["run", "-d", "--name", name, "--pull=never", "--platform=linux/amd64", + "--network=" + ("bridge" if lifecycle == "cold" else "none"), "--user=1000:1000", + *HARDEN, *ENV, *preload, *mounts, "-v", runtime + ":/opt/runtime:ro", + "-v", FIXTURES + ":/probe:ro", "-w", HOME, + IMAGE, "web", "--hostname", "127.0.0.1", "--port", "4096"]) + CURRENT[lifecycle + "_probe"] = ready(name, arm) + CURRENT["seconds"][lifecycle + "_bootstrap"] = round(time.monotonic() - start, 3) + CURRENT["onnx_elf"] = elf(runtime, inspect, home) + if not CURRENT["onnx_elf"]["needed"]: + raise Failure("ONNX DT_NEEDED evidence absent") + if lifecycle == "cold": + CURRENT["phase"] = "first real embedding write" + first = time.monotonic() + payload = request(name, 4747, "/api/memories", {"content": SENTENCE, "containerTag": TAG}, timeout=300) + CURRENT["seconds"]["first_write_attempt"] = round(time.monotonic() - first, 3) + if not isinstance(payload, dict) or payload.get("success") is not True: + raise Failure("first real embedding write failed; no fallback") + memory_id = (payload.get("data") or {}).get("id") + if not isinstance(memory_id, str) or not memory_id: + raise Failure("memory write returned no id") + CURRENT["seconds"]["first_write"] = round(time.monotonic() - first, 3) + CURRENT["seconds"]["cold_to_write"] = round(time.monotonic() - start, 3) + CURRENT["phase"] = lifecycle + " real embedding recall" + first = time.monotonic() + CURRENT[lifecycle + "_similarity"] = recall(name, memory_id) + CURRENT["seconds"][lifecycle + "_recall"] = round(time.monotonic() - first, 3) + CURRENT["same_memory_id"] = memory_id + run(["stop", "--time=20", name], timeout=45) + run(["rm", name]) + run(["volume", "rm", config]) + VOLUMES.remove(config) + CURRENT["status"] = "PASS" + except Exception as error: + CURRENT["failure"] = str(error) if isinstance(error, Failure) else type(error).__name__ + finally: + for name in names: + try: + logs = run(["logs", "--tail=80", name], check=False, cleanup=True, timeout=10) + CURRENT.setdefault("native_diagnostics", []).append(native_evidence(logs.stdout + logs.stderr)) + run(["rm", "-f", name], check=False, cleanup=True, timeout=20) + except Failure: + CURRENT["diagnostics_incomplete"] = True + if home and "onnx_elf" not in CURRENT: + try: + CURRENT["onnx_elf"] = elf(runtime, inspect, home, cleanup=True) + except Failure: + CURRENT["onnx_elf_unavailable"] = True + + +def main(): + global DEADLINE + try: + run(["pull", "--platform=linux/amd64", IMAGE], timeout=300) + identity = run(["image", "inspect", "--format", + "{{json .RepoDigests}}|{{.Architecture}}|{{.Os}}|{{json .Config.Entrypoint}}", IMAGE]).stdout.strip().split("|") + if len(identity) != 4 or identity[1:3] != ["amd64", "linux"] or json.loads(identity[3]) != ["opencode"]: + raise Failure("stock image identity or entrypoint mismatch") + if not any(item.endswith("@" + IMAGE.split("@")[1]) for item in json.loads(identity[0])): + raise Failure("stock image digest mismatch") + REPORT["image"] = IMAGE + runtime, inspect = volume("runtime"), volume("inspect") + started = time.monotonic() + helper(["--network=bridge", "--user=0:0", "--cap-add=CHOWN", + "-v", runtime + ":/opt/runtime", "-v", inspect + ":/opt/inspect", + "-v", FIXTURES + ":/probe:ro"], "sh /probe/prepare.sh", timeout=300) + REPORT["provision_seconds"] = round(time.monotonic() - started, 3) + for label, root in [("runtime", runtime), ("inspect", inspect)]: + output = helper(["--network=none", "--user=1000:1000", "-v", root + ":/opt/deps:ro"], + "apk --root /opt/deps list --installed --manifest").stdout + packages = [line.split() for line in output.splitlines() if line.strip()] + if not packages or any(len(p) != 2 or not all(re.fullmatch(r'[A-Za-z0-9_.+~-]+', s) for s in p) for p in packages): + raise Failure("installed package manifest malformed") + REPORT[label + "_packages"] = packages + scripts = helper(["--network=none", "--user=1000:1000", "-v", root + ":/opt/deps:ro"], + "for f in /opt/deps/lib/apk/db/scripts.tar /opt/deps/lib/apk/db/scripts.tar.gz; " + "do if test -f \"$f\"; then tar -tf \"$f\"; fi; done").stdout.splitlines() + if any(not re.fullmatch(r'[A-Za-z0-9_.+~/-]+', item) for item in scripts): + raise Failure("unexpected package script inventory") + REPORT[label + "_skipped_scripts"] = scripts[:100] + REPORT[label + "_skipped_script_count"] = len(scripts) + REPORT["elf"] = elf(runtime, inspect) + # Independent budgets; the expected control failure cannot starve/skip B. + arm_test("A", runtime, inspect) + arm_test("B", runtime, inspect) + except Exception as error: + REPORT["setup_failure"] = str(error) if isinstance(error, Failure) else type(error).__name__ + finally: + cleanup_ok = True + for kind, names in [("container", CONTAINERS), ("volume", VOLUMES)]: + for name in reversed(names): + try: + result = run([kind, "inspect", name], check=False, cleanup=True, timeout=10) + if result.returncode: + # Distinguish absence from daemon failure without dumping inspect data. + missing = "no such" in result.stderr.lower() + cleanup_ok = cleanup_ok and missing + continue + run([kind, "rm", "-f", name], cleanup=True, timeout=20) + except Failure: + cleanup_ok = False + REPORT["cleanup_ok"] = cleanup_ok + passed = cleanup_ok and "setup_failure" not in REPORT and REPORT["arms"].get("B", {}).get("status") == "PASS" + REPORT["status"] = "PASS" if passed else "FAIL" + output = json.dumps(REPORT, indent=2) + print(output, flush=True) + if os.environ.get("GITHUB_STEP_SUMMARY"): + with open(os.environ["GITHUB_STEP_SUMMARY"], "a", encoding="utf8") as summary: + summary.write("## Init runtime feasibility\n\n```json\n" + output + "\n```\n") + return 0 if passed else 1 + + +def interrupted(_signum, _frame): + raise Failure("interrupted") + + +if __name__ == "__main__": + signal.signal(signal.SIGTERM, interrupted) + signal.signal(signal.SIGINT, interrupted) + sys.exit(main()) diff --git a/.github/workflows/opencode-init-runtime.yml b/.github/workflows/opencode-init-runtime.yml new file mode 100644 index 0000000..3c934ed --- /dev/null +++ b/.github/workflows/opencode-init-runtime.yml @@ -0,0 +1,22 @@ +name: opencode-init-runtime + +on: + pull_request: + paths: + - .github/workflows/opencode-init-runtime.yml + - .github/tests/opencode-init-runtime/** + +permissions: + contents: read + +jobs: + init-runtime-feasibility: + runs-on: ubuntu-latest + timeout-minutes: 50 + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Test control A and required candidate B + run: python3 .github/tests/opencode-init-runtime/runtime.py From 4c62f2a77f781972b6f2551a53c78766886ee87c Mon Sep 17 00:00:00 2001 From: Steven Welch Date: Sun, 27 Sep 2026 20:23:31 -0600 Subject: [PATCH 2/7] test(opencode): tighten feasibility evidence and read-only inspection --- .github/tests/opencode-init-runtime/README.md | 2 +- .github/tests/opencode-init-runtime/probe.mjs | 1 - .../tests/opencode-init-runtime/runtime.py | 22 +++++++++++++++---- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/.github/tests/opencode-init-runtime/README.md b/.github/tests/opencode-init-runtime/README.md index ca0c29c..5d1c5c8 100644 --- a/.github/tests/opencode-init-runtime/README.md +++ b/.github/tests/opencode-init-runtime/README.md @@ -72,7 +72,7 @@ Cold dependency resolution is not hermetic. Offline replacement tests reuse, not cache immutability, Kubernetes storage, ARM64, concurrency, DR, context host dispatch, real conversation learning, or production safety. No raw maps, auth files, response bodies or log dumps are printed; only fixed diagnostic markers, -symbol names and bounded sanitized HTTP error fields are emitted. +symbol names and bounded sanitized command/HTTP error fields are emitted. API/helper provenance: charts PR 113 at a34d8ef1646470c5d24275660c72d314f63b617b; images PR 58 at 4aa256927c02577f253b2cbb8a09652c021e99ff. Prior run 36366947984 diff --git a/.github/tests/opencode-init-runtime/probe.mjs b/.github/tests/opencode-init-runtime/probe.mjs index 5615846..64f09b8 100644 --- a/.github/tests/opencode-init-runtime/probe.mjs +++ b/.github/tests/opencode-init-runtime/probe.mjs @@ -30,7 +30,6 @@ const db = new DatabaseSync(":memory:"); db.exec("CREATE VIRTUAL TABLE evidence USING fts5(content)"); db.close(); const mapped = fs.readFileSync("/proc/1/maps", "utf8").includes("/opt/runtime/lib/libgcompat.so.0"); -if (process.argv[2] === "B") assert(mapped, "candidate gcompat absent from host mappings"); const packages = {}; for (const [name, version] of [["context-mode", "1.0.169"], ["opencode-mem", "2.26.0"]]) { const manifest = JSON.parse(fs.readFileSync( diff --git a/.github/tests/opencode-init-runtime/runtime.py b/.github/tests/opencode-init-runtime/runtime.py index 406c5ce..d5c29c7 100644 --- a/.github/tests/opencode-init-runtime/runtime.py +++ b/.github/tests/opencode-init-runtime/runtime.py @@ -70,8 +70,18 @@ def run(args, timeout=60, check=True, cleanup=False, data=None): except (subprocess.TimeoutExpired, OSError): raise Failure("container command timeout or unavailable") from None if check and result.returncode: + detail = result.stderr[-8192:] + detail = re.sub(r'\x1b\[[0-?]*[ -/]*[@-~]', '', detail) + detail = re.sub(r'(?i)\b(?:https?|ftp|file)://[^\s<>"\x27]+', '[url]', detail) + detail = re.sub(r'(?i)\b(?:Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+', '[credential]', detail) + detail = re.sub(r'(?i)\b(?:password|passwd|token|api[_-]?key|authorization|secret|cookie)["\x27]?\s*[:=]\s*(?:"[^"]*"|\x27[^\x27]*\x27|[^\s,;]+)', '[credential]', detail) + detail = re.sub(r'\b(?:sk-|ghp_|github_pat_|hf_)[A-Za-z0-9_-]+', '[credential]', detail) + detail = re.sub(r'\b[^\s:@/]+:[^\s@/]+@[^\s/]+', '[credential]', detail) + detail = re.sub(r'\?[^\s<>"\x27]+', '[query]', detail) + detail = re.sub(r'(?:[A-Za-z]:[\\/]|~/|\.\.?/|/)[^\s<>"\x27()]+', '[path]', detail) + detail = re.sub(r'[\x00-\x1f\x7f-\x9f]', ' ', detail)[:768] raise Failure("docker " + args[0] + " failed rc=" + str(result.returncode) + " " - + json.dumps(native_evidence(result.stderr + result.stdout))) + + json.dumps({"error": detail, **native_evidence(result.stderr + result.stdout)})) return result @@ -129,7 +139,7 @@ def elf(runtime, inspect, home=None, cleanup=False): else: mounts += ["-v", home + ":" + HOME + ":ro"] command = ("find " + HOME + "/.cache/opencode/packages/opencode-mem@2.26.0/node_modules " - "-type f \\( -name 'libonnxruntime.so*' -o -name 'onnxruntime_binding.node' \\) " + "-type f -path '*/linux/x64/*' \\( -name 'libonnxruntime.so*' -o -name 'onnxruntime_binding.node' \\) " "-exec /opt/inspect/usr/bin/readelf -d '{}' ';'") output = helper(mounts, command, cleanup=cleanup).stdout if home is not None: @@ -150,6 +160,8 @@ def prepare_home(home, config, cold): helper(["--network=none", "--user=0:0", "--cap-add=CHOWN", *mounts], "chown -R 1000:1000 " + (HOME if cold else CONFIG)) seed = "mkdir -p " + HOME + "/.cache " + HOME + "/.local/share/context-mode " + HOME + "/.local/state; " + if cold: + seed += "test ! -e " + HOME + "/.cache/opencode/packages; test ! -e " + HOME + "/.opencode-mem; " # stdin contains only synthetic config; no external installer or package scripts. seed += "IFS= read -r config; printf '%s\\n' \"$config\" > " + CONFIG + "/opencode.json; " seed += "IFS= read -r memory; printf '%s\\n' \"$memory\" > " + CONFIG + "/opencode-mem.jsonc" @@ -210,6 +222,8 @@ def arm_test(arm, runtime, inspect): "-v", FIXTURES + ":/probe:ro", "-w", HOME, IMAGE, "web", "--hostname", "127.0.0.1", "--port", "4096"]) CURRENT[lifecycle + "_probe"] = ready(name, arm) + if arm == "B" and not CURRENT[lifecycle + "_probe"]["host_gcompat_mapped"]: + raise Failure("candidate gcompat absent from compiled host mappings after plugin load") CURRENT["seconds"][lifecycle + "_bootstrap"] = round(time.monotonic() - start, 3) CURRENT["onnx_elf"] = elf(runtime, inspect, home) if not CURRENT["onnx_elf"]["needed"]: @@ -277,7 +291,7 @@ def main(): if not packages or any(len(p) != 2 or not all(re.fullmatch(r'[A-Za-z0-9_.+~-]+', s) for s in p) for p in packages): raise Failure("installed package manifest malformed") REPORT[label + "_packages"] = packages - scripts = helper(["--network=none", "--user=1000:1000", "-v", root + ":/opt/deps:ro"], + scripts = helper(["--network=none", "--user=0:0", "-v", root + ":/opt/deps:ro"], "for f in /opt/deps/lib/apk/db/scripts.tar /opt/deps/lib/apk/db/scripts.tar.gz; " "do if test -f \"$f\"; then tar -tf \"$f\"; fi; done").stdout.splitlines() if any(not re.fullmatch(r'[A-Za-z0-9_.+~/-]+', item) for item in scripts): @@ -295,7 +309,7 @@ def main(): for kind, names in [("container", CONTAINERS), ("volume", VOLUMES)]: for name in reversed(names): try: - result = run([kind, "inspect", name], check=False, cleanup=True, timeout=10) + result = run([kind, "inspect", "--format", "{{.Name}}", name], check=False, cleanup=True, timeout=10) if result.returncode: # Distinguish absence from daemon failure without dumping inspect data. missing = "no such" in result.stderr.lower() From 4de9c682ceb5963c381f1244e4bdffa8ea19a5e0 Mon Sep 17 00:00:00 2001 From: Steven Welch Date: Sun, 27 Sep 2026 20:34:04 -0600 Subject: [PATCH 3/7] test(opencode): defer native evidence gates until after real recall --- .github/tests/opencode-init-runtime/README.md | 12 +++++++++++- .github/tests/opencode-init-runtime/runtime.py | 17 +++++++++++------ 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/.github/tests/opencode-init-runtime/README.md b/.github/tests/opencode-init-runtime/README.md index 5d1c5c8..53eccf8 100644 --- a/.github/tests/opencode-init-runtime/README.md +++ b/.github/tests/opencode-init-runtime/README.md @@ -14,6 +14,10 @@ using the stock image's trusted public apk keys and strictly matched Alpine dependencies are prepared once. Binutils lives in a separate inspection volume, never the application. Applications mount runtime dependencies read-only. +Image pull/identity checks, dependency provisioning, and setup inspection share +one 10-minute setup budget; provisioning does not receive an additional budget. +Successful pull/identity and provisioning durations are reported separately. + A uses PATH and LD_LIBRARY_PATH only. B adds `LD_PRELOAD=/opt/runtime/lib/libgcompat.so.0`. Both use distinct fresh UUID HOME volumes; application paths are identical. Each has its own 15-minute budget and @@ -41,6 +45,12 @@ localhost API and recall identical id/content with finite similarity >=0.6. HTTP 200 with success=false fails. It then stops/removes the application, discards config/tmp, seeds a fresh config volume, and starts the same stock image with retained HOME/deps and network=none. The same memory must be recalled. +Security prerequisites remain before each real operation. Mapping is observed +before the operation, then checked again after successful recall; B cannot pass +without gcompat mapped in the compiled host. ONNX DT_NEEDED is required only +after successful write/recall, never as a prerequisite to the first write. +Failed operations retain their original failure and best-effort ELF inspection +in cleanup, rather than being preempted by asset timing/layout assumptions. No model inference request to an LLM, custom plugin bridge, or remote provider is used; the memory path does execute the default local CPU embedding model. @@ -77,5 +87,5 @@ symbol names and bounded sanitized command/HTTP error fields are emitted. API/helper provenance: charts PR 113 at a34d8ef1646470c5d24275660c72d314f63b617b; images PR 58 at 4aa256927c02577f253b2cbb8a09652c021e99ff. Prior run 36366947984 failed first write on __vsnprintf_chk; it is not a passing baseline. CI is the -only validation environment. Parent reviews/opens the PR; no dispatch/rerun, +only validation environment. No dispatch/rerun, merge, publication, chart bump, GitOps rollout or production changes are implied. diff --git a/.github/tests/opencode-init-runtime/runtime.py b/.github/tests/opencode-init-runtime/runtime.py index d5c29c7..25a14df 100644 --- a/.github/tests/opencode-init-runtime/runtime.py +++ b/.github/tests/opencode-init-runtime/runtime.py @@ -25,7 +25,7 @@ PREFIX = "opencode-init-" + uuid.uuid4().hex FIXTURES = str(Path(__file__).resolve().parent) VOLUMES, CONTAINERS = [], [] -DEADLINE = time.monotonic() + 600 +DEADLINE = time.monotonic() + 600 # Shared setup budget: pull, identity, provisioning, inspection. REPORT = {"arms": {}, "context_host_dispatch": "not tested; registration only"} HARDEN = ["--read-only", "--cap-drop=ALL", "--security-opt=no-new-privileges", "--tmpfs=/tmp:rw,size=512m,mode=1777"] @@ -222,12 +222,7 @@ def arm_test(arm, runtime, inspect): "-v", FIXTURES + ":/probe:ro", "-w", HOME, IMAGE, "web", "--hostname", "127.0.0.1", "--port", "4096"]) CURRENT[lifecycle + "_probe"] = ready(name, arm) - if arm == "B" and not CURRENT[lifecycle + "_probe"]["host_gcompat_mapped"]: - raise Failure("candidate gcompat absent from compiled host mappings after plugin load") CURRENT["seconds"][lifecycle + "_bootstrap"] = round(time.monotonic() - start, 3) - CURRENT["onnx_elf"] = elf(runtime, inspect, home) - if not CURRENT["onnx_elf"]["needed"]: - raise Failure("ONNX DT_NEEDED evidence absent") if lifecycle == "cold": CURRENT["phase"] = "first real embedding write" first = time.monotonic() @@ -245,6 +240,14 @@ def arm_test(arm, runtime, inspect): CURRENT[lifecycle + "_similarity"] = recall(name, memory_id) CURRENT["seconds"][lifecycle + "_recall"] = round(time.monotonic() - first, 3) CURRENT["same_memory_id"] = memory_id + CURRENT["phase"] = lifecycle + " post-operation inspection" + CURRENT[lifecycle + "_post_recall_probe"] = json.loads( + run(["exec", name, "node", "/probe/probe.mjs", arm]).stdout) + if arm == "B" and not CURRENT[lifecycle + "_post_recall_probe"]["host_gcompat_mapped"]: + raise Failure("candidate gcompat absent from compiled host mappings after real embedding recall") + CURRENT["onnx_elf"] = elf(runtime, inspect, home) + if not CURRENT["onnx_elf"]["needed"]: + raise Failure("ONNX DT_NEEDED evidence absent after real embedding recall") run(["stop", "--time=20", name], timeout=45) run(["rm", name]) run(["volume", "rm", config]) @@ -270,6 +273,7 @@ def arm_test(arm, runtime, inspect): def main(): global DEADLINE try: + started = time.monotonic() run(["pull", "--platform=linux/amd64", IMAGE], timeout=300) identity = run(["image", "inspect", "--format", "{{json .RepoDigests}}|{{.Architecture}}|{{.Os}}|{{json .Config.Entrypoint}}", IMAGE]).stdout.strip().split("|") @@ -278,6 +282,7 @@ def main(): if not any(item.endswith("@" + IMAGE.split("@")[1]) for item in json.loads(identity[0])): raise Failure("stock image digest mismatch") REPORT["image"] = IMAGE + REPORT["pull_identity_seconds"] = round(time.monotonic() - started, 3) runtime, inspect = volume("runtime"), volume("inspect") started = time.monotonic() helper(["--network=bridge", "--user=0:0", "--cap-add=CHOWN", From 73b204be815d851654419e144e8965a6a20f5653 Mon Sep 17 00:00:00 2001 From: Steven Welch Date: Sun, 27 Sep 2026 20:46:39 -0600 Subject: [PATCH 4/7] test(opencode): require exact well-formed configured plugin specs --- .github/tests/opencode-init-runtime/runtime.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/.github/tests/opencode-init-runtime/runtime.py b/.github/tests/opencode-init-runtime/runtime.py index 25a14df..04f63f9 100644 --- a/.github/tests/opencode-init-runtime/runtime.py +++ b/.github/tests/opencode-init-runtime/runtime.py @@ -174,8 +174,21 @@ def ready(name, arm): def config_ok(value): if not isinstance(value, dict): return False - specs = [p[0] if isinstance(p, list) and p else p for p in value.get("plugin", [])] - return all(p in specs for p in PLUGINS) and value.get("enabled_providers") == [] and not value.get("mcp") + plugins = value.get("plugin") + if not isinstance(plugins, list): + raise Failure("malformed configured plugin list") + specs = [] + for plugin in plugins: + if isinstance(plugin, list): + if len(plugin) != 2 or not isinstance(plugin[1], dict): + raise Failure("malformed configured plugin tuple") + plugin = plugin[0] + if not isinstance(plugin, str) or not plugin: + raise Failure("malformed configured plugin spec") + specs.append(plugin) + if sorted(specs) != sorted(PLUGINS): + raise Failure("configured plugin specs do not exactly match fixture") + return value.get("enabled_providers") == [] and not value.get("mcp") wait(name, 4096, "/config", config_ok, window=420) ids = wait(name, 4096, "/experimental/tool/ids", lambda v: isinstance(v, list) and TOOLS.issubset(set(v))) if any(ids.count(tool) != 1 for tool in TOOLS): From 6a125e235966dd632ed0bb35b19182dec90546b1 Mon Sep 17 00:00:00 2001 From: Steven Welch Date: Sun, 27 Sep 2026 20:57:56 -0600 Subject: [PATCH 5/7] test(opencode): observe target state and isolate HTTP client preload --- .github/tests/opencode-init-runtime/README.md | 15 ++++ .../tests/opencode-init-runtime/runtime.py | 78 ++++++++++++++++--- 2 files changed, 81 insertions(+), 12 deletions(-) diff --git a/.github/tests/opencode-init-runtime/README.md b/.github/tests/opencode-init-runtime/README.md index 53eccf8..5cce45e 100644 --- a/.github/tests/opencode-init-runtime/README.md +++ b/.github/tests/opencode-init-runtime/README.md @@ -67,6 +67,21 @@ be a loadable ELF library; presence or symbol export is not compatibility proof. First-write latency includes remaining background model warmup, not just compute. No warm success/timing is claimed when cold write fails. +Before readiness and before cleanup, bounded observations capture only Docker +State Status/Running/ExitCode/OOMKilled and a sanitized Error, PID 1 gcompat mapping +as a boolean, and available cgroup memory peak/current and OOM counters. The +mapping/counter reader uses stock shell builtins, not Node; unavailable samples +are not zero or negative evidence. Exited containers may no longer expose their +cgroup. Exit code 137 alone is not an OOM or native-compatibility diagnosis. + +Only the HTTP measurement client uses `docker exec -e LD_PRELOAD=` so a client +loader failure cannot be confused with the application's response. This creates +a process-local environment override, not a container configuration change; PID 1 +retains the exact candidate B preload. Stock-shell observations and application +security/post-recall probes retain their original environment. This is measurement +isolation, not a compatibility fallback: B still requires actual application +security, mapped gcompat, real embeddings, and offline replacement recall to pass. + `--no-scripts` is deliberate fresh binary/library extraction, not a complete bootable alternate root. Official 3.24 gcompat has no install hooks; Node's post-upgrade script only warns about obsolete npm. Runtime dependencies may have diff --git a/.github/tests/opencode-init-runtime/runtime.py b/.github/tests/opencode-init-runtime/runtime.py index 04f63f9..4a6953a 100644 --- a/.github/tests/opencode-init-runtime/runtime.py +++ b/.github/tests/opencode-init-runtime/runtime.py @@ -61,6 +61,19 @@ def native_evidence(text): return {"markers": phrases, "unresolved_symbols": sorted(set(symbols))[:12]} +def sanitize(text): + detail = text[-8192:] + detail = re.sub(r'\x1b\[[0-?]*[ -/]*[@-~]', '', detail) + detail = re.sub(r'(?i)\b(?:https?|ftp|file)://[^\s<>"\x27]+', '[url]', detail) + detail = re.sub(r'(?i)\b(?:Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+', '[credential]', detail) + detail = re.sub(r'(?i)\b(?:password|passwd|token|api[_-]?key|authorization|secret|cookie)["\x27]?\s*[:=]\s*(?:"[^"]*"|\x27[^\x27]*\x27|[^\s,;]+)', '[credential]', detail) + detail = re.sub(r'\b(?:sk-|ghp_|github_pat_|hf_)[A-Za-z0-9_-]+', '[credential]', detail) + detail = re.sub(r'\b[^\s:@/]+:[^\s@/]+@[^\s/]+', '[credential]', detail) + detail = re.sub(r'\?[^\s<>"\x27]+', '[query]', detail) + detail = re.sub(r'(?:[A-Za-z]:[\\/]|~/|\.\.?/|/)[^\s<>"\x27()]+', '[path]', detail) + return re.sub(r'[\x00-\x1f\x7f-\x9f]', ' ', detail)[:768] + + def run(args, timeout=60, check=True, cleanup=False, data=None): budget = timeout if cleanup else min(timeout, DEADLINE - time.monotonic()) if budget <= 0: @@ -70,21 +83,58 @@ def run(args, timeout=60, check=True, cleanup=False, data=None): except (subprocess.TimeoutExpired, OSError): raise Failure("container command timeout or unavailable") from None if check and result.returncode: - detail = result.stderr[-8192:] - detail = re.sub(r'\x1b\[[0-?]*[ -/]*[@-~]', '', detail) - detail = re.sub(r'(?i)\b(?:https?|ftp|file)://[^\s<>"\x27]+', '[url]', detail) - detail = re.sub(r'(?i)\b(?:Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+', '[credential]', detail) - detail = re.sub(r'(?i)\b(?:password|passwd|token|api[_-]?key|authorization|secret|cookie)["\x27]?\s*[:=]\s*(?:"[^"]*"|\x27[^\x27]*\x27|[^\s,;]+)', '[credential]', detail) - detail = re.sub(r'\b(?:sk-|ghp_|github_pat_|hf_)[A-Za-z0-9_-]+', '[credential]', detail) - detail = re.sub(r'\b[^\s:@/]+:[^\s@/]+@[^\s/]+', '[credential]', detail) - detail = re.sub(r'\?[^\s<>"\x27]+', '[query]', detail) - detail = re.sub(r'(?:[A-Za-z]:[\\/]|~/|\.\.?/|/)[^\s<>"\x27()]+', '[path]', detail) - detail = re.sub(r'[\x00-\x1f\x7f-\x9f]', ' ', detail)[:768] raise Failure("docker " + args[0] + " failed rc=" + str(result.returncode) + " " - + json.dumps({"error": detail, **native_evidence(result.stderr + result.stdout)})) + + json.dumps({"error": sanitize(result.stderr), **native_evidence(result.stderr + result.stdout)})) return result +def observe(name): + evidence = {} + try: + template = '{' + ','.join('"' + key + '":{{json .State.' + key + '}}' + for key in ["Status", "Running", "ExitCode", "OOMKilled", "Error"]) + '}' + result = run(["inspect", "--format", template, name], timeout=15, check=False, cleanup=True) + evidence["inspect_returncode"] = result.returncode + if result.returncode: + return evidence + state = json.loads(result.stdout) + state["Error"] = sanitize(state["Error"]) + evidence["state"] = state + if not state["Running"]: + return evidence + # Stock shell builtins only; no Node, raw maps, environment, or config. + script = '''mapped=unavailable +if test -r /proc/1/maps; then + mapped=false + while IFS= read -r line; do + case "$line" in *"/opt/runtime/lib/libgcompat.so.0"*) mapped=true; break ;; esac + done < /proc/1/maps +fi +printf 'gcompat %s\\n' "$mapped" +for file in /sys/fs/cgroup/memory.peak /sys/fs/cgroup/memory.current /sys/fs/cgroup/memory.events /sys/fs/cgroup/memory.events.local /sys/fs/cgroup/memory/memory.max_usage_in_bytes /sys/fs/cgroup/memory/memory.oom_control; do + test -r "$file" || continue + count=0 + while read -r key value rest; do + count=$((count + 1)) + test "$count" -le 16 || break + printf '%s %s %s\\n' "${file##*/}" "$key" "$value" + done < "$file" +done''' + result = run(["exec", name, "/bin/sh", "-c", script], timeout=15, check=False, cleanup=True) + evidence["shell_returncode"] = result.returncode + for line in result.stdout[:4096].splitlines(): + fields = line.split() + if len(fields) == 2 and fields[0] == "gcompat" and fields[1] in ("true", "false", "unavailable"): + evidence["host_gcompat_mapped"] = {"true": True, "false": False, "unavailable": None}[fields[1]] + elif len(fields) == 2 and fields[0] in ("memory.peak", "memory.current", "memory.max_usage_in_bytes") and fields[1].isdigit(): + evidence.setdefault("cgroup", {})[fields[0]] = int(fields[1]) + elif len(fields) == 3 and fields[0] in ("memory.events", "memory.events.local", "memory.oom_control") and fields[1] in ("oom", "oom_kill", "oom_group_kill", "under_oom") and fields[2].isdigit(): + evidence.setdefault("cgroup", {})[fields[0] + "." + fields[1]] = int(fields[2]) + except (Failure, ValueError, KeyError, TypeError) as error: + evidence["unavailable"] = type(error).__name__ + return evidence + + def volume(suffix): name = PREFIX + "-" + suffix VOLUMES.append(name) @@ -106,7 +156,8 @@ def request(name, port, route, post=None, timeout=30): headers["Authorization"] = "Bearer " + TOKEN if post is not None: headers["Content-Type"] = "application/json" - result = run(["exec", "-i", name, "node", "/probe/http.mjs"], timeout=timeout + 10, + # Per-exec client override does not modify PID 1 or the container's config. + result = run(["exec", "-i", "-e", "LD_PRELOAD=", name, "node", "/probe/http.mjs"], timeout=timeout + 10, data=json.dumps({"port": port, "route": route, "post": post, "headers": headers, "timeout": timeout})) envelope = json.loads(result.stdout) @@ -234,6 +285,7 @@ def arm_test(arm, runtime, inspect): *HARDEN, *ENV, *preload, *mounts, "-v", runtime + ":/opt/runtime:ro", "-v", FIXTURES + ":/probe:ro", "-w", HOME, IMAGE, "web", "--hostname", "127.0.0.1", "--port", "4096"]) + CURRENT[lifecycle + "_before_readiness"] = observe(name) CURRENT[lifecycle + "_probe"] = ready(name, arm) CURRENT["seconds"][lifecycle + "_bootstrap"] = round(time.monotonic() - start, 3) if lifecycle == "cold": @@ -261,6 +313,7 @@ def arm_test(arm, runtime, inspect): CURRENT["onnx_elf"] = elf(runtime, inspect, home) if not CURRENT["onnx_elf"]["needed"]: raise Failure("ONNX DT_NEEDED evidence absent after real embedding recall") + CURRENT[lifecycle + "_before_stop"] = observe(name) run(["stop", "--time=20", name], timeout=45) run(["rm", name]) run(["volume", "rm", config]) @@ -270,6 +323,7 @@ def arm_test(arm, runtime, inspect): CURRENT["failure"] = str(error) if isinstance(error, Failure) else type(error).__name__ finally: for name in names: + CURRENT.setdefault("before_cleanup", []).append(observe(name)) try: logs = run(["logs", "--tail=80", name], check=False, cleanup=True, timeout=10) CURRENT.setdefault("native_diagnostics", []).append(native_evidence(logs.stdout + logs.stderr)) From c2c871b9d7d5e7968418d0b783ffad3447d91c6e Mon Sep 17 00:00:00 2001 From: Steven Welch Date: Sun, 27 Sep 2026 21:14:56 -0600 Subject: [PATCH 6/7] test(opencode): isolate preload startup across plugin configurations --- .github/tests/opencode-init-runtime/README.md | 23 ++++++- .../opencode-init-runtime/diagnostics.py | 63 +++++++++++++++++++ .../tests/opencode-init-runtime/runtime.py | 20 ++++-- 3 files changed, 98 insertions(+), 8 deletions(-) create mode 100644 .github/tests/opencode-init-runtime/diagnostics.py diff --git a/.github/tests/opencode-init-runtime/README.md b/.github/tests/opencode-init-runtime/README.md index 5cce45e..05f9c64 100644 --- a/.github/tests/opencode-init-runtime/README.md +++ b/.github/tests/opencode-init-runtime/README.md @@ -82,6 +82,24 @@ security/post-recall probes retain their original environment. This is measureme isolation, not a compatibility fallback: B still requires actual application security, mapped gcompat, real embeddings, and offline replacement recall to pass. +After A/B, three observational preload cases use separate fresh HOME/config +volumes: no configured plugins, context-mode only, and opencode-mem only. Each +has a 120-second budget including setup, with the last 10 seconds reserved for +final observations; bounded cleanup is separate. They request /config and keep +observing process lifetime after readiness to expose background-init failures. +Up to eight samples approximately 15 seconds apart record safe state, gcompat +mapping, and available memory/OOM counters. They do not write memories or call +an LLM. No configured plugins does not disable upstream built-in functionality. +Diagnostic outcomes never replace the original B cold/write/recall/warm gate. +The 50-minute job cap remains; A/B retain their original budgets and timeouts. + +Core dumps are disabled. Last-80-line synthetic logs are filtered to relevant +assertion/panic/crash/illegal-instruction/segmentation/Bun/ONNX/plugin/stack-frame +messages, excluding sensitive-keyword lines, then sanitized. At most 16 lines +of 384 characters are retained per observation. No raw log/config/environment, +core/binary dump, or host kernel log is collected. Missing excerpts are not proof +of no crash; a signal-shaped exit code is not a root-cause diagnosis. + `--no-scripts` is deliberate fresh binary/library extraction, not a complete bootable alternate root. Official 3.24 gcompat has no install hooks; Node's post-upgrade script only warns about obsolete npm. Runtime dependencies may have @@ -96,8 +114,9 @@ Package revisions/transitive npm ranges and model revision are not frozen. Cold dependency resolution is not hermetic. Offline replacement tests reuse, not cache immutability, Kubernetes storage, ARM64, concurrency, DR, context host dispatch, real conversation learning, or production safety. No raw maps, auth -files, response bodies or log dumps are printed; only fixed diagnostic markers, -symbol names and bounded sanitized command/HTTP error fields are emitted. +files, response bodies or raw log dumps are printed; only diagnostic markers, +symbol names, selected sanitized crash lines, and bounded sanitized command/HTTP +error fields are emitted. API/helper provenance: charts PR 113 at a34d8ef1646470c5d24275660c72d314f63b617b; images PR 58 at 4aa256927c02577f253b2cbb8a09652c021e99ff. Prior run 36366947984 diff --git a/.github/tests/opencode-init-runtime/diagnostics.py b/.github/tests/opencode-init-runtime/diagnostics.py new file mode 100644 index 0000000..1cb6614 --- /dev/null +++ b/.github/tests/opencode-init-runtime/diagnostics.py @@ -0,0 +1,63 @@ +"""Short observational cases; never substitute for the required B gate.""" + +import json +import time + + +def run_cases(r, runtime): + for label, plugins in [("no-configured-plugins", []), + ("context-only", [r.PLUGINS[0]]), ("mem-only", [r.PLUGINS[1]])]: + started = time.monotonic() + r.DEADLINE = started + 120 + case = {"plugins": plugins, "config_ready": False, "survived_window": False, "samples": []} + r.REPORT.setdefault("diagnostic_cases", {})[label] = case + r.CURRENT = case + name = r.PREFIX + "-diagnostic-" + label + try: + home = r.volume("diagnostic-" + label + "-home") + config = r.volume("diagnostic-" + label + "-config") + mounts = r.prepare_home(home, config, True, plugins) + r.CONTAINERS.append(name) + r.run(["run", "-d", "--name", name, "--pull=never", "--platform=linux/amd64", + "--network=bridge", "--user=1000:1000", *r.HARDEN, *r.ENV, + "-e", "LD_PRELOAD=/opt/runtime/lib/libgcompat.so.0", *mounts, + "-v", runtime + ":/opt/runtime:ro", "-v", r.FIXTURES + ":/probe:ro", + "-w", r.HOME, r.IMAGE, "web", "--hostname", "127.0.0.1", "--port", "4096"]) + next_sample = 0 + while time.monotonic() < r.DEADLINE - 10: + elapsed = time.monotonic() - started + if elapsed >= next_sample and len(case["samples"]) < 8: + sample = r.observe(name) + sample["elapsed_seconds"] = round(elapsed, 3) + case["samples"].append(sample) + next_sample = elapsed + 15 + if sample.get("state", {}).get("Running") is False: + break + if not case["config_ready"]: + value = r.request(name, 4096, "/config", timeout=5) + if isinstance(value, dict): + specs = value.get("plugin") + if not isinstance(specs, list): + raise r.Failure("diagnostic malformed plugin list") + specs = [p[0] if isinstance(p, list) and len(p) == 2 and isinstance(p[1], dict) else p for p in specs] + if not all(isinstance(p, str) for p in specs) or sorted(specs) != sorted(plugins): + raise r.Failure("diagnostic plugin list mismatch") + if value.get("enabled_providers") != [] or value.get("mcp"): + raise r.Failure("diagnostic provider or MCP config mismatch") + case["config_ready"] = True + case["config_seconds"] = round(time.monotonic() - started, 3) + time.sleep(2) + final = r.observe(name) + case["end_of_window"] = final + case["survived_window"] = time.monotonic() >= r.DEADLINE - 10 and final.get("state", {}).get("Running") is True + except Exception as error: + case["failure"] = str(error) if isinstance(error, r.Failure) else type(error).__name__ + finally: + case["before_cleanup"] = r.observe(name) + case["observed_seconds"] = round(time.monotonic() - started, 3) + try: + logs = r.run(["logs", "--tail=80", name], timeout=10, check=False, cleanup=True) + case["crash_evidence"] = r.native_evidence(logs.stdout + logs.stderr) + r.run(["rm", "-f", name], timeout=20, check=False, cleanup=True) + except r.Failure: + case["diagnostics_incomplete"] = True diff --git a/.github/tests/opencode-init-runtime/runtime.py b/.github/tests/opencode-init-runtime/runtime.py index 4a6953a..c2d329c 100644 --- a/.github/tests/opencode-init-runtime/runtime.py +++ b/.github/tests/opencode-init-runtime/runtime.py @@ -14,6 +14,8 @@ from urllib.parse import quote import uuid +from diagnostics import run_cases + IMAGE = "ghcr.io/anomalyco/opencode:1.18.29@sha256:ecc3bf96ee55dad226d9cde50d79aaa8a1215c47860c0fcdc71570461bf438b8" HOME = "/home/opencode" CONFIG = HOME + "/.config/opencode" @@ -27,7 +29,7 @@ VOLUMES, CONTAINERS = [], [] DEADLINE = time.monotonic() + 600 # Shared setup budget: pull, identity, provisioning, inspection. REPORT = {"arms": {}, "context_host_dispatch": "not tested; registration only"} -HARDEN = ["--read-only", "--cap-drop=ALL", "--security-opt=no-new-privileges", +HARDEN = ["--read-only", "--cap-drop=ALL", "--security-opt=no-new-privileges", "--ulimit=core=0", "--tmpfs=/tmp:rw,size=512m,mode=1777"] ENV = ["-e", "HOME=" + HOME, "-e", "XDG_CONFIG_HOME=" + HOME + "/.config", "-e", "XDG_CACHE_HOME=" + HOME + "/.cache", "-e", "XDG_DATA_HOME=" + HOME + "/.local/share", @@ -52,13 +54,18 @@ class Failure(Exception): def native_evidence(text): - # Fixed phrases plus symbol identifiers only, never raw logs/argv/environment. - text = text[:65536] + # Only bounded, relevant synthetic crash context; never a raw log dump. + text = text[-65536:] phrases = [p for p in ["error loading shared library", "undefined symbol", "symbol not found", "invalid ELF", "Exec format error", "Permission denied", "Read-only file system", "certificate", "ENOTFOUND", "ECONNREFUSED", "timed out"] if p.lower() in text.lower()] symbols = re.findall(r"(?:undefined symbol[: ]+|Error relocating [^\n:]+: )([A-Za-z_][A-Za-z0-9_]{0,100})", text) - return {"markers": phrases, "unresolved_symbols": sorted(set(symbols))[:12]} + relevant = re.compile(r"assert|panic|crash|illegal.?instruction|segmentation|\bbun\b|onnx|plugin|^\s*at ", re.I) + sensitive = re.compile(r"authorization|bearer|token|password|secret|cookie|environment|config(?:uration)?\s*[:=]", re.I) + lines = [sanitize(line)[:384] for line in text.splitlines()[-80:] + if relevant.search(line) and not sensitive.search(line)] + return {"markers": phrases, "unresolved_symbols": sorted(set(symbols))[:12], + "crash_lines": lines[-16:]} def sanitize(text): @@ -206,7 +213,7 @@ def elf(runtime, inspect, home=None, cleanup=False): "gcompat_exports___vsnprintf_chk": exported} -def prepare_home(home, config, cold): +def prepare_home(home, config, cold, plugins=PLUGINS): mounts = ["-v", home + ":" + HOME, "-v", config + ":" + CONFIG] helper(["--network=none", "--user=0:0", "--cap-add=CHOWN", *mounts], "chown -R 1000:1000 " + (HOME if cold else CONFIG)) @@ -217,7 +224,7 @@ def prepare_home(home, config, cold): seed += "IFS= read -r config; printf '%s\\n' \"$config\" > " + CONFIG + "/opencode.json; " seed += "IFS= read -r memory; printf '%s\\n' \"$memory\" > " + CONFIG + "/opencode-mem.jsonc" helper(["--network=none", "--user=1000:1000", *mounts], seed, - data=json.dumps(OPENCODE) + "\n" + json.dumps(MEMORY) + "\n") + data=json.dumps({**OPENCODE, "plugin": plugins}) + "\n" + json.dumps(MEMORY) + "\n") return mounts @@ -374,6 +381,7 @@ def main(): # Independent budgets; the expected control failure cannot starve/skip B. arm_test("A", runtime, inspect) arm_test("B", runtime, inspect) + run_cases(sys.modules[__name__], runtime) except Exception as error: REPORT["setup_failure"] = str(error) if isinstance(error, Failure) else type(error).__name__ finally: From 9015710e18258c7c4c7792bf7fb1380c3f52d8a8 Mon Sep 17 00:00:00 2001 From: Steven Welch Date: Sun, 27 Sep 2026 22:14:13 -0600 Subject: [PATCH 7/7] test(opencode): trace memory loader and isolate resolved native modules --- .github/tests/opencode-init-runtime/README.md | 47 ++++-- .../opencode-init-runtime/diagnostics.py | 158 ++++++++++++------ .../tests/opencode-init-runtime/native.mjs | 127 ++++++++++++++ 3 files changed, 271 insertions(+), 61 deletions(-) create mode 100644 .github/tests/opencode-init-runtime/native.mjs diff --git a/.github/tests/opencode-init-runtime/README.md b/.github/tests/opencode-init-runtime/README.md index 05f9c64..5a005b9 100644 --- a/.github/tests/opencode-init-runtime/README.md +++ b/.github/tests/opencode-init-runtime/README.md @@ -82,16 +82,43 @@ security/post-recall probes retain their original environment. This is measureme isolation, not a compatibility fallback: B still requires actual application security, mapped gcompat, real embeddings, and offline replacement recall to pass. -After A/B, three observational preload cases use separate fresh HOME/config -volumes: no configured plugins, context-mode only, and opencode-mem only. Each -has a 120-second budget including setup, with the last 10 seconds reserved for -final observations; bounded cleanup is separate. They request /config and keep -observing process lifetime after readiness to expose background-init failures. -Up to eight samples approximately 15 seconds apart record safe state, gcompat -mapping, and available memory/OOM counters. They do not write memories or call -an LLM. No configured plugins does not disable upstream built-in functionality. -Diagnostic outcomes never replace the original B cold/write/recall/warm gate. -The 50-minute job cap remains; A/B retain their original budgets and timeouts. +After A/B, a fresh memory-only case adds the official `GLIBC_FAKE_DEBUG=1` +diagnostic setting. It requests /config and samples process state for up to +120 seconds including setup, reserving ten seconds for final observations. +The previously tested no-configured-plugin and context-only cases are not +repeated. At most twenty sanitized gcompat namespace/version lookup messages +are retained. The setting traces lookups; it is not a compatibility fix. + +Four independent Node containers then reuse that diagnostic fixture's public +package/model cache read-only with network=none and the same prepared runtime, +UID1000, rootfs hardening, gcompat preload and debug setting. They separately +exercise libsql file-backed CRUD in fresh /tmp, a one-pixel Sharp PNG only when +Sharp is an actual Transformers dependency, direct ONNX 1.20.1 import/session +creation using an existing selected-model artifact if present, and Transformers +CJS initialization through the plugin's existing ONNX shim followed by local-only +embedding initialization. No packages/models are installed or downloaded by +these Node cases, and no model is generated. Missing artifacts are reported, +not fetched. Session probing accepts only existing nomic model/model_quantized +ONNX files up to 1 GiB. Each case has 45 seconds; diagnostic work shares a +six-minute budget, with bounded cleanup separate. The job cap stays 50 minutes. + +Synchronous before/after markers, resolved package names/versions/relative +entrypoints, selected native-library mappings, exit state, and sanitized errors +identify the failing stage. Node success is NOT compiled-Bun compatibility and +never substitutes for B's actual write/recall/warm gate. A/B use their original +separate HOME volumes, configurations, timeouts, and untouched package/model +caches. Native diagnostic mutations are confined to fresh /tmp, not source HOME. + +Source contracts: opencode-mem 2.26.0 `src/services/embedding.ts`, +`onnxruntime-resolve.ts`, `runtime-require.ts`, and tsconfig map the published +dist paths and resolve-before-shim CJS ordering. Transformers 4.2.0's published +manifest declares Sharp and a nested ONNX version; diagnostics record actual +installed versions and require the plugin's direct ONNX to remain 1.20.1. +ONNX v1.20.1's `InferenceSession.create/release`, libsql-client v0.17.4's local +createClient/execute/close, and Sharp's documented create/png/toBuffer APIs are +used without changing the plugin or native binaries. The existing model remains +Xenova/nomic-embed-text-v1; native probes disable remote models and use +local_files_only, so absent cache files are a diagnostic limitation. Core dumps are disabled. Last-80-line synthetic logs are filtered to relevant assertion/panic/crash/illegal-instruction/segmentation/Bun/ONNX/plugin/stack-frame diff --git a/.github/tests/opencode-init-runtime/diagnostics.py b/.github/tests/opencode-init-runtime/diagnostics.py index 1cb6614..54716bc 100644 --- a/.github/tests/opencode-init-runtime/diagnostics.py +++ b/.github/tests/opencode-init-runtime/diagnostics.py @@ -1,63 +1,119 @@ -"""Short observational cases; never substitute for the required B gate.""" +"""Bounded diagnostic-only cases; never substitute for the required B gate.""" import json +import re import time +def evidence(r, name): + logs = r.run(["logs", "--tail=80", name], timeout=10, check=False, cleanup=True) + text = (logs.stdout + logs.stderr)[-65536:] + result = r.native_evidence(text) + lookups = re.findall(r"symbol [A-Za-z_][A-Za-z0-9_]* with version [A-Za-z0-9_.-]+ is being redirected|loading library [^\r\n]{1,200}? was requested in namespace -?\d+", text) + result["gcompat_lookups"] = [r.sanitize(line)[:256] for line in lookups[-20:]] + result["stages"] = [] + for line in logs.stdout.splitlines(): + if not line.startswith("NATIVE ") or len(result["stages"]) >= 40: + continue + try: + item = json.loads(line[7:]) + # Only fixture-authored metadata, never arbitrary stdout/response data. + clean = {} + for key in ("stage", "mode", "node", "name", "version", "entry", "artifact", "error", "bytes", "inputs", "dimensions", "libraries"): + if key not in item: + continue + value = item[key] + if key in ("entry", "artifact", "libraries"): + values = value if isinstance(value, list) else [value] + if not all(isinstance(v, str) and len(v) <= 512 and re.fullmatch(r"[A-Za-z0-9_@./+-]+", v) and not v.startswith(("/", "..")) for v in values): + continue + clean[key] = value + elif isinstance(value, str): + clean[key] = r.sanitize(value) + elif isinstance(value, int): + clean[key] = value + result["stages"].append(clean) + except (ValueError, TypeError): + result["malformed_marker"] = True + return result + + def run_cases(r, runtime): - for label, plugins in [("no-configured-plugins", []), - ("context-only", [r.PLUGINS[0]]), ("mem-only", [r.PLUGINS[1]])]: - started = time.monotonic() - r.DEADLINE = started + 120 - case = {"plugins": plugins, "config_ready": False, "survived_window": False, "samples": []} - r.REPORT.setdefault("diagnostic_cases", {})[label] = case - r.CURRENT = case - name = r.PREFIX + "-diagnostic-" + label + overall = time.monotonic() + 360 + started = time.monotonic() + r.DEADLINE = started + 120 + case = {"plugins": [r.PLUGINS[1]], "config_ready": False, "samples": []} + r.REPORT["diagnostic_cases"] = {"mem-only-debug": case} + r.CURRENT = case + name = r.PREFIX + "-diagnostic-mem-debug" + home = None + try: + home = r.volume("diagnostic-mem-debug-home") + config = r.volume("diagnostic-mem-debug-config") + mounts = r.prepare_home(home, config, True, [r.PLUGINS[1]]) + r.CONTAINERS.append(name) + r.run(["run", "-d", "--name", name, "--pull=never", "--platform=linux/amd64", + "--network=bridge", "--user=1000:1000", *r.HARDEN, *r.ENV, + "-e", "LD_PRELOAD=/opt/runtime/lib/libgcompat.so.0", "-e", "GLIBC_FAKE_DEBUG=1", *mounts, + "-v", runtime + ":/opt/runtime:ro", "-v", r.FIXTURES + ":/probe:ro", + "-w", r.HOME, r.IMAGE, "web", "--hostname", "127.0.0.1", "--port", "4096"]) + next_sample = 0 + while time.monotonic() < r.DEADLINE - 10: + elapsed = time.monotonic() - started + if elapsed >= next_sample and len(case["samples"]) < 8: + sample = r.observe(name) + sample["elapsed_seconds"] = round(elapsed, 3) + case["samples"].append(sample) + next_sample = elapsed + 15 + if sample.get("state", {}).get("Running") is False: + break + if not case["config_ready"]: + value = r.request(name, 4096, "/config", timeout=5) + if isinstance(value, dict): + specs = value.get("plugin") + if not isinstance(specs, list): + raise r.Failure("diagnostic malformed plugin list") + specs = [p[0] if isinstance(p, list) and len(p) == 2 and isinstance(p[1], dict) else p for p in specs] + if specs != [r.PLUGINS[1]] or value.get("enabled_providers") != [] or value.get("mcp"): + raise r.Failure("diagnostic config mismatch") + case["config_ready"] = True + case["config_seconds"] = round(time.monotonic() - started, 3) + time.sleep(2) + except Exception as error: + case["failure"] = str(error) if isinstance(error, r.Failure) else type(error).__name__ + finally: + case["before_cleanup"] = r.observe(name) + case["observed_seconds"] = round(time.monotonic() - started, 3) + try: + case["evidence"] = evidence(r, name) + r.run(["rm", "-f", name], timeout=20, check=False, cleanup=True) + except r.Failure: + case["diagnostics_incomplete"] = True + if home is None: + return + # Separate Node processes, no network and read-only source HOME/cache. All + # synthetic mutations (including SQLite CRUD) stay in each fresh /tmp. + for mode in ("libsql", "sharp", "onnx", "transformers"): + r.DEADLINE = min(overall, time.monotonic() + 45) + name = r.PREFIX + "-native-" + mode + result = {"runtime": "Node diagnostic, not OpenCode compatibility"} + r.REPORT.setdefault("native_cases", {})[mode] = result + r.CONTAINERS.append(name) try: - home = r.volume("diagnostic-" + label + "-home") - config = r.volume("diagnostic-" + label + "-config") - mounts = r.prepare_home(home, config, True, plugins) - r.CONTAINERS.append(name) r.run(["run", "-d", "--name", name, "--pull=never", "--platform=linux/amd64", - "--network=bridge", "--user=1000:1000", *r.HARDEN, *r.ENV, - "-e", "LD_PRELOAD=/opt/runtime/lib/libgcompat.so.0", *mounts, - "-v", runtime + ":/opt/runtime:ro", "-v", r.FIXTURES + ":/probe:ro", - "-w", r.HOME, r.IMAGE, "web", "--hostname", "127.0.0.1", "--port", "4096"]) - next_sample = 0 - while time.monotonic() < r.DEADLINE - 10: - elapsed = time.monotonic() - started - if elapsed >= next_sample and len(case["samples"]) < 8: - sample = r.observe(name) - sample["elapsed_seconds"] = round(elapsed, 3) - case["samples"].append(sample) - next_sample = elapsed + 15 - if sample.get("state", {}).get("Running") is False: - break - if not case["config_ready"]: - value = r.request(name, 4096, "/config", timeout=5) - if isinstance(value, dict): - specs = value.get("plugin") - if not isinstance(specs, list): - raise r.Failure("diagnostic malformed plugin list") - specs = [p[0] if isinstance(p, list) and len(p) == 2 and isinstance(p[1], dict) else p for p in specs] - if not all(isinstance(p, str) for p in specs) or sorted(specs) != sorted(plugins): - raise r.Failure("diagnostic plugin list mismatch") - if value.get("enabled_providers") != [] or value.get("mcp"): - raise r.Failure("diagnostic provider or MCP config mismatch") - case["config_ready"] = True - case["config_seconds"] = round(time.monotonic() - started, 3) - time.sleep(2) - final = r.observe(name) - case["end_of_window"] = final - case["survived_window"] = time.monotonic() >= r.DEADLINE - 10 and final.get("state", {}).get("Running") is True - except Exception as error: - case["failure"] = str(error) if isinstance(error, r.Failure) else type(error).__name__ + "--network=none", "--user=1000:1000", *r.HARDEN, *r.ENV, + "-e", "LD_PRELOAD=/opt/runtime/lib/libgcompat.so.0", "-e", "GLIBC_FAKE_DEBUG=1", + "-v", home + ":" + r.HOME + ":ro", "-v", runtime + ":/opt/runtime:ro", + "-v", r.FIXTURES + ":/probe:ro", "-w", "/tmp", "--entrypoint=node", + r.IMAGE, "/probe/native.mjs", mode]) + waited = r.run(["wait", name], timeout=45, check=False) + result["wait_returncode"] = waited.returncode + except r.Failure as error: + result["failure"] = str(error) finally: - case["before_cleanup"] = r.observe(name) - case["observed_seconds"] = round(time.monotonic() - started, 3) + result["before_cleanup"] = r.observe(name) try: - logs = r.run(["logs", "--tail=80", name], timeout=10, check=False, cleanup=True) - case["crash_evidence"] = r.native_evidence(logs.stdout + logs.stderr) + result["evidence"] = evidence(r, name) r.run(["rm", "-f", name], timeout=20, check=False, cleanup=True) except r.Failure: - case["diagnostics_incomplete"] = True + result["diagnostics_incomplete"] = True diff --git a/.github/tests/opencode-init-runtime/native.mjs b/.github/tests/opencode-init-runtime/native.mjs new file mode 100644 index 0000000..78720c2 --- /dev/null +++ b/.github/tests/opencode-init-runtime/native.mjs @@ -0,0 +1,127 @@ +// CI-only probes against the existing public plugin cache; no installation. +import fs from "node:fs"; +import path from "node:path"; +import { createRequire } from "node:module"; +import { pathToFileURL } from "node:url"; +import assert from "node:assert/strict"; + +const cache = `${process.env.HOME}/.cache/opencode/packages/opencode-mem@2.26.0`; +const root = `${cache}/node_modules/opencode-mem`; +const requirePlugin = createRequire(`${root}/package.json`); +const model = "Xenova/nomic-embed-text-v1"; +const modelCache = `${process.env.HOME}/.opencode-mem/data/.cache`; +let stage = "start"; +function mark(next, fields = {}) { + stage = next; + fs.writeSync(1, `NATIVE ${JSON.stringify({ stage, ...fields })}\n`); +} +function resolve(request, name) { + const entry = fs.realpathSync(request.resolve(name)); + const relative = path.relative(cache, entry); + assert(!relative.startsWith("..") && !path.isAbsolute(relative), "resolution escaped fixture cache"); + let dir = path.dirname(entry); + for (let i = 0; i < 8 && dir.startsWith(cache + "/"); i++, dir = path.dirname(dir)) { + const manifest = path.join(dir, "package.json"); + if (!fs.existsSync(manifest)) continue; + const data = JSON.parse(fs.readFileSync(manifest, "utf8")); + if (data.name !== name) continue; + mark("resolved", { name, version: data.version, entry: relative }); + return { entry, manifest: data }; + } + throw new Error("resolved package manifest not found"); +} +function mapped() { + const names = fs.readFileSync("/proc/self/maps", "utf8").split("\n") + .map(line => line.trim().split(/\s+/).at(-1)) + .filter(name => name?.startsWith(cache + "/") && /\.(node|so)(\.|$)/.test(name)) + .map(name => path.relative(cache, name)); + mark("native-maps", { libraries: [...new Set(names)].sort().slice(0, 16) }); +} + +try { + assert.equal(JSON.parse(fs.readFileSync(`${root}/package.json`, "utf8")).version, "2.26.0"); + const mode = process.argv[2]; + mark("start", { mode, node: process.versions.node }); + if (mode === "libsql") { + const { entry } = resolve(requirePlugin, "@libsql/client"); + mark("libsql.import.before"); + const { createClient } = requirePlugin(entry); + mark("libsql.import.after"); + mapped(); + mark("libsql.crud.before"); + const db = createClient({ url: "file:/tmp/native-diagnostic.db" }); + try { + await db.execute("CREATE TABLE evidence (id INTEGER PRIMARY KEY, value TEXT)"); + await db.execute({ sql: "INSERT INTO evidence VALUES (?, ?)", args: [1, "synthetic"] }); + assert.equal((await db.execute("SELECT value FROM evidence WHERE id=1")).rows[0].value, "synthetic"); + await db.execute("UPDATE evidence SET value='updated' WHERE id=1"); + assert.equal((await db.execute("SELECT value FROM evidence WHERE id=1")).rows[0].value, "updated"); + await db.execute("DELETE FROM evidence WHERE id=1"); + assert.equal((await db.execute("SELECT * FROM evidence")).rows.length, 0); + } finally { db.close(); } + mark("libsql.crud.after"); + } else if (mode === "sharp") { + const transformers = resolve(requirePlugin, "@huggingface/transformers"); + if (!transformers.manifest.dependencies?.sharp) { + mark("sharp.not-a-dependency"); + } else { + const request = createRequire(transformers.entry); + const { entry } = resolve(request, "sharp"); + mark("sharp.import.before"); + const sharp = request(entry); + mark("sharp.import.after"); + mapped(); + mark("sharp.pixel.before"); + const output = await sharp({ create: { width: 1, height: 1, channels: 3, + background: { r: 1, g: 2, b: 3 } } }).png().toBuffer(); + assert(output.length > 0); + mark("sharp.pixel.after", { bytes: output.length }); + } + } else if (mode === "onnx") { + const { entry, manifest } = resolve(requirePlugin, "onnxruntime-node"); + assert.equal(manifest.version, "1.20.1"); + mark("onnx.import.before"); + const ort = requirePlugin(entry); + mark("onnx.import.after"); + mapped(); + const file = ["model_quantized.onnx", "model.onnx"].map(name => `${modelCache}/${model}/onnx/${name}`) + .find(name => fs.existsSync(name) && fs.statSync(name).size <= 1024 ** 3); + if (!file) mark("onnx.model.unavailable"); + else { + mark("onnx.session.before", { artifact: path.relative(modelCache, file) }); + const session = await ort.InferenceSession.create(file, { executionProviders: ["cpu"] }); + mark("onnx.session.after", { inputs: session.inputNames.length }); + await session.release(); + mark("onnx.release.after"); + } + } else if (mode === "transformers") { + // Match the published plugin's resolve-before-shim, CJS import ordering. + const { entry } = resolve(requirePlugin, "@huggingface/transformers"); + assert.equal(resolve(requirePlugin, "onnxruntime-node").manifest.version, "1.20.1"); + mark("transformers.shim.before"); + const shim = await import(pathToFileURL(`${root}/dist/services/onnxruntime-resolve.js`).href); + shim.prepareOnnxruntimeForTransformers(); + mark("transformers.shim.after"); + mark("transformers.import.before"); + const { pipeline, env } = requirePlugin(entry); + mark("transformers.import.after"); + mapped(); + env.allowLocalModels = true; + env.allowRemoteModels = false; + env.cacheDir = modelCache; + env.backends.onnx.wasm.numThreads = 1; + mark("transformers.pipeline.before"); + const pipe = await pipeline("feature-extraction", model, { local_files_only: true }); + mark("transformers.pipeline.after"); + const result = await pipe("search_document: synthetic native diagnostic", { pooling: "mean", normalize: true }); + assert.equal(result.data.length, 768); + assert([...result.data].every(Number.isFinite)); + mark("transformers.embedding.after", { dimensions: result.data.length }); + await pipe.dispose(); + } else throw new Error("unknown diagnostic case"); + mark("complete"); +} catch (error) { + // Captured by the orchestrator, which sanitizes this before printing. + fs.writeSync(1, `NATIVE ${JSON.stringify({ stage, error: String(error) })}\n`); + process.exitCode = 1; +}