From 04e1ce24cc9598cc37b4302a24ef132ab5546c08 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Mon, 2 Nov 2026 15:41:17 -0600 Subject: [PATCH] row 227 R2 (#1728): run test-case2.sh standalone, without the slot controller Remove the R1 slot-controller ties from the C2-14 fixture: the ROW227_CONTROL_ROOT check, the native permit wait in bindLaunch, the fixture-cleanup.json write, the priority.mjs preload and the owned-bwrap.sh override. The launcher uses its own bwrap. The shell runs the driver from an explicit env -i environment (PATH, LANG, a sandbox HOME, XDG dirs and TMPDIR, MOSAIC_CONFIG, no global git config) as its own process group. On a signal the driver gets 3 s by the clock, then its group and any process whose cwd is in the sandbox get SIGKILL, inside the landing gate's 5 s grace. The fixture credentials are defined only in scripts/case2/fixture-credentials.mjs. The new scripts/case2/leak-check.mjs reads them from there and never prints them. --- scripts/case2/citation-adapter.mjs | 1 - scripts/case2/e2e.mjs | 9 ++-- scripts/case2/fixture-credentials.mjs | 8 +++- scripts/case2/launch.mjs | 31 ++----------- scripts/case2/leak-check.mjs | 16 +++++++ scripts/test-case2.sh | 66 +++++++++++++++++++-------- 6 files changed, 77 insertions(+), 54 deletions(-) create mode 100644 scripts/case2/leak-check.mjs diff --git a/scripts/case2/citation-adapter.mjs b/scripts/case2/citation-adapter.mjs index edd17fbfa..75086fc4e 100644 --- a/scripts/case2/citation-adapter.mjs +++ b/scripts/case2/citation-adapter.mjs @@ -1,6 +1,5 @@ // C2-14 adds an observed real library tool read before C2-06's scripted reply adapter. // The reply plan is fixture input, never evidence that a lookup or guard actually ran. -import '../../agents/steelbeak/work/queue-227/gate/priority.mjs'; import { appendFileSync, existsSync, readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { callTool } from '../../packages/harness/src/tools.mjs'; diff --git a/scripts/case2/e2e.mjs b/scripts/case2/e2e.mjs index 00f694ea0..6b514085a 100644 --- a/scripts/case2/e2e.mjs +++ b/scripts/case2/e2e.mjs @@ -1,7 +1,7 @@ // C2-14 (#1728): one scratch-root acceptance driver. No live host or real model. // The human uses bindHuman in-process because a development agent cannot prove human CLI ancestry. // The creative session is real createLauncher -> runner -> C2-06's scripted citation adapter. -// All 17 steps run against landed APIs. Execution is serialized by Sage (Mos1961). +// All 17 steps run against landed APIs. It runs standalone from scripts/test-case2.sh, as the landing gate runs it. import { mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'; import { dirname, join, resolve } from 'node:path'; import { DatabaseSync } from 'node:sqlite'; @@ -10,17 +10,14 @@ import { BASE, MARKER, git, mappingEntry } from '../../packages/library/tests/he import * as library from '../../packages/library/src/index.mjs'; import { launchFixture } from './launch.mjs'; import { consoleAcceptance } from './console.mjs'; -import { FAKE_ZAI_KEY } from './fixture-credentials.mjs'; +import { FETCH_TOKEN, SEAT_TOKEN, HUMAN_TOKEN, FIXTURE_CREDENTIALS } from './fixture-credentials.mjs'; -const FETCH_TOKEN = 'fx227-fetch-q7-2270'; -const SEAT_TOKEN = 'fx227-seat-q7-2270'; -const HUMAN_TOKEN = 'fx227-human-q7-2270'; const USER = 'books-bot', BUSINESS = 'acme'; const CHARACTER = 'Book One/Characters/Mara Vell.md'; const SUPPORT = 'Book One/Notes/Eyes.md'; const WRITE = 'Book One/Notes/Agent.md'; const EDITOR = { name: 'Fixture author', email: 'author@example.test', user: 'writer' }; -const secrets = [FETCH_TOKEN, SEAT_TOKEN, HUMAN_TOKEN, FAKE_ZAI_KEY]; +const secrets = FIXTURE_CREDENTIALS; const redact = value => secrets.reduce((s, token) => s.replaceAll(token, '[fixture credential]'), String(value)); let failures = 0; const out = line => process.stdout.write(redact(line) + '\n'); diff --git a/scripts/case2/fixture-credentials.mjs b/scripts/case2/fixture-credentials.mjs index 9263b160c..4557637b5 100644 --- a/scripts/case2/fixture-credentials.mjs +++ b/scripts/case2/fixture-credentials.mjs @@ -1,2 +1,8 @@ -// Made up and confined to the nonproxy fake pi/zai fixture. Never a live credential. +// Made-up fixture credentials for C2-14 (#1728). Never a live credential. No other file under +// scripts/ holds them: the driver (which hands them to the fake Gitea), the fixture launcher (the +// fake pi/zai key) and the shell's leak check import them from here. +export const FETCH_TOKEN = 'fx227-fetch-q7-2270'; +export const SEAT_TOKEN = 'fx227-seat-q7-2270'; +export const HUMAN_TOKEN = 'fx227-human-q7-2270'; export const FAKE_ZAI_KEY = 'fx227-zai-q7-2270'; +export const FIXTURE_CREDENTIALS = Object.freeze([FETCH_TOKEN, SEAT_TOKEN, HUMAN_TOKEN, FAKE_ZAI_KEY]); diff --git a/scripts/case2/launch.mjs b/scripts/case2/launch.mjs index 288f264fe..d29709067 100644 --- a/scripts/case2/launch.mjs +++ b/scripts/case2/launch.mjs @@ -1,5 +1,5 @@ // C2-14 fixture: the real host/launcher with C2-06's scripted citation adapter. No model binary runs. -import { chmodSync, mkdirSync, writeFileSync, existsSync, readdirSync, readFileSync, renameSync } from 'node:fs'; +import { chmodSync, mkdirSync, writeFileSync, existsSync, readdirSync } from 'node:fs'; import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; import { BusError, writeGrantEvent } from '../../packages/bus/src/broker.mjs'; @@ -43,21 +43,6 @@ function seedGrant(dataRoot, storeRoot, token) { } export async function launchFixture(root, entry, { fetchToken, seatToken, humanToken, ca }) { - const control = process.env.ROW227_CONTROL_ROOT; - if (!control) throw new Error('row227-native-control-required'); - const nativeNonce = JSON.parse(readFileSync(join(control, 'control.json'), 'utf8')).nonce; - const nativePermit = async record => { - const file = join(control, 'authorizations', `${record.pid}-${record.startTime}.json`); - const end = Date.now() + 5000; - while (!existsSync(file)) { - if (Date.now() >= end) throw Object.assign(new Error('native-host-permit-timeout'), { code: 'native-host-permit-timeout' }); - await new Promise(resolve => setTimeout(resolve, 10)); - } - const permit = JSON.parse(readFileSync(file, 'utf8')); - if (permit.nonce !== nativeNonce || permit.binding?.role !== 'runner' - || permit.binding.wrapper?.[0] !== record.pid || permit.binding.wrapper?.[1] !== record.startTime - || permit.binding.run?.split('/').at(-1) !== record.run) throw Object.assign(new Error('native-host-permit-refused'), { code: 'native-host-permit-refused' }); - }; const configDir = join(root, 'config'), storeRoot = join(configDir, 'secrets', 'agents'); const slotDir = join(configDir, 'secrets', 'acme'); mkdirSync(slotDir, { recursive: true, mode: 0o700 }); @@ -103,9 +88,6 @@ export async function launchFixture(root, entry, { fetchToken, seatToken, humanT for (const cleanup of cleanups.reverse()) await cleanup(); const directory = join(f.dataRoot, 'control', 'runtimes'); const remaining = existsSync(directory) ? readdirSync(directory) : []; - writeFileSync(join(control, 'fixture-cleanup.pending'), JSON.stringify({ version: 1, dataRoot: f.dataRoot, runtimeEntries: remaining, - runs: records.map(({ pid, startTime, run }) => ({ pid, startTime, run })) }) + '\n', { mode: 0o600 }); - renameSync(join(control, 'fixture-cleanup.pending'), join(control, 'fixture-cleanup.json')); if (remaining.length) throw new Error('owned-runtime-cleanup-incomplete'); } } @@ -127,7 +109,6 @@ export async function launchFixture(root, entry, { fetchToken, seatToken, humanT const host = { business: 'acme', path: runtime.path, dataRoot: f.dataRoot, op: wrap(m => { if (!Object.hasOwn(ops, m.op)) throw new BusError('invalid-host-request'); return ops[m.op](m); }), bindLaunch: wrap(async record => { - await nativePermit(record); const bound = runtime.bindLaunch(record); records.push(record); caps.set(record.thread, bound.cap); return bound; @@ -135,15 +116,11 @@ export async function launchFixture(root, entry, { fetchToken, seatToken, humanT beforeClose: fn => void hooks.push(fn), }; const adapter = join(root, 'citation-adapter.sh'); - writeFileSync(adapter, `cp ${shell(plan)} "$(dirname "$MOSAIC_TURN_MARKER")/fixture-steps.json"\nexec ${shell(process.execPath)} --import ${shell(fileURLToPath(new URL('../../agents/steelbeak/work/queue-227/gate/priority.mjs', import.meta.url)))} ${shell(ADAPTER)}\n`, { mode: 0o644 }); - const priority = fileURLToPath(new URL('../../agents/steelbeak/work/queue-227/gate/priority.mjs', import.meta.url)); - const wrapper = fileURLToPath(new URL('../../agents/steelbeak/work/queue-227/gate/bwrap.py', import.meta.url)); - const bwrap = join(root, 'owned-bwrap.sh'); - writeFileSync(bwrap, `#!/bin/sh\nexec /usr/bin/python3 ${shell(wrapper)} ${shell(control)} ${shell(priority)} \"$@\"\n`, { mode: 0o700 }); - const launcher = createLauncher({ host, system, env: f.env, bwrap, + writeFileSync(adapter, `cp ${shell(plan)} "$(dirname "$MOSAIC_TURN_MARKER")/fixture-steps.json"\nexec ${shell(process.execPath)} ${shell(ADAPTER)}\n`, { mode: 0o644 }); + const launcher = createLauncher({ host, system, env: f.env, makeLoader: await fakeHarnesses({ after: fn => cleanups.push(fn) }, { pi: adapter }, write => write('channel', 'webui', `export * from ${JSON.stringify(new URL('../../packages/plugins/channel/webui/index.mjs', import.meta.url).href)};`)), - readOnly: [ADAPTER, REPLY_ADAPTER, plan, priority], harnessVersion: '0.85.1', + readOnly: [ADAPTER, REPLY_ADAPTER, plan], harnessVersion: '0.85.1', sessionDefaults: { pollInterval: 50 }, threadTickMs: 50, log: line => logs.push(line), }); await launcher.listen(); diff --git a/scripts/case2/leak-check.mjs b/scripts/case2/leak-check.mjs new file mode 100644 index 000000000..2b317c2f7 --- /dev/null +++ b/scripts/case2/leak-check.mjs @@ -0,0 +1,16 @@ +// C2-14 (#1728): the shell's check that no fixture credential reached the driver's output. +// The values come from fixture-credentials.mjs, the one place they are defined, and are never +// printed. Exit 0: none of the files holds one. Exit 1: one does. Exit 2: no file given, or a +// file could not be read, so nothing was checked. +import { readFileSync } from 'node:fs'; +import { FIXTURE_CREDENTIALS } from './fixture-credentials.mjs'; + +const files = process.argv.slice(2); +if (!files.length || !FIXTURE_CREDENTIALS.length) process.exit(2); +let found = false; +for (const file of files) { + let bytes; + try { bytes = readFileSync(file); } catch { process.exit(2); } + if (FIXTURE_CREDENTIALS.some(value => bytes.includes(value))) found = true; +} +process.exit(found ? 1 : 0); diff --git a/scripts/test-case2.sh b/scripts/test-case2.sh index 7a37e4715..432d640f3 100755 --- a/scripts/test-case2.sh +++ b/scripts/test-case2.sh @@ -1,27 +1,57 @@ #!/usr/bin/env bash -# C2-14 (#1728, row227): one acceptance run on a scratch root, no skipped steps. +# C2-14 (#1728, row 227): one acceptance run on a scratch root, no skipped steps. # Entry point for the real launcher/runner, C2-06 scripted adapter, fake TLS Gitea and Console DOM. -# Run only in the serialized target slot (Mos1961); full gates/review/landing belong to the parent. +# Stands alone: the landing gate runs it as `bash scripts/test-case2.sh` with nothing else set up. # The only human stand-in is bindHuman in-process; a development agent cannot prove human CLI ancestry. set -uo pipefail cd "$(dirname "$0")/.." SANDBOX="$(mktemp -d)" DRIVER_PID='' CASE2_HOME="$SANDBOX/home" +CASE2_TMP="$SANDBOX/t" CASE2_CONFIG="$SANDBOX/root/config/config.json" -mkdir -p "$CASE2_HOME" +mkdir -p "$CASE2_HOME" "$CASE2_TMP" +# The driver runs from an explicit environment: PATH and LANG, a sandbox HOME, XDG dirs and TMPDIR, +# and the variables it needs. Nothing else from the caller, a credential variable included, reaches +# the driver or the launcher it starts. TMPDIR in the sandbox takes the Chromium profile and the +# plug-in tree with it when the sandbox goes. +CLEAN_ENV=(env -i PATH="$PATH" LANG=C.UTF-8 HOME="$CASE2_HOME" + XDG_CONFIG_HOME="$CASE2_HOME/.config" XDG_CACHE_HOME="$CASE2_HOME/.cache" + XDG_DATA_HOME="$CASE2_HOME/.local/share" XDG_STATE_HOME="$CASE2_HOME/.local/state" + TMPDIR="$CASE2_TMP" MOSAIC_CONFIG="$CASE2_CONFIG" GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1) +alive() { + local stat + read -r stat 2>/dev/null <"/proc/$1/stat" || return 1 + stat="${stat##*) }" + [ "${stat%% *}" != Z ] +} +# The driver leads its own process group (setsid below). On a signal it gets SIGTERM and 3 s by the +# clock to close, inside the landing gate's 5 s grace, then its group gets SIGKILL. The launcher +# starts each bwrap session in a group of its own, with its working directory in the sandbox; any +# such process still alive then gets SIGKILL too, before the sandbox goes. One find reads every +# cwd link, so the sweep stays in milliseconds however many processes the host runs. +now_us() { echo "${EPOCHREALTIME/[.,]/}"; } +stop_driver() { + local deadline p left=0 + if [ -n "$DRIVER_PID" ]; then + kill -TERM "$DRIVER_PID" 2>/dev/null + deadline=$(( $(now_us) + 3000000 )) + while alive "$DRIVER_PID" && [ "$(now_us)" -lt "$deadline" ]; do sleep 0.1; done + kill -KILL -- "-$DRIVER_PID" 2>/dev/null + wait "$DRIVER_PID" 2>/dev/null + DRIVER_PID='' + fi + for p in $(find /proc/[0-9]*/cwd -maxdepth 0 \( -lname "$SANDBOX" -o -lname "$SANDBOX/*" \) 2>/dev/null); do + p="${p#/proc/}" + kill -KILL "${p%/cwd}" 2>/dev/null && left=$((left+1)) + done + if [ "$left" -gt 0 ]; then echo "note: $left process(es) under the sandbox outlived the driver; sent SIGKILL"; fi +} cleanup() { local result=$? trap - EXIT INT TERM - if [ -n "$DRIVER_PID" ]; then - kill -TERM "$DRIVER_PID" 2>/dev/null || true - wait "$DRIVER_PID" 2>/dev/null || true - fi - # The private controlled invocation delegates removal to the controller only after - # its source/native/capture/closure checks. Unset marker retains standalone cleanup. - if [ "${ROW227_CONTROLLER_CLEANUP:-}" != 1 ]; then - rm -rf "$SANDBOX" - fi + stop_driver + rm -rf "$SANDBOX" exit "$result" } trap cleanup EXIT @@ -39,10 +69,10 @@ check() { } DRIVER=scripts/case2/e2e.mjs STEPS='S1a S1b S2a S2b S2c S2d S3a S3b S3c S4 S5a S5b S6 S7 S8 S9 S10' -node --check "$DRIVER" >/dev/null 2>&1 +"${CLEAN_ENV[@]}" node --check "$DRIVER" >/dev/null 2>&1 check "syntax: $DRIVER" "$?" OUT="$SANDBOX/steps.txt" ERR="$SANDBOX/stderr.txt" -env HOME="$CASE2_HOME" XDG_CONFIG_HOME="$CASE2_HOME/.config" MOSAIC_CONFIG="$CASE2_CONFIG" GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1 node "$DRIVER" "$SANDBOX/root" >"$OUT" 2>"$ERR" & +"${CLEAN_ENV[@]}" setsid node "$DRIVER" "$SANDBOX/root" >"$OUT" 2>"$ERR" & DRIVER_PID=$! wait "$DRIVER_PID" RC=$? @@ -60,10 +90,8 @@ while IFS= read -r line; do done <"$OUT" [ "${seen# }" = "$STEPS" ] check 'every acceptance step ran once, in order, with nothing skipped' "$?" -if grep -qF -e 'fx227-fetch-q7-2270' -e 'fx227-seat-q7-2270' -e 'fx227-human-q7-2270' -e 'fx227-zai-q7-2270' "$OUT" "$ERR"; then - check 'fixture credentials are in no driver output' 1 -else - check 'fixture credentials are in no driver output' 0 -fi +# The values come from scripts/case2/fixture-credentials.mjs and are never printed. +"${CLEAN_ENV[@]}" node scripts/case2/leak-check.mjs "$OUT" "$ERR" +check 'fixture credentials are in no driver output' "$?" printf '\nselftest: %s passed, %s failed, 0 skipped\n' "$PASS" "$FAIL" [ "$FAIL" -eq 0 ]