Add Phase 9 validation and advisory review
Record the finite dependency search, Wi-Fi maintenance blocker, and pinned icon provenance. Add bounded host orchestration and fixture coverage, and update release documentation with current evidence.
This commit is contained in:
@@ -28,8 +28,9 @@ verbatim, including mixed grants and the wolfSSH package/header discrepancy.
|
||||
|
||||
This is NOT corresponding source, a complete SBOM, an archive of the SDK/tools,
|
||||
or proof of notice delivery to firmware/device/browser recipients. Radio-blob
|
||||
corresponding-source/System Library questions remain unresolved. Icon provenance,
|
||||
wolfSSH packaging clarification, bootloader/runtime attribution, source delivery
|
||||
corresponding-source/System Library questions remain unresolved. Exact icon SVG
|
||||
source evidence and manual-derivative limits are in inputs/project/docs/icon_provenance.md.
|
||||
WolfSSH packaging clarification, bootloader/runtime attribution, source delivery
|
||||
and Installation Information still require release review. No legal clearance.
|
||||
""".encode("utf-8")
|
||||
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Bounded Phase 9 host validation; build and local interop are explicit opt-ins."""
|
||||
import argparse
|
||||
from dataclasses import dataclass
|
||||
import math
|
||||
import os
|
||||
from pathlib import Path
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
BUILD = Path('.pio/build/esp32-s3-devkitc-1-n16r8')
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Command:
|
||||
name: str
|
||||
argv: tuple
|
||||
timeout: float
|
||||
required: tuple = ()
|
||||
|
||||
|
||||
def positive_seconds(value):
|
||||
seconds = float(value)
|
||||
if not math.isfinite(seconds) or not 0 < seconds <= 3600:
|
||||
raise argparse.ArgumentTypeError('timeout must be finite and in (0, 3600] seconds')
|
||||
return seconds
|
||||
|
||||
|
||||
def parser():
|
||||
cli = argparse.ArgumentParser(description=__doc__)
|
||||
cli.add_argument('--build', action='store_true', help='run pio run before host checks (may fetch dependencies)')
|
||||
cli.add_argument('--interop', action='store_true', help='enable ordering OpenSSH local Unix-socket matrix')
|
||||
cli.add_argument('--dry-run', action='store_true', help='print plan only; no execution or prerequisite validation')
|
||||
cli.add_argument('--fail-fast', action='store_true', help='stop after first failure; default collects failures')
|
||||
cli.add_argument('--timeout', type=positive_seconds, default=180, help='seconds per host command (default: 180, maximum: 3600)')
|
||||
cli.add_argument('--build-timeout', type=positive_seconds, default=600, help='seconds for optional build (default: 600, maximum: 3600)')
|
||||
return cli
|
||||
|
||||
|
||||
def plan(options):
|
||||
commands = []
|
||||
if options.build:
|
||||
commands.append(Command('build', ('pio', 'run'), options.build_timeout))
|
||||
|
||||
def suite(name, *args, required=(), label=None):
|
||||
path = Path('tests') / name / 'run.py'
|
||||
commands.append(Command(label or name, (sys.executable, '-B', str(path), *map(str, args)),
|
||||
options.timeout, (path, *required)))
|
||||
|
||||
header = BUILD / 'config/sdkconfig.h'
|
||||
database = BUILD / 'compile_commands.json'
|
||||
suite('security_build_policy', '--sdkconfig-header', header, required=(header,))
|
||||
for name in ('ssh_auth_policy', 'ssh_auth_transport', 'hidden_input', 'ssh_memory'):
|
||||
suite(name)
|
||||
suite('sdk_security_overrides', '--build-dir', BUILD, required=(database,))
|
||||
for name in ('wolfssh_auth_contract', 'ssh_protocol_policy', 'wolf_crypto_policy'):
|
||||
suite(name, '--compile-commands', database, required=(database,))
|
||||
suite('wolfssh_parser_contract')
|
||||
suite('wolfssh_order_contract', *(['--interop'] if options.interop else []))
|
||||
suite('release_notices')
|
||||
for name in ('ssh_management', 'admin_console_boundary', 'admin_ssh_policy',
|
||||
'web_admin_transport', 'web_admin_tickets', 'web_httpd_idle'):
|
||||
suite(name)
|
||||
# The current runner has no --admission branch: its default covers admission.
|
||||
for mode in ('base', 'admin', 'accounts', 'ssh', 'lifecycle'):
|
||||
suite('web_cookie_auth', *([] if mode == 'base' else ['--' + mode]),
|
||||
label='web_cookie_auth:' + mode)
|
||||
return commands
|
||||
|
||||
|
||||
def kill_group(process):
|
||||
try:
|
||||
os.killpg(process.pid, signal.SIGKILL)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
process.wait(timeout=5)
|
||||
|
||||
|
||||
def run_command(command, root, env):
|
||||
missing = [str(path) for path in command.required if not (root / path).is_file()]
|
||||
if missing:
|
||||
return 'PREREQ', 'missing ' + ', '.join(missing)
|
||||
try:
|
||||
# Inherit stdout/stderr: no persistent captures or environment dumps.
|
||||
process = subprocess.Popen(command.argv, cwd=root, env=env, stdin=subprocess.DEVNULL,
|
||||
start_new_session=True)
|
||||
except OSError as error:
|
||||
return 'PREREQ', f'cannot launch command (errno {error.errno})'
|
||||
try:
|
||||
code = process.wait(timeout=command.timeout)
|
||||
except subprocess.TimeoutExpired:
|
||||
kill_group(process)
|
||||
return 'TIMEOUT', f'exceeded {command.timeout:g}s; process group killed'
|
||||
except KeyboardInterrupt:
|
||||
kill_group(process)
|
||||
raise
|
||||
# Also retire any descendants left by a runner that exited early.
|
||||
try:
|
||||
os.killpg(process.pid, signal.SIGKILL)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
return ('PASS', 'exit 0') if code == 0 else ('FAIL', f'exit {code}')
|
||||
|
||||
|
||||
def execute(commands, root, *, dry_run=False, fail_fast=False):
|
||||
if os.name != 'posix':
|
||||
print('PREREQ: POSIX process groups required', flush=True)
|
||||
return 1
|
||||
env = os.environ.copy()
|
||||
env['CCACHE_DISABLE'] = '1'
|
||||
results = []
|
||||
stopped = False
|
||||
for command in commands:
|
||||
print(f'COMMAND {command.name} timeout={command.timeout:g}s argv={list(command.argv)!r}', flush=True)
|
||||
if dry_run:
|
||||
status, detail = 'PLAN', 'not executed; prerequisites not checked'
|
||||
elif stopped:
|
||||
status, detail = 'SKIP', 'earlier failure (fail-fast or failed build)'
|
||||
else:
|
||||
started = time.monotonic()
|
||||
try:
|
||||
status, detail = run_command(command, root, env)
|
||||
except KeyboardInterrupt:
|
||||
print(f'INTERRUPTED {command.name}; process group killed', flush=True)
|
||||
return 130
|
||||
detail += f' ({time.monotonic() - started:.1f}s)'
|
||||
if status != 'PASS' and (fail_fast or command.name == 'build'):
|
||||
stopped = True
|
||||
results.append((command.name, status, detail))
|
||||
print(f'{status} {command.name}: {detail}', flush=True)
|
||||
print('\nSUMMARY (command exits, not target validation):', flush=True)
|
||||
for name, status, detail in results:
|
||||
print(f'{status} {name}: {detail}', flush=True)
|
||||
return int(any(status not in ('PASS', 'PLAN') for _, status, _ in results))
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
options = parser().parse_args(argv)
|
||||
if not options.build:
|
||||
print('SKIP optional build: --build not requested; existing artifacts required', flush=True)
|
||||
if not options.interop:
|
||||
print('SKIP optional interop: --interop not requested', flush=True)
|
||||
return execute(plan(options), ROOT, dry_run=options.dry_run, fail_fast=options.fail_fast)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user