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.
This commit is contained in:
1 parent
46c07df75f
commit
04e1ce24cc
6 files changed
+77
-54
No files matched your search
@@ -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';
|
||||
|
||||
@@ -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: '[email protected]', 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');
|
||||
|
||||
@@ -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]);
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
+47
-19
@@ -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 ]
|
||||
Reference in new issue
Block a user