Integrate delivered HQ memory, finite physical convoy and RHS WCS dependencies

This commit is contained in:
2026-09-20 02:55:30 +02:00
parent e7b4141f69
commit a2aab17171
230 changed files with 37976 additions and 139 deletions
+148 -5
View File
@@ -20,6 +20,7 @@ import zipfile
from source_bundle import build, inventory, PROJECTS
from headless_game import assert_no_game, assert_prefix_idle, cleanup_owned_run
from native_report import analyze
from external_addons import policy as external_policy, installed as external_installed, prepare_profile
ROOT = Path(__file__).resolve().parents[1]
ORDER = tuple(PROJECTS)
@@ -56,6 +57,89 @@ def project_order(project):
return [name for name in ORDER if name in needed]
def external_request(project, source, root):
needs_external = project in {'ai', 'chat'}
if needs_external and source is None:
raise ValueError('AI and Chat builds require --external-addons with the audited installed RHS/WCS/BMP closure')
if source is not None and not needs_external:
raise ValueError('Core, Intel and Map builds must use vanilla-only profiles; --external-addons is not permitted')
if source is None:
return None
source = Path(source).resolve()
if not source.is_dir():
raise ValueError('External addon source directory missing')
policy_path = root/'docs/external-addons.json'
if policy_path.is_symlink() or not policy_path.is_file():
raise ValueError('External policy must be a regular repository file')
before = digest(policy_path)
value = external_policy(root)
if digest(policy_path) != before:
raise ValueError('External policy changed while reading')
return {'source': source, 'policy': value, 'policy_path': policy_path,
'policy_sha256': before}
def capture_external(context, session):
cache = {}
context['records'] = external_installed(context['source'], context['policy'], cache)
context['cache_path'] = session/'external-source-cache.json'
save(context['cache_path'], cache)
context['cache_sha256'] = digest(context['cache_path'])
save(session/'external-addons-policy.json', context['policy'])
save(session/'external-addons-before.json', context['records'])
verify_external(context)
def verify_external(context):
context['last_records'] = None
if context['cache_path'].is_symlink() or digest(context['cache_path']) != context['cache_sha256']:
raise ValueError('External fingerprint cache changed during package attempt')
if context['policy_path'].is_symlink() or digest(context['policy_path']) != context['policy_sha256']:
raise ValueError('External dependency policy changed during package attempt')
cache = json.loads(context['cache_path'].read_text())
current = external_installed(context['source'], context['policy'], cache)
context['last_records'] = current
if current != context['records']:
raise ValueError('External addon inputs changed during package attempt')
return current
def verify_package_profile(profile, steam, records):
addons = profile/'addons'
expected = {'ArmaReforger', *('ERRI_'+guid for guid in records)}
if addons.is_symlink() or not addons.is_dir() or addons.resolve() != addons:
raise ValueError('Private package addon directory changed')
names = {path.name for path in addons.iterdir()}
if names - {'saves'} != expected:
raise ValueError('Private package profile contains missing or unrelated addons')
saves = addons/'saves'
if saves.exists() or saves.is_symlink():
if saves.is_symlink() or not saves.is_dir():
raise ValueError('Unexpected private package saves entry')
targets = {'ArmaReforger': steam/'steamapps/common/Arma Reforger/addons/data'}
targets.update({'ERRI_'+guid: Path(entry['path']) for guid, entry in records.items()})
for name, target in targets.items():
link = addons/name
if not link.is_symlink() or link.resolve() != target.resolve():
raise ValueError('Private package dependency link changed: '+name)
def expose_external(project, profile, steam, context):
records = {}
if project in {'ai', 'chat'}:
if context is None:
raise ValueError('AI and Chat native packaging require audited external inputs')
verify_external(context)
records = prepare_profile(profile/'addons', context['source'], context['policy'], context['cache_path'])
if records != context['records']:
raise ValueError('External addon inputs changed before package launch')
elif context is not None:
raise ValueError('External links are forbidden in vanilla-only package profiles')
verify_package_profile(profile, steam, records)
return records
def validate_session(root, steam, compat, session):
if session.exists() or session.is_symlink():
raise ValueError('--session must not already exist; outputs are never reused')
@@ -215,7 +299,9 @@ def quiet_streams(application_id, sink, compat):
break
def run_project(steam, compat, session, project, timeout, authored, silent_workbench=False, exit_after_init=False):
def run_project(steam, compat, session, project, timeout, authored, silent_workbench=False, exit_after_init=False, *, external=None):
if (project in {'ai', 'chat'}) != (external is not None):
raise ValueError('Only AI/Chat package profiles require and permit audited external inputs')
command = command_for(steam, session, project, silent_workbench, exit_after_init)
proton = Path(command[command.index('--')+1])
executable = Path(command[command.index('--')+3])
@@ -225,6 +311,9 @@ def run_project(steam, compat, session, project, timeout, authored, silent_workb
for name in ('packages', 'cache', 'evidence'):
(session/name/project).mkdir(parents=True)
evidence = session/'evidence'/project
external_records = expose_external(project, profile, steam, external)
if external_records:
save(evidence/'external-addons-before.json', external_records)
separator = command.index('--')
native_command = command[separator+1:]
recorder = evidence/'native-child.py'
@@ -238,6 +327,9 @@ def run_project(steam, compat, session, project, timeout, authored, silent_workb
save(evidence/'launch.json', {'command': command, 'native_command': native_command, 'authored': authored, 'started_unix': time.time(),
'executable_sha256': digest(executable), 'proton_sha256': digest(proton),
'child_recorder_sha256': digest(recorder), 'native_environment_sha256': digest(environment_path),
'external_addon_count': len(external_records),
'external_policy_sha256': external['policy_sha256'] if external else None,
'external_addon_inputs': 'external-addons-before.json' if external else None,
'silent_workbench_experiment': silent_workbench,
'exit_after_init_experiment': exit_after_init})
run_id = uuid.uuid4().hex
@@ -327,10 +419,27 @@ def run_project(steam, compat, session, project, timeout, authored, silent_workb
except (OSError, ValueError) as error:
logs, packages, unchanged = {}, {}, False
errors = ['Output/evidence validation failed: '+str(error)]
profile_unchanged, external_unchanged = True, True
try:
verify_package_profile(profile, steam, external_records)
except (OSError, ValueError) as error:
profile_unchanged = False
errors.append('Private profile validation failed: '+str(error))
if external is not None:
try:
verify_external(external)
except (OSError, ValueError) as error:
external_unchanged = False
errors.append('External input validation failed: '+str(error))
if external_records:
save(evidence/'external-addons-after.json', {'records': external['last_records'], 'unchanged': external_unchanged})
save(evidence/'external-addons-verification.json', {
'unchanged': external_unchanged, 'profile_unchanged': profile_unchanged,
'policy_sha256': external['policy_sha256'], 'before': 'external-addons-before.json'})
native_logs = [name for name in logs if Path(name).name == 'console.log' and logs[name]['bytes'] > 0]
native_exit = child_status(status_path, native_command)
wrapper_ok = exit_code == 0 or (exit_code is None and result == 'native-child-exited')
candidate = wrapper_ok and candidate_ok(native_exit, result, cleanup_ok, unchanged, native_logs, errors, packages)
candidate = wrapper_ok and profile_unchanged and external_unchanged and candidate_ok(native_exit, result, cleanup_ok, unchanged, native_logs, errors, packages)
report = {'project': project, 'result': 'native-pack-candidate' if candidate else ('validation-failed' if result in {'process-exited', 'native-child-exited'} else result),
'candidate_created': candidate, 'native_exit_code': native_exit, 'launcher_exit_code': exit_code,
'completion_observed': result, 'display_cleanup_after_native_exit': result == 'native-child-exited',
@@ -339,6 +448,11 @@ def run_project(steam, compat, session, project, timeout, authored, silent_workb
'native_status_sha256': digest(status_path) if status_path.is_file() and not status_path.is_symlink() else None,
'cleanup_confirmed': cleanup_ok, 'cleanup_report': cleanup_report,
'copied_authored_unchanged': unchanged, 'runner_error': runner_error,
'profile_addons_unchanged': profile_unchanged,
'external_addons_unchanged_after_run': external_unchanged,
'external_addon_count': len(external_records),
'external_policy_sha256': external['policy_sha256'] if external else None,
'external_addon_inputs': 'external-addons-before.json' if external else None,
'logs': logs, 'errors': errors, 'packages': packages,
'package_contents_verified': False, 'native_consumption_verified': False,
'tool_completion_semantics_verified': False, 'release_qualified': False,
@@ -351,6 +465,7 @@ def run(a, root=ROOT):
root, steam, compat = root.resolve(), a.steam.resolve(), a.compat.resolve()
session = a.session.absolute() # refuse existing symlinks before resolve
validate_session(root, steam, compat, session)
external = external_request(a.project, getattr(a, 'external_addons', None), root)
if not 30 <= a.timeout <= 1800:
raise ValueError('--timeout must be 30..1800 seconds per addon')
silent_workbench = getattr(a, 'silent_workbench', False)
@@ -371,16 +486,30 @@ def run(a, root=ROOT):
raise ValueError('Another native runner owns the disposable prefix')
assert_prefix_idle(compat)
authored = copy_payload(root, session)
if external is not None:
capture_external(external, session)
reports = []
order = project_order(a.project)
continue_on_failure = getattr(a, 'continue_on_failure', False)
stopped_reason = None
for project in order:
report = run_project(steam, compat, session, project, a.timeout, authored, silent_workbench, exit_after_init)
selected_external = external if project in {'ai', 'chat'} else None
report = run_project(steam, compat, session, project, a.timeout, authored, silent_workbench, exit_after_init, external=selected_external)
reports.append(report)
# Diagnostic continuation never grants authority to reuse an unsafe
# prefix or a changed source copy. Timeout/interruption also stop.
if not report.get('cleanup_confirmed'):
if external is not None:
try:
verify_external(external)
except (OSError, ValueError) as error:
stopped_reason = 'external dependency inputs changed or unverifiable: '+str(error)
if stopped_reason:
break
if report.get('profile_addons_unchanged') is False:
stopped_reason = 'private addon profile changed or unverifiable'
elif selected_external is not None and report.get('external_addons_unchanged_after_run') is not True:
stopped_reason = 'external dependency inputs changed or unverifiable'
elif not report.get('cleanup_confirmed'):
stopped_reason = 'owned cleanup unconfirmed'
elif report.get('runner_error'):
stopped_reason = 'native runner error'
@@ -404,14 +533,27 @@ def run(a, root=ROOT):
original_unchanged = original_after == authored
except (OSError, ValueError):
original_unchanged = False
external_unchanged = True
if external is not None:
try:
verify_external(external)
except (OSError, ValueError) as error:
external_unchanged = False
if stopped_reason is None:
stopped_reason = 'external dependency final verification failed: '+str(error)
save(session/'external-addons-after.json', {'records': external['last_records'], 'unchanged': external_unchanged})
summary = {'kind': 'UNQUALIFIED NATIVE PACKAGE ATTEMPT', 'session': str(session),
'project_order': order, 'projects': reports,
'continue_on_failure': continue_on_failure, 'stopped_reason': stopped_reason,
'skipped_projects': order[len(reports):],
'source_repository_unchanged': original_unchanged,
'external_addons_unchanged_after_run': external_unchanged,
'external_addon_count': len(external['records']) if external else 0,
'external_addon_inputs': 'external-addons-before.json' if external else None,
'external_policy_sha256': external['policy_sha256'] if external else None,
'release_qualified': False, 'published': False,
'qualification': 'PENDING native contents inspection and packed-only consumption'}
summary['all_candidates_created'] = original_unchanged and stopped_reason is None and len(reports) == len(order) and all(r['candidate_created'] for r in reports)
summary['all_candidates_created'] = original_unchanged and external_unchanged and stopped_reason is None and len(reports) == len(order) and all(r['candidate_created'] for r in reports)
save(session/'package-report.json', summary)
print(json.dumps(summary, indent=2))
return 0 if summary['all_candidates_created'] else 1
@@ -424,6 +566,7 @@ def main(argv=None):
parser.add_argument('--compat', type=Path, required=True, help='Idle disposable cloned Tools prefix under /tmp')
parser.add_argument('--steam', type=Path, default=Path.home()/'.local/share/Steam')
parser.add_argument('--timeout', type=int, default=300, help='30..1800 seconds per addon')
parser.add_argument('--external-addons', type=Path, help='Required for AI/Chat: installed audited RHS/WCS/BMP root; only allowlisted links enter private profiles')
lifecycle = parser.add_mutually_exclusive_group()
lifecycle.add_argument('--silent-workbench', action='store_true', help='Opt-in documented -wbSilent experiment; packing compatibility still requires native evidence')
lifecycle.add_argument('--exit-after-init', action='store_true', help='Opt-in documented -exitAfterInit experiment; natural exit and all native error gates remain mandatory')