// Isolated kind upgrade drill. Requires two locally loaded image digests with // identical tool contracts but DIFFERENT bundled coworker instructions/identity. // Never uses local.mjs's live namespace or regenerates its credentials. import assert from "node:assert/strict"; import { readFile, writeFile, mkdir } from "node:fs/promises"; import { join, resolve } from "node:path"; import { randomUUID, randomBytes } from "node:crypto"; import { spawn } from "node:child_process"; import { run, kubectl, repo } from "./local.mjs"; const input = JSON.parse(await readFile(resolve(process.argv[2]), "utf8")); const { kubeconfig, context, helm, imageA, imageB, runnerImage, certificates, gateway, } = input; assert.equal(context, "kind-ow-server-20261011"); for (const image of [imageA, imageB, runnerImage]) assert.match(image, /@sha256:[a-f0-9]{64}$/); const m = { kubeconfig, context }, ns = "ow-version-" + randomBytes(4).toString("hex"), root = join(repo, ".ow-stack", ns), id = randomUUID(), checks = [], forwards = []; await mkdir(root, { recursive: true, mode: 0o700 }); const save = (name, v) => writeFile( join(root, name), typeof v === "string" ? v : JSON.stringify(v, null, 2), { mode: 0o600 }, ); const k = (args, body) => kubectl(m, ["-n", ns, ...args], body); const apply = (items) => k( ["apply", "-f", "-"], JSON.stringify({ apiVersion: "v1", kind: "List", items }), ); const meta = (name) => ({ name, namespace: ns }); const secret = (name, stringData) => ({ apiVersion: "v1", kind: "Secret", metadata: meta(name), stringData, }); const pass = (s) => { checks.push(s); console.log("PASS " + s); }; const wait = async (fn, ms = 120000) => { const end = Date.now() + ms; while (Date.now() < end) { const v = await fn(); if (v) return v; await new Promise((r) => setTimeout(r, 500)); } throw Error("drill state deadline"); }; const token = randomBytes(32).toString("hex"), password = randomBytes(32).toString("hex"); let environment; await save("ownership.json", { ns, id, context, kubeconfig }); async function certificate(name, dns) { const key = join(root, name + ".key"), crt = join(root, name + ".crt"); await run("openssl", [ "req", "-x509", "-newkey", "rsa:2048", "-nodes", "-days", "2", "-keyout", key, "-out", crt, "-subj", "/CN=" + dns, "-addext", "subjectAltName=DNS:" + dns, "-addext", "basicConstraints=critical,CA:TRUE", ]); return { key: await readFile(key, "utf8"), crt: await readFile(crt, "utf8") }; } const pg = await certificate("pg", `postgres.${ns}.svc.cluster.local`), tls = await certificate("api", "localhost"); const shell = {}; for (const name of ["ca.crt", "tls.crt", "tls.key"]) { shell[name] = await readFile(join(certificates, name), "utf8"); } const config = { database_env: "OW_SERVER_DATABASE_URL", schema: "ow_versions", database_ca: "/database-ca/ca.crt", state_root: "/state", bundle: "/opt/openworker/bundle", runner: "/opt/openworker/openworker", model: "fixture", scripted: true, api_token_file: "/bootstrap/admin", actor: "version-drill", openshell: { identity: "version-drill-" + id, endpoint: gateway, certificates: "/openshell", tls_server_name: "localhost", workspace: "default", image: runnerImage, }, }; const values = (image, replicas = 1) => ({ image, api: { replicas }, worker: { replicas, slots: 1, drainTimeoutSeconds: 60, shutdownTimeoutSeconds: 30, }, terminationGracePeriodSeconds: 110, resources: { requests: { cpu: "100m", memory: "128Mi" }, limits: { cpu: "1", memory: "768Mi" }, }, config, secrets: { database: "database", databaseCA: "database-ca", openshell: "openshell-client", apiTLS: "api-tls", probe: "probe", }, network: { enabled: false }, }); async function install(release, image, replicas = 1, override = {}) { const path = join(root, release + ".json"); await save(release + ".json", { ...values(image, replicas), ...override }); await run(helm, [ "upgrade", "--install", release, join(repo, "deploy/helm/openworker"), "--kubeconfig", kubeconfig, "--kube-context", context, "-n", ns, "-f", path, ]); } async function admin(release, args) { const dep = JSON.parse( await k([ "get", "deployment", release + "-openworker-worker", "-o", "json", ]), ); const spec = dep.spec.template.spec, c = spec.containers[0]; c.args = ["--config", "/config/reference.json", ...args]; delete c.readinessProbe; delete c.livenessProbe; delete c.envFrom; c.env = c.env.filter((e) => e.name !== "POD_NAME"); c.volumeMounts.push({ name: "bootstrap", mountPath: "/bootstrap/admin", subPath: "admin", readOnly: true, }); spec.volumes.push({ name: "bootstrap", secret: { secretName: "bootstrap", defaultMode: 288 }, }); spec.restartPolicy = "Never"; const name = "admin-" + randomBytes(4).toString("hex"); await apply([ { apiVersion: "batch/v1", kind: "Job", metadata: meta(name), spec: { backoffLimit: 0, template: { spec } }, }, ]); await k([ "wait", "--for=condition=complete", "job/" + name, "--timeout=150s", ]); return k(["logs", "job/" + name]); } async function forward(release) { const ps = JSON.parse( await k([ "get", "pods", "-l", `app.kubernetes.io/instance=${release},app.kubernetes.io/component=api`, "-o", "json", ]), ).items; const pod = ps.find( (p) => !p.metadata.deletionTimestamp && p.status.containerStatuses?.every((c) => c.ready), ); assert.ok(pod); const child = spawn( "kubectl", [ "--kubeconfig", kubeconfig, "--context", context, "-n", ns, "port-forward", "--address", "127.0.0.1", "pod/" + pod.metadata.name, "0:8080", ], { stdio: ["ignore", "pipe", "pipe"] }, ); forwards.push(child); child.stderr.resume(); return new Promise((resolve, reject) => { let out = ""; const timer = setTimeout(() => reject(Error("forward timeout")), 15000); child.stdout.on("data", (b) => { out += b; const m = out.match(/127.0.0.1:(\d+) ->/); if (m) { clearTimeout(timer); resolve("http://127.0.0.1:" + m[1]); } }); child.on("error", reject); }); } async function request(base, path, body) { const r = await fetch(base + path, { method: body ? "POST" : "GET", headers: { Authorization: "Bearer " + token, "Content-Type": "application/json", }, body: body ? JSON.stringify(body) : undefined, signal: AbortSignal.timeout(20000), }); assert.ok(r.ok, `${path} ${r.status}`); return r.json(); } const create = async (base) => ( await request(base, "/sessions", { request_id: randomUUID(), parameters: { name: "Version qualification", coworker: "assistant-example", workspace: "/sandbox", environment_id: environment.id, }, }) ).session_id; const turn = (base, sid, name, args) => request(base, `/sessions/${sid}/turns`, { submission_id: randomUUID(), text: "TEST_RESOURCE_GRANTS " + JSON.stringify([{ name, arguments: args }]), }); const snap = (base, sid) => request(base, `/sessions/${sid}`); const state = (base, sid, status) => wait(async () => { const s = await snap(base, sid); assert.ok(!["failed", "truncated"].includes(s.turn?.status)); return s.turn?.status === status && s; }); const approve = (base, sid, s) => { const i = s.interactions.find((i) => !i.response); assert.ok(i); return request(base, `/sessions/${sid}/controls`, { request_id: randomUUID(), turn_id: i.turn_id, action: { kind: "response", interaction_id: i.id, response: "once", binding: Object.fromEntries( [ "bundle_digest", "schema_digest", "policy_digest", "resource_policy_digest", ].map((k) => [k, i[k]]), ), }, }); }; const rollout = (release, role = "worker") => k([ "rollout", "status", `deployment/${release}-openworker-${role}`, "--timeout=180s", ]); try { await kubectl(m, ["create", "namespace", ns]); await kubectl(m, [ "annotate", "namespace", ns, "openworker.dev/version-drill=" + id, ]); await apply([ secret("database", { url: `postgresql://openworker:${password}@postgres.${ns}.svc.cluster.local/openworker`, }), secret("postgres-login", { POSTGRES_USER: "openworker", POSTGRES_DB: "openworker", POSTGRES_PASSWORD: password, }), secret("database-ca", { "ca.crt": pg.crt }), secret("postgres-tls", { "tls.crt": pg.crt, "tls.key": pg.key }), secret("api-tls", { "tls.crt": tls.crt, "tls.key": tls.key }), secret("openshell-client", shell), secret("bootstrap", { admin: token }), secret("probe", { "curl.conf": `header = "Authorization: Bearer ${token}"\n`, }), { apiVersion: "v1", kind: "Service", metadata: meta("postgres"), spec: { selector: { app: "postgres" }, ports: [{ port: 5432 }] }, }, { apiVersion: "apps/v1", kind: "Deployment", metadata: meta("postgres"), spec: { replicas: 1, selector: { matchLabels: { app: "postgres" } }, template: { metadata: { labels: { app: "postgres" } }, spec: { containers: [ { name: "postgres", image: "postgres@sha256:cf78e76683b9ca8c5733cbbdce6c9262b45b6767934dd0a95e671f9a0fc20685", command: [ "sh", "-ec", "cp /cert/* /tmp/; chown postgres:postgres /tmp/tls.*; chmod 600 /tmp/tls.key; exec docker-entrypoint.sh postgres -c ssl=on -c ssl_cert_file=/tmp/tls.crt -c ssl_key_file=/tmp/tls.key", ], envFrom: [{ secretRef: { name: "postgres-login" } }], resources: { limits: { cpu: "1", memory: "1Gi" } }, readinessProbe: { exec: { command: ["pg_isready", "-U", "openworker"] }, periodSeconds: 2, }, volumeMounts: [ { name: "cert", mountPath: "/cert" }, { name: "data", mountPath: "/var/lib/postgresql/data" }, ], }, ], volumes: [ { name: "cert", secret: { secretName: "postgres-tls" } }, { name: "data", emptyDir: {} }, ], }, }, }, }, ]); await k(["rollout", "status", "deployment/postgres", "--timeout=90s"]); await install("old", imageA, 0); await admin("old", ["migrate"]); await admin("old", ["auth-bootstrap"]); environment = JSON.parse( ( await admin("old", ["environment-create", "--request-id", id, "--shared"]) ).trim(), ); await save("environment.json", environment); await install("old", imageA); await rollout("old"); await rollout("old", "api"); await install("bad", imageA, 1, { api: { replicas: 0 }, config: { ...config, openshell: { ...config.openshell, endpoint: "https://127.0.0.1:9" }, }, }); await wait(async () => { const pods = JSON.parse( await k([ "get", "pods", "-l", "app.kubernetes.io/instance=bad,app.kubernetes.io/component=worker", "-o", "json", ]), ).items; return pods.some((p) => p.status.containerStatuses?.some((c) => c.restartCount > 0 && !c.ready), ); }); assert.ok( !JSON.parse(await admin("old", ["worker-pools"])).pools.some((p) => p.workers.some((w) => w.node.startsWith("bad-")), ), ); await run(helm, [ "uninstall", "bad", "--kubeconfig", kubeconfig, "--kube-context", context, "-n", ns, ]); pass( "Unavailable gateway cannot pass readiness or advertise a usable worker", ); let a = await forward("old"); const parked = await create(a); await turn(a, parked, "write_file", { path: "old.txt", content: "old identity retained", }); const pending = await state(a, parked, "waiting"), binding = pending.interactions[0].bundle_digest; pass("Old cohort parked at a durable approval"); await install("new", imageB); await rollout("new"); await rollout("new", "api"); let b = await forward("new"); const fresh = await create(b); await turn(b, fresh, "write_file", { path: "new.txt", content: "new identity", }); const newPending = await state(b, fresh, "waiting"); assert.notEqual(newPending.interactions[0].bundle_digest, binding); await approve(b, fresh, newPending); await state(b, fresh, "completed"); assert.deepEqual((await snap(b, parked)).interactions, pending.interactions); pass( "Side-by-side pools execute only their exact identities; new API reads old approval unchanged", ); // Prove missing old pool is visible and new pool cannot consume its answer. await k(["scale", "deployment/old-openworker-worker", "--replicas=0"]); await wait( async () => !JSON.parse( await k([ "get", "pods", "-l", "app.kubernetes.io/instance=old,app.kubernetes.io/component=worker", "-o", "json", ]), ).items.length, ); await approve(b, parked, pending); await turn(b, parked, "read_file", { path: "old.txt" }); let report = JSON.parse(await admin("new", ["worker-pools"])); const old = report.pools.find((p) => p.execution_digest === binding); assert.equal(old.pending_controls, 1); assert.equal(old.queued_turns, 1); assert.equal(old.sessions_without_advertised_worker, 1); assert.deepEqual((await snap(b, parked)).interactions, pending.interactions); pass( "Removing old pool leaves approval and queued follow-up intact; catalog flags uncovered session", ); await k(["scale", "deployment/old-openworker-worker", "--replicas=1"]); await rollout("old"); await wait(async () => { const s = await snap(b, parked); return s.turn?.status === "waiting" && s.turn.id !== pending.turn.id; }); // This bundled MCP read is still external: previous write approval does not // authorize a distinct read call. Answer the follow-up's own durable prompt. await approve(b, parked, await snap(b, parked)); await state(b, parked, "completed"); pass( "Restoring pinned old pool applies approval and queued follow-up without identity rewrite", ); // Active execution finishes during a genuine Deployment rolling restart. const active = await create(a); await turn(a, active, "run_shell", { command: "sleep 20; printf 'one\\n' >> rollout-effects.txt", }); const ap = await state(a, active, "waiting"); await approve(a, active, ap); await state(a, active, "executing"); const oldWorker = JSON.parse( await k([ "get", "pods", "-l", "app.kubernetes.io/instance=old,app.kubernetes.io/component=worker", "-o", "json", ]), ).items.find((p) => !p.metadata.deletionTimestamp).metadata.name; await k(["rollout", "restart", "deployment/old-openworker-worker"]); await wait( async () => JSON.parse(await k(["get", "pod", oldWorker, "-o", "json"])).metadata .deletionTimestamp, ); assert.equal((await snap(b, active)).turn.status, "executing"); assert.equal( await k([ "exec", oldWorker, "--", "curl", "--silent", "--output", "/dev/null", "--write-out", "%{http_code}", "--max-time", "2", "http://127.0.0.1:8081/readyz", ]), "503", ); await state(b, active, "completed"); await rollout("old"); assert.equal( await kubectl(m, [ "-n", "openshell", "exec", "default--" + environment.binding.remote_name, "-c", "agent", "--", "cat", "/sandbox/rollout-effects.txt", ]), "one\n", ); pass( "Kubernetes rolling restart marks draining worker unready, finishes active execution exactly once and starts a ready replacement", ); // API creation rollback restores old identity; both worker pools stay present. await k([ "set", "image", "deployment/new-openworker-api", "harness=" + imageA, ]); await rollout("new", "api"); b = await forward("new"); const rollback = await create(b); await turn(b, rollback, "write_file", { path: "rollback.txt", content: "rollback", }); const rp = await state(b, rollback, "waiting"); assert.equal(rp.interactions[0].bundle_digest, binding); // New-version idle sessions still exist: their worker pool MUST remain. await k(["rollout", "undo", "deployment/new-openworker-api"]); await rollout("new", "api"); await approve(a, rollback, rp); await state(a, rollback, "completed"); report = JSON.parse(await admin("old", ["worker-pools"])); assert.equal(report.pools.length, 2); assert.ok( report.pools.every((p) => p.sessions_without_advertised_worker === 0), ); pass( "Creation rollback uses old identity; both cohorts retained for resumable sessions", ); await save("report.json", { checks, namespace: ns, context, imageA, imageB, report, live_model: false, scope: "local kind; synthetic model; real PostgreSQL/OpenShell/tools", }); console.log("REPORT " + join(root, "report.json")); } finally { for (const child of forwards) child.kill("SIGTERM"); // Keep exact owned resources on failure for diagnosis. Cleanup is a separate // explicit operator action after inspecting report and environment identity. await save("checks.json", { checks, environment_id: environment?.id, namespace: ns, }); }