diff --git a/qualification/issue-184/tool-acquisition/184-acquire-tools-v5-adapter-controls-result.json b/qualification/issue-184/tool-acquisition/184-acquire-tools-v5-adapter-controls-result.json new file mode 100644 index 0000000..40d1056 --- /dev/null +++ b/qualification/issue-184/tool-acquisition/184-acquire-tools-v5-adapter-controls-result.json @@ -0,0 +1,77 @@ +{ + "cases": 16, + "downloaded_wheel_imports_or_installs": 0, + "failed": 0, + "guardian_sha256": "032a2da74a26ea8ed036428d5470420dd49f099b7fe61cfcf4d7ab216d720066", + "owner_sha256": "234e099b5208aaafca379d3e5535cf44b3158a327764eec0ef4e12afcfc45ffb", + "passed": 16, + "real_network_requests": 0, + "results": [ + { + "id": "adopt-preallocated-nodes-write-with-same-inodes-seal", + "outcome": "pass" + }, + { + "id": "network-requires-independent-guardian-before-inputs", + "outcome": "pass" + }, + { + "id": "missing-readiness-refused", + "outcome": "pass" + }, + { + "id": "wrong-root-path-refused", + "outcome": "pass" + }, + { + "id": "changed-root-inode-refused", + "outcome": "pass" + }, + { + "id": "changed-wheelhouse-inode-refused", + "outcome": "pass" + }, + { + "id": "incomplete-descriptor-set-refused", + "outcome": "pass" + }, + { + "id": "duplicate-descriptors-refused", + "outcome": "pass" + }, + { + "id": "expired-anchor-refused", + "outcome": "pass" + }, + { + "id": "changed-wall-clock-anchor-refused", + "outcome": "pass" + }, + { + "id": "duplicate-adoption-refused", + "outcome": "pass" + }, + { + "id": "selected-six-filename-closure-before-directory-creation", + "outcome": "pass" + }, + { + "id": "guarded-fdopen-failure-closes-duplicate-cleans-used-and-unused", + "outcome": "pass" + }, + { + "id": "late-failure-cleans-earlier-and-unused-preallocations", + "outcome": "pass" + }, + { + "id": "unknown-replacement-inode-never-written", + "outcome": "pass" + }, + { + "id": "foreign-fd-never-adopted-or-written", + "outcome": "pass" + } + ], + "scope": "synthetic owner adapter, not integrated acquisition or launcher", + "terminal_acceptance_and_exact_launcher_pending": true +} diff --git a/qualification/issue-184/tool-acquisition/184-acquire-tools-v5-adapter-controls.py b/qualification/issue-184/tool-acquisition/184-acquire-tools-v5-adapter-controls.py new file mode 100644 index 0000000..9ed39fa --- /dev/null +++ b/qualification/issue-184/tool-acquisition/184-acquire-tools-v5-adapter-controls.py @@ -0,0 +1,206 @@ +"""Synthetic preallocated-pool adapter controls; no acquisition or tool execution.""" +from __future__ import annotations + +from contextlib import contextmanager +import datetime as dt +import hashlib +import importlib.util +import json +import os +from pathlib import Path +import socket +import sys +import tempfile +import time +from unittest.mock import patch + +sys.dont_write_bytecode=True +BASE=Path('/tmp/sdk-python-handoff') +REF='https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-9999999999' + + +def load(name,path): + spec=importlib.util.spec_from_file_location(name,path) + module=importlib.util.module_from_spec(spec);spec.loader.exec_module(module) + return module + + +owner=load('trusted_v5_owner_adapter',BASE/'184-acquire-tools-v5.py') +guard=load('trusted_reviewed_guard_custody',BASE/'184-acquisition-custody-guardian-v7-proposal.py') +assert hashlib.sha256((BASE/'184-acquisition-custody-guardian-v7-proposal.py').read_bytes()).hexdigest()== \ + '032a2da74a26ea8ed036428d5470420dd49f099b7fe61cfcf4d7ab216d720066' +results=[] + + +def check(name,operation): + try:operation() + except BaseException as error: + results.append({'id':name,'outcome':'fail','type':type(error).__name__,'detail':str(error)[:160]}) + else:results.append({'id':name,'outcome':'pass'}) + + +def refuses(operation,code): + try:operation() + except owner.Refusal as error:assert error.code==code,(error.code,code) + else:raise AssertionError('required refusal absent: '+code) + + +@contextmanager +def fixture(): + with tempfile.TemporaryDirectory(prefix='sdk-python-184-adapter-controls-') as temporary: + parent=Path(temporary) + custody=guard.ArchiveCustody(parent,'sdk-python-184-tools-guardian-14adc40b-9999999999',['one.whl','two.whl']) + ids={'.':list(custody.root_id),'wheelhouse':list(custody.wheelhouse_id)} + for name in ('metadata','events'): + (custody.path/name).mkdir(mode=0o700) + st=(custody.path/name).stat();ids[name]=[st.st_dev,st.st_ino] + borrowed={name:custody.duplicate_for_owner(name) for name in custody.archive_nodes} + proof={'pool':str(custody.path),'ready':True,'started_monotonic':time.monotonic(), + 'started_utc':dt.datetime.now(dt.timezone.utc).isoformat(), + 'dir_ids':ids, + 'archive_fds':dict(borrowed)} + with patch.object(owner,'POOL_BASE',parent):item=owner.Acquisition(REF) + try:yield item,custody,proof + finally: + for directory in (custody.path,custody.path/'wheelhouse',custody.path/'metadata',custody.path/'events'): + if directory.exists() and not directory.is_symlink():directory.chmod(0o700) + for fd in borrowed.values():os.close(fd) + custody.close() + + +def prepare(item,proof): + item.attach_guarded_pool(proof) + item.prepare_pool([{'filename':'one.whl'},{'filename':'two.whl'}]) + + +def positive_handoff_write_seal(): + with fixture() as (item,custody,proof): + prepare(item,proof) + assert item.created and item.guard_attached and item.archive_opened==2 + for name,raw in [('one.whl',b'first synthetic bytes'),('two.whl',b'second synthetic bytes')]: + identity=custody.archive_nodes[name]['identity'] + item.write('wheelhouse/'+name,raw) + assert (custody.node(name).st_dev,custody.node(name).st_ino)==identity + assert os.pread(custody.archive_nodes[name]['fd'],len(raw),0)==raw + assert item.archive_opened==item.archive_written==2 + assert len(item.check_pool())==2 + item.seal();assert len(item.check_pool())==2 + + +def wrong_proof(change,code): + with fixture() as (item,custody,proof): + change(proof) + refuses(lambda:item.attach_guarded_pool(proof),code) + assert not item.guard_attached and not item.requests + assert all(custody.node(name).st_size==0 for name in custody.archive_nodes) + + +def network_without_guard_refused(): + with fixture() as (item,custody,proof), \ + patch.object(owner,'VERIFIED_SOURCE_SHA256','0'*64),patch.dict(os.environ,owner.MINIMAL_ENV,clear=True), \ + patch.object(owner,'read_regular',side_effect=AssertionError('must refuse before reading inputs')): + refuses(item.run,'INDEPENDENT_GUARDIAN_CUSTODY_REQUIRED_BEFORE_NETWORK') + assert not item.requests + + +def duplicate_attach_refused(): + with fixture() as (item,custody,proof): + item.attach_guarded_pool(proof) + refuses(lambda:item.attach_guarded_pool(proof),'GUARDIAN_ALREADY_ATTACHED') + + +def full_filename_set_required(): + with fixture() as (item,custody,proof): + item.attach_guarded_pool(proof) + refuses(lambda:item.prepare_pool([{'filename':'one.whl'}]),'COMPLETE_GUARDIAN_ARCHIVE_CUSTODY_REQUIRED') + assert not list((item.pool/'metadata').iterdir()) + + +def fdopen_failure_closes_duplicate_cleans_all_preallocated(): + with fixture() as (item,custody,proof): + prepare(item,proof) + duplicate=[] + def refuse_wrap(fd,mode):duplicate.append(fd);raise OSError('synthetic wrap failure') + with patch.object(owner.os,'fdopen',side_effect=refuse_wrap): + try:item.write('wheelhouse/one.whl',b'partial') + except OSError:pass + else:raise AssertionError('fdopen failure not exercised') + assert len(duplicate)==1 + try:os.fstat(duplicate[0]) + except OSError:pass + else:raise AssertionError('duplicate fd remains open') + result=item.failure_result(OSError('synthetic wrap failure')) + assert result['archive_cleanup']['complete'] and not list((item.pool/'wheelhouse').iterdir()) + assert result['archive_nodes']=={'opened':2,'written':0,'removed':2,'remaining_owned':0} + assert custody.cleanup_archives()['complete'] + + +def late_failure_cleans_unused_preallocations(): + with fixture() as (item,custody,proof): + prepare(item,proof);item.write('wheelhouse/one.whl',b'first synthetic bytes') + result=item.failure_result(owner.Refusal('synthetic late refusal')) + assert result['archive_cleanup']['complete'] and result['archive_nodes']['removed']==2 + assert not list((item.pool/'wheelhouse').iterdir()) and custody.cleanup_archives()['complete'] + + +def replaced_archive_not_written(): + with fixture() as (item,custody,proof): + prepare(item,proof) + path=item.pool/'wheelhouse/one.whl' + path.rename(item.pool/'retained-original') + path.write_bytes(b'unknown node') + refuses(lambda:item.write('wheelhouse/one.whl',b'never written'), + 'GUARDED_ARCHIVE_NAME_CHANGED_BEFORE_WRITE') + assert path.read_bytes()==b'unknown node' + assert os.fstat(custody.archive_nodes['one.whl']['fd']).st_size==0 + + +def wrong_descriptor_refused(): + with fixture() as (item,custody,proof): + foreign=item.pool.parent/'foreign' + foreign.write_bytes(b'foreign untouched') + fd=os.open(foreign,os.O_RDONLY) + try: + proof['archive_fds']['one.whl']=fd + refuses(lambda:item.attach_guarded_pool(proof),'GUARDIAN_ARCHIVE_DESCRIPTOR_IDENTITY_CHANGED') + assert foreign.read_bytes()==b'foreign untouched' + finally:os.close(fd) + + +with patch.object(socket,'create_connection',side_effect=AssertionError('NO_NETWORK_IN_ADAPTER_CONTROLS')): + check('adopt-preallocated-nodes-write-with-same-inodes-seal',positive_handoff_write_seal) + check('network-requires-independent-guardian-before-inputs',network_without_guard_refused) + check('missing-readiness-refused',lambda:wrong_proof(lambda p:p.update(ready=False),'GUARDIAN_POOL_PROOF_MISMATCH')) + check('wrong-root-path-refused',lambda:wrong_proof(lambda p:p.update(pool='/synthetic/foreign'),'GUARDIAN_POOL_PROOF_MISMATCH')) + check('changed-root-inode-refused',lambda:wrong_proof(lambda p:p['dir_ids']['.'].__setitem__(1,0),'GUARDIAN_DIRECTORY_IDENTITY_CHANGED')) + check('changed-wheelhouse-inode-refused',lambda:wrong_proof(lambda p:p['dir_ids']['wheelhouse'].__setitem__(1,0),'GUARDIAN_DIRECTORY_IDENTITY_CHANGED')) + check('incomplete-descriptor-set-refused',lambda:wrong_proof(lambda p:p['archive_fds'].pop('one.whl'),'GUARDIAN_ARCHIVE_SET_CHANGED')) + check('duplicate-descriptors-refused',lambda:wrong_proof(lambda p:p['archive_fds'].update({'two.whl':p['archive_fds']['one.whl']}),'GUARDIAN_DESCRIPTOR_SET_INVALID')) + check('expired-anchor-refused',lambda:wrong_proof(lambda p:p.update(started_monotonic=time.monotonic()-271),'GUARDIAN_START_DEADLINE_INVALID')) + check('changed-wall-clock-anchor-refused',lambda:wrong_proof(lambda p:p.update(started_utc='2020-01-01T00:00:00+00:00'),'GUARDIAN_WALL_CLOCK_ANCHOR_CHANGED')) + check('duplicate-adoption-refused',duplicate_attach_refused) + check('selected-six-filename-closure-before-directory-creation',full_filename_set_required) + check('guarded-fdopen-failure-closes-duplicate-cleans-used-and-unused',fdopen_failure_closes_duplicate_cleans_all_preallocated) + check('late-failure-cleans-earlier-and-unused-preallocations',late_failure_cleans_unused_preallocations) + check('unknown-replacement-inode-never-written',replaced_archive_not_written) + check('foreign-fd-never-adopted-or-written',wrong_descriptor_refused) + +report={'scope':'synthetic owner adapter, not integrated acquisition or launcher', + 'owner_sha256':hashlib.sha256((BASE/'184-acquire-tools-v5.py').read_bytes()).hexdigest(), + 'guardian_sha256':hashlib.sha256((BASE/'184-acquisition-custody-guardian-v7-proposal.py').read_bytes()).hexdigest(), + 'cases':len(results),'passed':sum(row['outcome']=='pass' for row in results), + 'failed':sum(row['outcome']=='fail' for row in results),'results':results, + 'real_network_requests':0,'downloaded_wheel_imports_or_installs':0, + 'terminal_acceptance_and_exact_launcher_pending':True} +report_path=BASE/'184-acquire-tools-v5-adapter-controls-result.json' +report_bytes=(json.dumps(report,sort_keys=True,indent=2)+'\n').encode() +if report_path.exists(): + if report_path.read_bytes()!=report_bytes: + print(json.dumps(report,indent=2)) + raise SystemExit('CONTROL_RESULT_COLLISION; prior record preserved') +else: + descriptor=os.open(report_path,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o400) + with os.fdopen(descriptor,'wb') as stream: + assert stream.write(report_bytes)==len(report_bytes) +print(json.dumps(report,indent=2)) +raise SystemExit(bool(report['failed'])) diff --git a/qualification/issue-184/tool-acquisition/184-acquire-tools-v5-bootstrap.py b/qualification/issue-184/tool-acquisition/184-acquire-tools-v5-bootstrap.py new file mode 100644 index 0000000..33effec --- /dev/null +++ b/qualification/issue-184/tool-acquisition/184-acquire-tools-v5-bootstrap.py @@ -0,0 +1,61 @@ +"""Unexecuted reviewed-source bootstrap; a new General decision is still required. + +For any future admission, these exact bytes are the inline python -I - stdin +bootstrap. The syntax guard below does not itself grant authorization. +""" +from __future__ import annotations + +import hashlib +import os +from pathlib import Path +import re +import stat +from types import ModuleType + +OWNER_PATH = Path('/tmp/sdk-python-handoff/184-acquire-tools-v5.py') +OWNER_SHA = '234e099b5208aaafca379d3e5535cf44b3158a327764eec0ef4e12afcfc45ffb' +OLD_ADMISSION = 'https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-6649046802' +MINIMAL_ENV = {'PATH': '/usr/bin:/bin', 'LANG': 'C.UTF-8', 'LC_ALL': 'C.UTF-8', 'TZ': 'UTC'} + + +def require(value: bool, code: str) -> None: + if not value: + raise ValueError(code) + + +def stamp(value: os.stat_result) -> tuple: + return (value.st_dev, value.st_ino, value.st_mode, value.st_nlink, value.st_uid, + value.st_size, value.st_mtime_ns, value.st_ctime_ns) + + +def verified_source(path: Path, expected: str) -> tuple[bytes, tuple]: + descriptor = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK) + with os.fdopen(descriptor, 'rb') as stream: + before = os.fstat(stream.fileno()) + require(stat.S_ISREG(before.st_mode) and before.st_uid == os.geteuid() + and before.st_nlink == 1 and before.st_size <= 65536, 'BOOTSTRAP_SOURCE_TYPE_OR_SIZE') + raw = stream.read(65537) + require(stamp(before) == stamp(os.fstat(stream.fileno())), 'BOOTSTRAP_SOURCE_CHANGED_WHILE_READING') + require(hashlib.sha256(raw).hexdigest() == expected, 'BOOTSTRAP_SOURCE_HASH_MISMATCH') + require(stamp(os.stat(path, follow_symlinks=False)) == stamp(before), 'BOOTSTRAP_SOURCE_PATH_CHANGED') + return raw, stamp(before) + + +def launch(admission: str, proof: dict | None = None) -> dict: + require(bool(re.fullmatch(r'https://github\.com/durable-workflow/sdk-python/issues/184#issuecomment-[1-9][0-9]{5,17}', admission)) + and admission != OLD_ADMISSION, 'NEW_GENERAL_ADMISSION_REQUIRED_OLD_ATTEMPT_CANNOT_RETRY') + os.environ.clear() + os.environ.update(MINIMAL_ENV) + raw, identity = verified_source(OWNER_PATH, OWNER_SHA) + code = compile(raw, '', 'exec') + require(stamp(os.stat(OWNER_PATH, follow_symlinks=False)) == identity, 'BOOTSTRAP_SOURCE_PATH_CHANGED') + owner = ModuleType('sdk_python_verified_data_owner_v5') + owner.__file__ = str(OWNER_PATH) + owner.VERIFIED_SOURCE_SHA256 = OWNER_SHA + exec(code, owner.__dict__) + require(stamp(os.stat(OWNER_PATH, follow_symlinks=False)) == identity, 'BOOTSTRAP_SOURCE_PATH_CHANGED') + return owner.run_guarded_once(admission, proof or {}) + + +if __name__ == '__main__': + raise SystemExit('UNEXECUTED_OWNER_ROLE_ONLY; independent guardian proof and reviewed launcher required') diff --git a/qualification/issue-184/tool-acquisition/184-acquire-tools-v5-controls-result.json b/qualification/issue-184/tool-acquisition/184-acquire-tools-v5-controls-result.json new file mode 100644 index 0000000..c5ed060 --- /dev/null +++ b/qualification/issue-184/tool-acquisition/184-acquire-tools-v5-controls-result.json @@ -0,0 +1,359 @@ +{ + "actual_failed_archive_cause_established": false, + "bootstrap_sha256": "6ca5da88054b4c3ea27b2614dfd987f88ecb3db981271a995bb9941d06398fed", + "cases": 86, + "downloaded_wheel_imports_or_installs": 0, + "failed": 0, + "original_source_sha256": "e0b8955c8c26c68333e1439c4eb195ef803d710d68f4a4b297b3dd17fcfd7c54", + "passed": 86, + "real_network_requests": 0, + "results": [ + { + "id": "old-refuses-equivalent-marker-quotes-new-accepts", + "outcome": "pass" + }, + { + "id": "derived-authority-tuple-build", + "outcome": "pass" + }, + { + "id": "authority-derived-synthetic-headers-build", + "outcome": "pass" + }, + { + "id": "derived-authority-tuple-packaging", + "outcome": "pass" + }, + { + "id": "authority-derived-synthetic-headers-packaging", + "outcome": "pass" + }, + { + "id": "derived-authority-tuple-pip", + "outcome": "pass" + }, + { + "id": "authority-derived-synthetic-headers-pip", + "outcome": "pass" + }, + { + "id": "derived-authority-tuple-pyproject-hooks", + "outcome": "pass" + }, + { + "id": "authority-derived-synthetic-headers-pyproject-hooks", + "outcome": "pass" + }, + { + "id": "derived-authority-tuple-setuptools", + "outcome": "pass" + }, + { + "id": "authority-derived-synthetic-headers-setuptools", + "outcome": "pass" + }, + { + "id": "derived-authority-tuple-wheel", + "outcome": "pass" + }, + { + "id": "authority-derived-synthetic-headers-wheel", + "outcome": "pass" + }, + { + "id": "metadata-refuses-version", + "outcome": "pass" + }, + { + "id": "metadata-refuses-project", + "outcome": "pass" + }, + { + "id": "metadata-refuses-python", + "outcome": "pass" + }, + { + "id": "metadata-refuses-URL", + "outcome": "pass" + }, + { + "id": "metadata-refuses-size", + "outcome": "pass" + }, + { + "id": "metadata-refuses-bool-size", + "outcome": "pass" + }, + { + "id": "metadata-refuses-hash", + "outcome": "pass" + }, + { + "id": "metadata-refuses-yanked", + "outcome": "pass" + }, + { + "id": "metadata-refuses-unknown-yank", + "outcome": "pass" + }, + { + "id": "metadata-refuses-string-yank", + "outcome": "pass" + }, + { + "id": "metadata-refuses-duplicate", + "outcome": "pass" + }, + { + "id": "single-request-receipt-and-body-hash", + "outcome": "pass" + }, + { + "id": "oversize-header-stable-refusal", + "outcome": "pass" + }, + { + "id": "encoded-response-stable-refusal", + "outcome": "pass" + }, + { + "id": "changed-response-URL", + "outcome": "pass" + }, + { + "id": "archive-missing-length", + "outcome": "pass" + }, + { + "id": "archive-changed-length", + "outcome": "pass" + }, + { + "id": "partial-body-count-hash-refusal-ledger", + "outcome": "pass" + }, + { + "id": "diagnostic-failure-preserves-primary-code", + "outcome": "pass" + }, + { + "id": "zero-archive-after-semantic-refusal", + "outcome": "pass" + }, + { + "id": "hash-refusal-does-not-retain-unverified-candidate", + "outcome": "pass" + }, + { + "id": "late-refusal-removes-all-new-archives", + "outcome": "pass" + }, + { + "id": "malformed-ZIP-removes-new-archive", + "outcome": "pass" + }, + { + "id": "special-ZIP-member-removes-new-archive", + "outcome": "pass" + }, + { + "id": "partial-archive-write-tracked-and-removed", + "outcome": "pass" + }, + { + "id": "failure-record-collision-preserves-primary-and-zero-archives", + "outcome": "pass" + }, + { + "id": "unknown-failure-record-collision-preserved-and-zero-archives", + "outcome": "pass" + }, + { + "id": "v2-retains-semantic-refused-archive-v3-removes-countercontrol", + "outcome": "pass" + }, + { + "id": "redirect-refusal-ledger-and-zero-archives", + "outcome": "pass" + }, + { + "id": "v3-fdopen-leaks-archive-and-fd-v4-closes-and-removes", + "outcome": "pass" + }, + { + "id": "v3-bootstrap-inherits-sentinels-v4-closed-owner-env", + "outcome": "pass" + }, + { + "id": "exact-reviewed-CA-reused-without-child-changed-bytes-refuse", + "outcome": "pass" + }, + { + "id": "guarded-owner-has-no-child-launch-surface", + "outcome": "pass" + }, + { + "id": "owner-refuses-ambient-env-before-pool-or-network", + "outcome": "pass" + }, + { + "id": "cleanup-identity-collision-preserves-unknown-node-nonpass", + "outcome": "pass" + }, + { + "id": "cleanup-directory-collision-preserves-unknown-tree-nonpass", + "outcome": "pass" + }, + { + "id": "uncreated-pool-collision-never-mutates-existing-pool", + "outcome": "pass" + }, + { + "id": "transport-refusal-zero-archives-missing-Length", + "outcome": "pass" + }, + { + "id": "transport-refusal-zero-archives-changed-Length", + "outcome": "pass" + }, + { + "id": "transport-refusal-zero-archives-final-URL", + "outcome": "pass" + }, + { + "id": "transport-refusal-zero-archives-encoding", + "outcome": "pass" + }, + { + "id": "transport-refusal-zero-archives-timeout", + "outcome": "pass" + }, + { + "id": "wrong-size-removes-earlier-new-archive", + "outcome": "pass" + }, + { + "id": "wrong-hash-removes-earlier-new-archive", + "outcome": "pass" + }, + { + "id": "changed-active-graph-refuses", + "outcome": "pass" + }, + { + "id": "unsafe-ZIP-path-refuses", + "outcome": "pass" + }, + { + "id": "parent-output-refuses", + "outcome": "pass" + }, + { + "id": "changed-directory-inode-refuses", + "outcome": "pass" + }, + { + "id": "exclusive-output-collision-refuses", + "outcome": "pass" + }, + { + "id": "failure-record-reserve-survives-pool-bound", + "outcome": "pass" + }, + { + "id": "empty-extra-directory-refuses", + "outcome": "pass" + }, + { + "id": "written-file-mutation-refuses", + "outcome": "pass" + }, + { + "id": "complete-synthetic-pool-custody-and-seal", + "outcome": "pass" + }, + { + "id": "old-admission-cannot-retry", + "outcome": "pass" + }, + { + "id": "missing-hash-bootstrap-refuses-before-pool", + "outcome": "pass" + }, + { + "id": "HTTP-scheme-refuses-before-request", + "outcome": "pass" + }, + { + "id": "foreign-origin-refuses-before-request", + "outcome": "pass" + }, + { + "id": "URL-userinfo-refuses-before-request", + "outcome": "pass" + }, + { + "id": "redirect-status-custody-no-second-request", + "outcome": "pass" + }, + { + "id": "HTTP-error-status-custody", + "outcome": "pass" + }, + { + "id": "body-shorter-than-header-refuses", + "outcome": "pass" + }, + { + "id": "bootstrap-validates-owned-bytes-without-execution", + "outcome": "pass" + }, + { + "id": "bootstrap-changed-source-refuses", + "outcome": "pass" + }, + { + "id": "bootstrap-symlink-source-refuses", + "outcome": "pass" + }, + { + "id": "bootstrap-shared-inode-source-refuses", + "outcome": "pass" + }, + { + "id": "bootstrap-old-admission-refuses-before-source-read", + "outcome": "pass" + }, + { + "id": "marker-numeric-python-order", + "outcome": "pass" + }, + { + "id": "marker-old-Python-dependency-excluded", + "outcome": "pass" + }, + { + "id": "marker-Windows-dependency-excluded", + "outcome": "pass" + }, + { + "id": "marker-extra-dependency-excluded", + "outcome": "pass" + }, + { + "id": "marker-code-is-not-executed", + "outcome": "pass" + }, + { + "id": "patch-sensitive-marker-refuses", + "outcome": "pass" + }, + { + "id": "unsupported-marker-variable-refuses", + "outcome": "pass" + } + ], + "scope": "synthetic source controls, not archive acquisition/SDK/runtime qualification", + "simulated_transport_calls": 17, + "successor_source_sha256": "234e099b5208aaafca379d3e5535cf44b3158a327764eec0ef4e12afcfc45ffb" +} diff --git a/qualification/issue-184/tool-acquisition/184-acquire-tools-v5-controls.py b/qualification/issue-184/tool-acquisition/184-acquire-tools-v5-controls.py new file mode 100644 index 0000000..12674ce --- /dev/null +++ b/qualification/issue-184/tool-acquisition/184-acquire-tools-v5-controls.py @@ -0,0 +1,810 @@ +"""Synthetic source-only controls: no network, selected archives or tool imports.""" +from __future__ import annotations + +from contextlib import contextmanager +from copy import deepcopy +import hashlib +import ast +import importlib.util +import io +import json +import os +from pathlib import Path +import socket +import sys +import subprocess +import tempfile +from types import ModuleType +import urllib.error +import urllib.request +from unittest.mock import patch +import zipfile + +sys.dont_write_bytecode = True +BASE = Path('/tmp/sdk-python-handoff') +FAILED = BASE / 'control-inputs' +NEW_REF = 'https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-9999999999' + + +def load(name, path): + if path.suffix=='.source': + manifest=json.loads((path.parent/'manifest.json').read_text()) + expected=next(row['sha256'] for row in manifest['records'] if row['file']==path.name) + raw=path.read_bytes();assert hashlib.sha256(raw).hexdigest()==expected + result=ModuleType(name);result.__file__=str(path) + exec(compile(raw,str(path),'exec'),result.__dict__) + return result + spec = importlib.util.spec_from_file_location(name, path) + result = importlib.util.module_from_spec(spec) + spec.loader.exec_module(result) + return result + + +HISTORICAL = FAILED / 'historical-source' +old = load('original_acquisition_source_only', HISTORICAL / 'original-owner.source') +new = load('successor_acquisition_source_only', BASE / '184-acquire-tools-v5.py') +bootstrap = load('successor_bootstrap_source_only', BASE / '184-acquire-tools-v5-bootstrap.py') +assert hashlib.sha256((HISTORICAL / 'original-owner.source').read_bytes()).hexdigest() \ + == 'e0b8955c8c26c68333e1439c4eb195ef803d710d68f4a4b297b3dd17fcfd7c54' +assert hashlib.sha256((BASE / '184-acquire-tools-v5.py').read_bytes()).hexdigest() == bootstrap.OWNER_SHA +selection = json.loads((FAILED / 'selection.json').read_text()) +tools = {row['name']: row for row in selection['tools']} +authorities = {name: json.loads((FAILED / 'metadata' / (name + '.json')).read_text()) for name in tools} +results = [] +fake_calls = 0 + + +def check(label, body): + try: + body() + except BaseException as error: + results.append({'id': label, 'outcome': 'fail', 'error': type(error).__name__, 'detail': str(error)[:240]}) + else: + results.append({'id': label, 'outcome': 'pass'}) + + +def refuses(body, code): + try: + body() + except new.Refusal as error: + assert error.code == code, (error.code, code) + else: + raise AssertionError('expected refusal ' + code) + + +def synthetic_wheel(name='build', *, single_quotes=False, requirements=None, change=None, nested=False): + tool = deepcopy(tools[name]) + authority = deepcopy(authorities[name]) + reqs = list(authority['info'].get('requires_dist') or []) if requirements is None else requirements + headers = ['Metadata-Version: 2.4', 'Name: ' + tool['name'], 'Version: ' + tool['version'], + 'Requires-Python: ' + tool['requires_python']] + headers += ['Requires-Dist: ' + (row.replace('"', "'") if single_quotes else row) for row in reqs] + metadata = '\n'.join(headers) + '\n\nSynthetic header-only ZIP; not an acquired tool archive.' + wheel = 'Wheel-Version: 1.0\nRoot-Is-Purelib: true\nTag: py3-none-any\n' + members = {tool['name'].replace('-', '_') + '-' + tool['version'] + '.dist-info/METADATA': metadata, + tool['name'].replace('-', '_') + '-' + tool['version'] + '.dist-info/WHEEL': wheel} + if nested: + members['build/_vendor/other-1.dist-info/METADATA'] = 'Name: other\nVersion: 1\n' + if change: + change(members, tool, authority) + stream = io.BytesIO() + with zipfile.ZipFile(stream, 'w') as archive: + for path, text in members.items(): + archive.writestr(path, text) + raw = stream.getvalue() + tool['bytes'], tool['sha256'] = len(raw), hashlib.sha256(raw).hexdigest() + return raw, tool, authority + + +@contextmanager +def owner(): + with tempfile.TemporaryDirectory(prefix='sdk-python-184-source-controls-') as temporary: + item = new.Acquisition(NEW_REF) + item.pool = Path(temporary) / 'synthetic-pool' + item.pool.mkdir() + item.created = True + for name in ('metadata', 'wheelhouse', 'events'): + (item.pool / name).mkdir() + for name in ('.', 'metadata', 'wheelhouse', 'events'): + st = (item.pool if name == '.' else item.pool / name).stat() + item.dir_ids[name] = (st.st_dev, st.st_ino) + try: + yield item + finally: + item.pool.chmod(0o700) + for name in ('metadata', 'wheelhouse', 'events'): + (item.pool / name).chmod(0o700) + + +class Response: + def __init__(self, body=b'{}', *, status=200, length='auto', encoding='identity', url=None, partial=False): + self.status = status + self.headers = {'Content-Encoding': encoding} + if length is not None: + self.headers['Content-Length'] = str(len(body)) if length == 'auto' else length + self.body, self.url, self.partial = io.BytesIO(body), url, partial + self.reads = 0 + def __enter__(self): return self + def __exit__(self, *args): self.body.close() + def geturl(self): return self.url + def read(self, count): + self.reads += 1 + if self.partial and self.reads > 1: + raise TimeoutError('synthetic only') + return self.body.read(count) + + +class Opener: + def __init__(self, response=None, error=None): + self.response, self.error, self.calls = response, error, 0 + def open(self, request, timeout): + global fake_calls + self.calls += 1 + fake_calls += 1 + if self.error: + raise self.error + if self.response.url is None: + self.response.url = request.full_url + return self.response + + +def old_quote_control(): + raw, tool, authority = synthetic_wheel(single_quotes=True) + try: + old.Acquisition.__new__(old.Acquisition).inspect_wheel(raw, tool, authority) + except ValueError as error: + assert str(error) == 'wheel dependency metadata mismatch' + else: + raise AssertionError('original did not expose the equivalent-marker defect') + with owner() as item: + result = item.inspect_wheel(raw, tool, authority) + assert result['version'] == tool['version'] + + +def metadata_mutation(mutator, code): + authority = deepcopy(authorities['build']) + mutator(authority) + refuses(lambda: new.validate_authority(authority, tools['build']), code) + + +def transport_control(response, code=None, *, kind='metadata', size=None): + with owner() as item: + item.opener = Opener(response) + url = tools['build']['metadata_source' if kind == 'metadata' else 'url'] + if code: + refuses(lambda: item.get(url, 64, kind, size), code) + record = json.loads((item.pool / 'events/01-refusal.json').read_text()) + assert record['refusal_code'] == code and item.opener.calls == 1 + else: + assert item.get(url, 64, kind, size) == b'{}' + assert json.loads((item.pool / 'events/01-complete.json').read_text())['body_sha256'] \ + == hashlib.sha256(b'{}').hexdigest() + assert item.opener.calls == 1 + + +def body_removed_after_semantic_refusal(): + def changed(members, tool, authority): + key = next(key for key in members if key.endswith('/METADATA')) + members[key] = members[key].replace('Version: 1.6.1', 'Version: 9.9') + raw, tool, authority = synthetic_wheel(change=changed) + with owner() as item: + refuses(lambda: item.retain_and_inspect(raw, tool, authority), 'WHEEL_NAME_VERSION_MISMATCH') + saved = item.pool / 'wheelhouse' / tool['filename'] + assert not saved.exists() and not item.archive_nodes + assert item.archive_used == 0 and item.archive_removed == 1 + assert item.archive_facts[-1]['zip_started'] and not item.archive_facts[-1]['zip_complete'] + + +def bad_hash_not_retained(): + raw, tool, authority = synthetic_wheel() + tool['sha256'] = '0' * 64 + with owner() as item: + refuses(lambda: item.retain_and_inspect(raw, tool, authority), 'ARCHIVE_BYTES_HASH_MISMATCH') + assert not list((item.pool / 'wheelhouse').iterdir()) + + +def assert_zero_archives(item, result): + assert result['outcome'] == 'fail' and result['archive_cleanup']['complete'] + assert result['archive_nodes']['remaining_owned'] == 0 + assert not item.archive_nodes and not list((item.pool / 'wheelhouse').iterdir()) + assert item.archive_used == 0 + + +def late_failure_removes_earlier_archives(): + with owner() as item: + for name in ('build', 'packaging'): + raw, tool, authority = synthetic_wheel(name) + item.retain_and_inspect(raw, tool, authority) + result = item.failure_result(new.Refusal('synthetic late refusal')) + assert_zero_archives(item, result) + assert result['archive_nodes'] == {'opened': 2, 'written': 2, 'removed': 2, 'remaining_owned': 0} + assert result['failure_record_written'] and result['custody_seal_complete'] + saved = json.loads((item.pool / 'failure.json').read_text()) + assert saved['refusal_code'] == 'SYNTHETIC_LATE_REFUSAL' + assert saved['archive_cleanup']['complete'] and len(item.check_pool()) == 1 + + +def malformed_zip_cleanup(): + raw, tool, authority = synthetic_wheel() + raw = b'synthetic invalid ZIP' + tool.update(bytes=len(raw), sha256=hashlib.sha256(raw).hexdigest()) + with owner() as item: + try: + item.retain_and_inspect(raw, tool, authority) + except zipfile.BadZipFile as error: + result = item.failure_result(error) + else: raise AssertionError('malformed ZIP accepted') + assert result['refusal_code'] == 'EXCEPTION_BADZIPFILE' + assert_zero_archives(item, result) + assert result['archive_facts'][0]['zip_started'] + assert not result['archive_facts'][0]['zip_complete'] + + +def special_zip_cleanup(): + import stat + raw, tool, authority = synthetic_wheel() + body = io.BytesIO(raw) + with zipfile.ZipFile(body, 'a') as archive: + link = zipfile.ZipInfo('synthetic-link') + link.create_system = 3 + link.external_attr = (stat.S_IFLNK | 0o777) << 16 + archive.writestr(link, 'never-followed') + raw = body.getvalue() + tool.update(bytes=len(raw), sha256=hashlib.sha256(raw).hexdigest()) + with owner() as item: + refuses(lambda: item.retain_and_inspect(raw, tool, authority), 'ZIP_SPECIAL_LINK_MEMBER') + result = item.failure_result(new.Refusal('ZIP special/link member')) + assert_zero_archives(item, result) + + +def partial_archive_write_cleanup(): + raw, tool, authority = synthetic_wheel() + original_fdopen = new.os.fdopen + class ShortWrite: + def __init__(self, fd): self.stream = original_fdopen(fd, 'wb') + def __enter__(self): return self + def __exit__(self, *args): self.stream.close() + def close(self): self.stream.close() + def fileno(self): return self.stream.fileno() + def write(self, value): return self.stream.write(value[:3]) + with owner() as item: + with patch.object(new.os, 'fdopen', side_effect=lambda fd, mode: ShortWrite(fd)): + refuses(lambda: item.retain_and_inspect(raw, tool, authority), 'SHORT_OUTPUT_WRITE') + result = item.failure_result(new.Refusal('short output write')) + assert_zero_archives(item, result) + assert result['archive_nodes'] == {'opened': 1, 'written': 0, 'removed': 1, 'remaining_owned': 0} + + +def failure_record_collision_cleanup(): + raw, tool, authority = synthetic_wheel() + with owner() as item: + item.retain_and_inspect(raw, tool, authority) + item.write('failure.json', b'original collision evidence') + result = item.failure_result(new.Refusal('primary remains primary')) + assert_zero_archives(item, result) + assert result['refusal_code'] == 'PRIMARY_REMAINS_PRIMARY' + assert not result['failure_record_written'] + assert result['failure_record_refusal_code'] == 'POOL_OUTPUT_BOUND_OR_COLLISION' + assert (item.pool / 'failure.json').read_bytes() == b'original collision evidence' + + +def untracked_failure_record_collision_cleanup(): + raw, tool, authority = synthetic_wheel() + with owner() as item: + item.retain_and_inspect(raw, tool, authority) + (item.pool / 'failure.json').write_bytes(b'unknown collision evidence') + result = item.failure_result(new.Refusal('primary remains primary')) + assert_zero_archives(item, result) + assert result['refusal_code'] == 'PRIMARY_REMAINS_PRIMARY' + assert not result['failure_record_written'] + assert result['failure_record_refusal_code'] == 'EXCEPTION_FILEEXISTSERROR' + assert (item.pool / 'failure.json').read_bytes() == b'unknown collision evidence' + + +def v2_semantic_refusal_countercontrol(): + previous = load('unexecuted_v2_source_only', HISTORICAL / 'v2-owner.source') + raw, tool, authority = synthetic_wheel() + authority['info']['requires_dist'] = ['different>=1'] + with owner() as item: + prior = previous.Acquisition(NEW_REF) + prior.pool, prior.created, prior.dir_ids = item.pool, True, item.dir_ids + try: + prior.retain_and_inspect(raw, tool, authority) + except previous.Refusal as error: + assert error.code == 'WHEEL_DEPENDENCY_METADATA_MISMATCH' + else: raise AssertionError('semantic refusal required') + saved = item.pool / 'wheelhouse' / tool['filename'] + assert saved.read_bytes() == raw, 'countercontrol: v2 retained exactly one refused archive' + with owner() as item: + refuses(lambda: item.retain_and_inspect(raw, tool, authority), 'WHEEL_DEPENDENCY_METADATA_MISMATCH') + assert not list((item.pool / 'wheelhouse').iterdir()) and item.archive_removed == 1 + + +def redirect_failure_cleans_prior_archive(): + raw, tool, authority = synthetic_wheel() + with owner() as item: + item.retain_and_inspect(raw, tool, authority) + refusal = new.Refusal('redirect refused no second request') + refusal.http_status = 302 + item.opener = Opener(error=refusal) + try: + item.get(tools['build']['url'], 64, 'archive', 2) + except new.Refusal as error: + result = item.failure_result(error) + else: raise AssertionError('redirect accepted') + assert result['refusal_code'] == 'REDIRECT_REFUSED_NO_SECOND_REQUEST' + record = json.loads((item.pool / 'events/01-refusal.json').read_text()) + assert record['http_status'] == 302 and item.opener.calls == 1 + assert_zero_archives(item, result) + + +def fdopen_failure_countercontrol(): + previous = load('held_v3_source_only', HISTORICAL / 'v3-owner.source') + for controller, expect_closed, expect_empty in ((previous, False, False), (new, True, True)): + with owner() as scope: + item = controller.Acquisition(NEW_REF) + item.pool, item.created, item.dir_ids = scope.pool, True, scope.dir_ids + descriptors = [] + def failed_wrap(fd, mode): + descriptors.append(fd) + raise OSError('synthetic fdopen boundary only') + with patch.object(controller.os, 'fdopen', side_effect=failed_wrap): + try: item.write('wheelhouse/probe.whl', b'synthetic only') + except OSError: pass + else: raise AssertionError('fdopen fault did not fire') + assert len(descriptors) == 1 + try: + os.fstat(descriptors[0]) + except OSError: + closed = True + else: + closed = False + os.close(descriptors[0]) # Control alone closes the reproduced predecessor leak. + assert closed is expect_closed + result = item.failure_result(OSError('synthetic fdopen boundary only')) + archive = item.pool / 'wheelhouse/probe.whl' + assert (not archive.exists()) is expect_empty + assert result['archive_cleanup']['complete'] is expect_empty + if expect_empty: + assert_zero_archives(item, result) + assert result['archive_nodes'] == {'opened': 1, 'written': 0, 'removed': 1, 'remaining_owned': 0} + else: + assert result['archive_nodes']['remaining_owned'] == 0 and archive.stat().st_size == 0 + + +SENTINELS = { + 'GH_TOKEN': 'synthetic-gh', 'GITHUB_TOKEN': 'synthetic-github', + 'SSH_AUTH_SOCK': '/synthetic/agent', 'SSH_ASKPASS': '/synthetic/askpass', + 'AWS_ACCESS_KEY_ID': 'synthetic-aws', 'AWS_SECRET_ACCESS_KEY': 'synthetic-aws-secret', + 'GOOGLE_APPLICATION_CREDENTIALS': '/synthetic/gcp', 'AZURE_CLIENT_SECRET': 'synthetic-azure', + 'HTTP_PROXY': 'http://synthetic.invalid', 'HTTPS_PROXY': 'http://synthetic.invalid', + 'ALL_PROXY': 'http://synthetic.invalid', 'NO_PROXY': '*', + 'HOME': '/synthetic/home', 'SSL_CERT_FILE': '/synthetic/ca', 'PYTHONPATH': '/synthetic/python', +} + + +def bootstrap_environment_countercontrol(): + previous = load('held_v3_bootstrap_source_only', HISTORICAL / 'v3-bootstrap.source') + with tempfile.TemporaryDirectory(prefix='sdk-python-184-bootstrap-env-control-') as temporary: + source = Path(temporary) / 'trusted-synthetic-owner.py' + # Only names, never even sentinel values, leave the synthetic owner. + raw = b'import os\nENV_SEEN = dict(os.environ)\ndef run_once(admission): pass\ndef run_guarded_once(admission, proof): return {}\n' + source.write_bytes(raw) + for launcher, expected_clean in ((previous, False), (bootstrap, True)): + observed = [] + real_exec = exec + def record_exec(code, namespace): + real_exec(code, namespace) + observed.append(namespace['ENV_SEEN']) + with patch.dict(os.environ, SENTINELS, clear=False), \ + patch.object(launcher, 'OWNER_PATH', source), \ + patch.object(launcher, 'OWNER_SHA', hashlib.sha256(raw).hexdigest()), \ + patch.dict(launcher.__dict__, {'exec': record_exec}): + launcher.launch(NEW_REF) + assert len(observed) == 1 + if expected_clean: + assert observed[0] == new.MINIMAL_ENV == bootstrap.MINIMAL_ENV + assert not set(SENTINELS) & set(observed[0]) + else: + assert all(observed[0].get(key) == value for key, value in SENTINELS.items()) + + +def pinned_bundle_without_child_control(): + ca = new.read_regular(new.CA_SOURCE, 300000) + with patch.dict(os.environ, SENTINELS, clear=False), \ + patch.object(subprocess, 'run', side_effect=AssertionError('NO_CHILD_FOR_PINNED_CA')) as child: + proof = new.verify_ca_bundle(ca) + assert proof['sha256'] == new.CA_SHA and proof['certificates'] == 118 and proof['blocked_subjects'] == 0 + refuses(lambda: new.verify_ca_bundle(ca+b'changed'), 'EXISTING_CLEAN_CA_PROVENANCE_CHANGED') + assert child.call_count == 0 + + +def owner_has_no_child_launch_api(): + tree=ast.parse((BASE/'184-acquire-tools-v5.py').read_bytes()) + for node in ast.walk(tree): + if isinstance(node,ast.Import):assert all(alias.name!='subprocess' for alias in node.names) + if isinstance(node,ast.ImportFrom):assert node.module!='subprocess' + if isinstance(node,ast.Call) and isinstance(node.func,ast.Attribute) and isinstance(node.func.value,ast.Name): + assert not (node.func.value.id=='os' and (node.func.attr in {'system','popen','fork','forkpty','posix_spawn','posix_spawnp'} + or node.func.attr.startswith(('exec','spawn')))) + + +def ambient_owner_environment_refuses(): + with patch.object(new, 'VERIFIED_SOURCE_SHA256', '0' * 64), patch.dict(os.environ, SENTINELS, clear=False): + item = new.Acquisition(NEW_REF) + refuses(item.run, 'CLOSED_OWNER_ENVIRONMENT_REQUIRED') + assert not item.created and not item.requests + + +def cleanup_refuses_unknown_replacement(): + raw, tool, authority = synthetic_wheel() + with owner() as item: + item.retain_and_inspect(raw, tool, authority) + p = item.pool / 'wheelhouse' / tool['filename'] + p.rename(item.pool / 'original-owned-node') + p.write_bytes(b'unknown replacement') + result = item.failure_result(new.Refusal('primary remains primary')) + assert result['refusal_code'] == 'PRIMARY_REMAINS_PRIMARY' + assert not result['archive_cleanup']['complete'] + assert result['archive_cleanup']['refusal_code'] == 'CLEANUP_ARCHIVE_IDENTITY_COLLISION' + assert p.read_bytes() == b'unknown replacement' + + +def cleanup_refuses_replaced_directory(): + raw, tool, authority = synthetic_wheel() + with owner() as item: + item.retain_and_inspect(raw, tool, authority) + (item.pool / 'wheelhouse').rename(item.pool / 'original-wheelhouse') + (item.pool / 'wheelhouse').mkdir() + result = item.failure_result(new.Refusal('primary remains primary')) + assert not result['archive_cleanup']['complete'] + assert result['archive_cleanup']['refusal_code'] == 'CLEANUP_ARCHIVE_DIRECTORY_CHANGED' + assert (item.pool / 'original-wheelhouse' / tool['filename']).read_bytes() == raw + + +def uncreated_pool_never_mutated(): + with owner() as item: + item.created = False + before = sorted(p.name for p in item.pool.iterdir()) + result = item.failure_result(FileExistsError('synthetic existing pool')) + assert result['archive_cleanup']['complete'] and not result['failure_record_written'] + assert sorted(p.name for p in item.pool.iterdir()) == before + assert not (item.pool / 'failure.json').exists() + + +def transport_failure_cleans_prior_archive(response, code, *, size=None): + raw, tool, authority = synthetic_wheel() + with owner() as item: + item.retain_and_inspect(raw, tool, authority) + item.opener = Opener(response) + try: + item.get(tools['build']['url'], 64, 'archive', size or 2) + except BaseException as error: + result = item.failure_result(error) + else: raise AssertionError('transport refusal required') + assert result['refusal_code'] == code and item.opener.calls == 1 + assert len(result['http_requests']) == 1 + assert_zero_archives(item, result) + + +def wrong_archive_tuple_cleans_prior_archive(change_size): + raw, tool, authority = synthetic_wheel() + with owner() as item: + item.retain_and_inspect(raw, tool, authority) + raw, tool, authority = synthetic_wheel('packaging') + if change_size: tool['bytes'] += 1 + else: tool['sha256'] = '0' * 64 + try: + item.retain_and_inspect(raw, tool, authority) + except new.Refusal as error: + result = item.failure_result(error) + else: raise AssertionError('wrong tuple accepted') + assert result['refusal_code'] == 'ARCHIVE_BYTES_HASH_MISMATCH' + assert_zero_archives(item, result) + + +def graph_change(): + changed = (authorities['build']['info']['requires_dist'] or []) + ['unselected-tool>=1'] + raw, tool, authority = synthetic_wheel(requirements=changed) + authority['info']['requires_dist'] = changed + with owner() as item: + refuses(lambda: item.inspect_wheel(raw, tool, authority), 'APPROVED_ACTIVE_DEPENDENCY_GRAPH_CHANGED') + + +def unsafe_zip(): + raw, tool, authority = synthetic_wheel(change=lambda members, tool, authority: members.update({'../escape': 'data'})) + with owner() as item: + refuses(lambda: item.inspect_wheel(raw, tool, authority), 'UNSAFE_ZIP_MEMBER') + + +def partial_response(): + with owner() as item: + item.opener = Opener(Response(b'abc', length=None, partial=True)) + try: + item.get(tools['build']['metadata_source'], 64, 'metadata') + except TimeoutError: + record = json.loads((item.pool / 'events/01-refusal.json').read_text()) + assert record['refusal_code'] == 'EXCEPTION_TIMEOUTERROR' + assert record['stage'] == 'body-reading' and record['bytes_received'] == 3 + assert item.requests[-1]['partial_body_sha256'] == hashlib.sha256(b'abc').hexdigest() + assert item.opener.calls == 1 + else: raise AssertionError('partial response did not fail') + + +def diagnostic_write_preserves_primary(): + with owner() as item: + item.opener = Opener(Response(length='99999')) + write = item.write + def refused_event(name, raw): + if name.endswith('-refusal.json'): + raise OSError('synthetic log failure') + write(name, raw) + item.write = refused_event + refuses(lambda: item.get(tools['build']['metadata_source'], 64, 'metadata'), 'HTTP_BODY_EXCEEDS_BOUND') + assert item.requests[-1]['diagnostic_write_failed'] is True + + +def outside_output(): + with owner() as item: + refuses(lambda: item.write('../outside', b'bad'), 'INVALID_OUTPUT_PATH') + assert not (item.pool.parent / 'outside').exists() + + +def changed_directory(): + with owner() as item: + (item.pool / 'metadata').rename(item.pool / 'original-metadata') + (item.pool / 'metadata').mkdir() + refuses(lambda: item.write('metadata/probe.json', b'{}'), 'POOL_SUBDIRECTORY_CHANGED') + assert not (item.pool / 'metadata/probe.json').exists() + + +def output_collision(): + with owner() as item: + item.write('once.json', b'one') + refuses(lambda: item.write('once.json', b'two'), 'POOL_OUTPUT_BOUND_OR_COLLISION') + assert (item.pool / 'once.json').read_bytes() == b'one' + + +def preserve_failure_reserve(): + with owner() as item: + item.used = new.POOL_CAP - 65536 + refuses(lambda: item.write('over-bound.json', b'x'), 'POOL_OUTPUT_BOUND_OR_COLLISION') + item.write('failure.json', b'{}') + assert (item.pool / 'failure.json').read_bytes() == b'{}' + + +def extra_empty_directory_refuses(): + with owner() as item: + (item.pool / 'extra-empty-directory').mkdir() + refuses(item.check_pool, 'EXTRA_OR_MISSING_POOL_OUTPUT') + + +def changed_written_file_refuses(): + with owner() as item: + item.write('written.json', b'one') + p = item.pool / 'written.json' + p.chmod(0o600) + p.write_bytes(b'two') + p.chmod(0o400) + refuses(item.check_pool, 'WRITTEN_POOL_BYTES_CHANGED') + + +def complete_synthetic_pool_checks_and_seals(): + with owner() as item: + item.write('written.json', b'one') + assert len(item.check_pool()) == 1 + item.seal() + assert len(item.check_pool()) == 1 and item.pool.stat().st_mode & 0o777 == 0o500 + item.pool.chmod(0o700) + for name in ('metadata', 'wheelhouse', 'events'): + (item.pool / name).chmod(0o700) + + +def old_admission_refuses_before_pool(): + refuses(lambda: new.Acquisition(new.OLD_ADMISSION), 'NEW_ADMISSION_REQUIRED_OLD_ATTEMPT_CANNOT_RETRY') + + +def missing_bootstrap_refuses_before_pool(): + item = new.Acquisition(NEW_REF) + refuses(item.run, 'VERIFIED_SOURCE_BOOTSTRAP_REQUIRED') + assert not item.created + + +def authority_refuses_before_request(url): + with owner() as item: + item.opener = Opener(Response()) + refuses(lambda: item.get(url, 64, 'metadata'), 'UNEXPECTED_PUBLIC_AUTHORITY') + assert not item.requests and item.opener.calls == 0 + + +def redirect_has_status_and_no_second_request(): + error = new.Refusal('redirect refused no second request') + error.http_status = 302 + with owner() as item: + item.opener = Opener(error=error) + refuses(lambda: item.get(tools['build']['metadata_source'], 64, 'metadata'), + 'REDIRECT_REFUSED_NO_SECOND_REQUEST') + record = json.loads((item.pool / 'events/01-refusal.json').read_text()) + assert record['http_status'] == 302 and item.opener.calls == 1 + refuses(lambda: new.NoRedirect().redirect_request(None, None, 302, None, {}, 'https://example.invalid'), + 'REDIRECT_REFUSED_NO_SECOND_REQUEST') + + +def HTTP_error_has_status(): + error = urllib.error.HTTPError(tools['build']['metadata_source'], 404, 'synthetic', {}, None) + with owner() as item: + item.opener = Opener(error=error) + try: + item.get(tools['build']['metadata_source'], 64, 'metadata') + except urllib.error.HTTPError: + record = json.loads((item.pool / 'events/01-refusal.json').read_text()) + assert record['http_status'] == 404 and record['refusal_code'] == 'EXCEPTION_HTTPERROR' + assert item.opener.calls == 1 + else: raise AssertionError('HTTP failure accepted') + + +def bootstrap_validates_without_execution(): + raw, _ = bootstrap.verified_source(bootstrap.OWNER_PATH, bootstrap.OWNER_SHA) + assert hashlib.sha256(raw).hexdigest() == bootstrap.OWNER_SHA + + +def bootstrap_changed_source_refuses(): + try: + bootstrap.verified_source(bootstrap.OWNER_PATH, '0' * 64) + except ValueError as error: + assert str(error) == 'BOOTSTRAP_SOURCE_HASH_MISMATCH' + else: raise AssertionError('changed source accepted') + + +def bootstrap_symlink_refuses(): + with tempfile.TemporaryDirectory(prefix='sdk-python-bootstrap-control-') as temporary: + p = Path(temporary) / 'source-link' + p.symlink_to(bootstrap.OWNER_PATH) + try: + bootstrap.verified_source(p, bootstrap.OWNER_SHA) + except OSError: + pass + else: raise AssertionError('symlink source accepted') + + +def bootstrap_hardlink_refuses(): + import os + with tempfile.TemporaryDirectory(prefix='sdk-python-bootstrap-control-') as temporary: + p = Path(temporary) / 'synthetic-source' + p.write_bytes(b'never executed') + os.link(p, Path(temporary) / 'second-link') + try: + bootstrap.verified_source(p, hashlib.sha256(p.read_bytes()).hexdigest()) + except ValueError as error: + assert str(error) == 'BOOTSTRAP_SOURCE_TYPE_OR_SIZE' + else: raise AssertionError('shared source inode accepted') + + +def bootstrap_old_admission_refuses_before_source_read(): + with patch.object(bootstrap, 'verified_source', side_effect=AssertionError('must not read/execute source')): + try: + bootstrap.launch(bootstrap.OLD_ADMISSION) + except ValueError as error: + assert str(error) == 'NEW_GENERAL_ADMISSION_REQUIRED_OLD_ATTEMPT_CANNOT_RETRY' + else: raise AssertionError('old admission reused') + + +with patch.object(socket, 'create_connection', side_effect=AssertionError('NO_NETWORK_IN_SOURCE_CONTROLS')), \ + patch.object(urllib.request, 'urlopen', side_effect=AssertionError('NO_NETWORK_IN_SOURCE_CONTROLS')): + check('old-refuses-equivalent-marker-quotes-new-accepts', old_quote_control) + for name in tools: + check('derived-authority-tuple-' + name, lambda name=name: new.validate_authority(authorities[name], tools[name])) + def headers_for_name(name=name): + raw, tool, authority = synthetic_wheel(name, single_quotes=True) + with owner() as item: + assert item.inspect_wheel(raw, tool, authority)['name'] == name + check('authority-derived-synthetic-headers-' + name, headers_for_name) + for label, mutator, code in [ + ('version', lambda a: a['info'].update(version='changed'), 'VERSIONED_METADATA_IDENTITY_CHANGED'), + ('project', lambda a: a['info'].update(name='changed'), 'VERSIONED_METADATA_IDENTITY_CHANGED'), + ('python', lambda a: a['info'].update(requires_python='>=9'), 'VERSIONED_METADATA_IDENTITY_CHANGED'), + ('URL', lambda a: a['urls'][0].update(url='https://example.invalid/tool'), 'VERSIONED_ARCHIVE_TUPLE_YANK_STATE_CHANGED'), + ('size', lambda a: a['urls'][0].update(size=1), 'VERSIONED_ARCHIVE_TUPLE_YANK_STATE_CHANGED'), + ('bool-size', lambda a: a['urls'][0].update(size=True), 'VERSIONED_ARCHIVE_TUPLE_YANK_STATE_CHANGED'), + ('hash', lambda a: a['urls'][0]['digests'].update(sha256='0'*64), 'VERSIONED_ARCHIVE_TUPLE_YANK_STATE_CHANGED'), + ('yanked', lambda a: a['urls'][0].update(yanked=True), 'VERSIONED_ARCHIVE_TUPLE_YANK_STATE_CHANGED'), + ('unknown-yank', lambda a: a['urls'][0].pop('yanked'), 'VERSIONED_ARCHIVE_TUPLE_YANK_STATE_CHANGED'), + ('string-yank', lambda a: a['urls'][0].update(yanked='false'), 'VERSIONED_ARCHIVE_TUPLE_YANK_STATE_CHANGED'), + ('duplicate', lambda a: a['urls'].append(deepcopy(a['urls'][0])), 'VERSIONED_METADATA_ARCHIVE_CARDINALITY'), + ]: + check('metadata-refuses-' + label, lambda m=mutator, c=code: metadata_mutation(m, c)) + check('single-request-receipt-and-body-hash', lambda: transport_control(Response())) + check('oversize-header-stable-refusal', lambda: transport_control(Response(length='99999'), 'HTTP_BODY_EXCEEDS_BOUND')) + check('encoded-response-stable-refusal', lambda: transport_control(Response(encoding='gzip'), 'ENCODED_BODY_REFUSED')) + check('changed-response-URL', lambda: transport_control(Response(url='https://example.invalid'), 'UNEXPECTED_HTTP_RESULT')) + check('archive-missing-length', lambda: transport_control(Response(length=None), 'ARCHIVE_HTTP_SIZE_CHANGED', kind='archive', size=2)) + check('archive-changed-length', lambda: transport_control(Response(length='3'), 'ARCHIVE_HTTP_SIZE_CHANGED', kind='archive', size=2)) + check('partial-body-count-hash-refusal-ledger', partial_response) + check('diagnostic-failure-preserves-primary-code', diagnostic_write_preserves_primary) + check('zero-archive-after-semantic-refusal', body_removed_after_semantic_refusal) + check('hash-refusal-does-not-retain-unverified-candidate', bad_hash_not_retained) + check('late-refusal-removes-all-new-archives', late_failure_removes_earlier_archives) + check('malformed-ZIP-removes-new-archive', malformed_zip_cleanup) + check('special-ZIP-member-removes-new-archive', special_zip_cleanup) + check('partial-archive-write-tracked-and-removed', partial_archive_write_cleanup) + check('failure-record-collision-preserves-primary-and-zero-archives', failure_record_collision_cleanup) + check('unknown-failure-record-collision-preserved-and-zero-archives', untracked_failure_record_collision_cleanup) + check('v2-retains-semantic-refused-archive-v3-removes-countercontrol', v2_semantic_refusal_countercontrol) + check('redirect-refusal-ledger-and-zero-archives', redirect_failure_cleans_prior_archive) + check('v3-fdopen-leaks-archive-and-fd-v4-closes-and-removes', fdopen_failure_countercontrol) + check('v3-bootstrap-inherits-sentinels-v4-closed-owner-env', bootstrap_environment_countercontrol) + check('exact-reviewed-CA-reused-without-child-changed-bytes-refuse', pinned_bundle_without_child_control) + check('guarded-owner-has-no-child-launch-surface',owner_has_no_child_launch_api) + check('owner-refuses-ambient-env-before-pool-or-network', ambient_owner_environment_refuses) + check('cleanup-identity-collision-preserves-unknown-node-nonpass', cleanup_refuses_unknown_replacement) + check('cleanup-directory-collision-preserves-unknown-tree-nonpass', cleanup_refuses_replaced_directory) + check('uncreated-pool-collision-never-mutates-existing-pool', uncreated_pool_never_mutated) + for label, response, code in [ + ('missing-Length', Response(length=None), 'ARCHIVE_HTTP_SIZE_CHANGED'), + ('changed-Length', Response(length='3'), 'ARCHIVE_HTTP_SIZE_CHANGED'), + ('final-URL', Response(url='https://example.invalid'), 'UNEXPECTED_HTTP_RESULT'), + ('encoding', Response(encoding='gzip'), 'ENCODED_BODY_REFUSED'), + ('timeout', Response(b'abc', length='3', partial=True), 'EXCEPTION_TIMEOUTERROR'), + ]: + check('transport-refusal-zero-archives-' + label, + lambda response=response, code=code: transport_failure_cleans_prior_archive( + response, code, size=3 if label == 'timeout' else 2)) + check('wrong-size-removes-earlier-new-archive', lambda: wrong_archive_tuple_cleans_prior_archive(True)) + check('wrong-hash-removes-earlier-new-archive', lambda: wrong_archive_tuple_cleans_prior_archive(False)) + check('changed-active-graph-refuses', graph_change) + check('unsafe-ZIP-path-refuses', unsafe_zip) + check('parent-output-refuses', outside_output) + check('changed-directory-inode-refuses', changed_directory) + check('exclusive-output-collision-refuses', output_collision) + check('failure-record-reserve-survives-pool-bound', preserve_failure_reserve) + check('empty-extra-directory-refuses', extra_empty_directory_refuses) + check('written-file-mutation-refuses', changed_written_file_refuses) + check('complete-synthetic-pool-custody-and-seal', complete_synthetic_pool_checks_and_seals) + check('old-admission-cannot-retry', old_admission_refuses_before_pool) + check('missing-hash-bootstrap-refuses-before-pool', missing_bootstrap_refuses_before_pool) + check('HTTP-scheme-refuses-before-request', lambda: authority_refuses_before_request('http://pypi.org/')) + check('foreign-origin-refuses-before-request', lambda: authority_refuses_before_request('https://example.invalid/')) + check('URL-userinfo-refuses-before-request', lambda: authority_refuses_before_request('https://synthetic:synthetic@pypi.org/')) + check('redirect-status-custody-no-second-request', redirect_has_status_and_no_second_request) + check('HTTP-error-status-custody', HTTP_error_has_status) + check('body-shorter-than-header-refuses', lambda: transport_control(Response(length='3'), 'HTTP_BODY_LENGTH_MISMATCH')) + check('bootstrap-validates-owned-bytes-without-execution', bootstrap_validates_without_execution) + check('bootstrap-changed-source-refuses', bootstrap_changed_source_refuses) + check('bootstrap-symlink-source-refuses', bootstrap_symlink_refuses) + check('bootstrap-shared-inode-source-refuses', bootstrap_hardlink_refuses) + check('bootstrap-old-admission-refuses-before-source-read', bootstrap_old_admission_refuses_before_source_read) + for name, text, expected in [ + ('numeric-python-order', "foo; python_version >= '3.9'", True), + ('old-Python-dependency-excluded', 'foo; python_full_version < "3.10.2"', False), + ('Windows-dependency-excluded', "foo; os_name == 'nt'", False), + ('extra-dependency-excluded', "foo; extra == 'core'", False), + ]: + check('marker-' + name, lambda text=text, expected=expected: (_ for _ in ()).throw(AssertionError('marker result')) + if new.marker_active(text) is not expected else None) + check('marker-code-is-not-executed', lambda: refuses( + lambda: new.marker_active("foo; extra == 'test' and __import__('os').system('false')"), 'UNSUPPORTED_MARKER_SYNTAX')) + check('patch-sensitive-marker-refuses', lambda: refuses( + lambda: new.marker_active("foo; python_full_version >= '3.13.1'"), 'PATCH_SENSITIVE_MARKER_REQUIRES_EXACT_RUNTIME_VERSION')) + check('unsupported-marker-variable-refuses', lambda: refuses( + lambda: new.marker_active("foo; unknown == 'yes'"), 'UNSUPPORTED_MARKER_VARIABLE')) + +report = {'scope': 'synthetic source controls, not archive acquisition/SDK/runtime qualification', + 'original_source_sha256': hashlib.sha256((HISTORICAL / 'original-owner.source').read_bytes()).hexdigest(), + 'successor_source_sha256': bootstrap.OWNER_SHA, + 'bootstrap_sha256': hashlib.sha256((BASE / '184-acquire-tools-v5-bootstrap.py').read_bytes()).hexdigest(), + 'cases': len(results), 'passed': sum(row['outcome'] == 'pass' for row in results), + 'failed': sum(row['outcome'] == 'fail' for row in results), 'results': results, + 'simulated_transport_calls': fake_calls, 'real_network_requests': 0, + 'downloaded_wheel_imports_or_installs': 0, 'actual_failed_archive_cause_established': False} +report_path=BASE/'184-acquire-tools-v5-controls-result.json' +report_bytes=(json.dumps(report,sort_keys=True,indent=2)+'\n').encode() +if report_path.exists(): + if report_path.read_bytes()!=report_bytes: + print(json.dumps(report,indent=2)) + raise SystemExit('CONTROL_RESULT_COLLISION; prior record preserved') +else: + descriptor=os.open(report_path,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o400) + with os.fdopen(descriptor,'wb') as stream: + assert stream.write(report_bytes)==len(report_bytes) +print(json.dumps(report, indent=2)) +raise SystemExit(bool(report['failed'])) diff --git a/qualification/issue-184/tool-acquisition/184-acquire-tools-v5.py b/qualification/issue-184/tool-acquisition/184-acquire-tools-v5.py new file mode 100644 index 0000000..7f82d1f --- /dev/null +++ b/qualification/issue-184/tool-acquisition/184-acquire-tools-v5.py @@ -0,0 +1,760 @@ +"""One admitted data-only acquisition; never import/install any acquired wheel.""" +from __future__ import annotations + +import ast +from contextlib import ExitStack +import datetime as dt +import email.parser +import hashlib +import io +import json +import os +from pathlib import Path, PurePosixPath +import re +import signal +import ssl +import stat +import sys +import time +import urllib.parse +import urllib.request +import zipfile + +SELECTION = Path('/tmp/sdk-python-handoff/184-immutable-build-tool-input-proposal.json') +SELECTION_SHA = 'e9a7d639325b866569cdab83b81a2949437209a6bca91c881c2e365fdc573a1c' +CA_SOURCE = Path('/home/lab4/fleet/sessions/sdk-python/20261105-010242-eef95b/.venv/lib/python3.14/site-packages/certifi/cacert.pem') +CA_SHA = '2ef52ad56b4803e264045c915e55c236cbfa045b84b81efefc218a19eb44bd2b' +CA_WHEEL_SHA = 'e9b54de76e530bb6cf0ccd036787103acb58fd42ba930ea8bf948ddbaa2f61d1' +SOURCE = '14adc40b00b68cc8a3e1ae5647a885a42f273650' +POOL_BASE = Path('/home/lab4/fleet/tmp') +OLD_ADMISSION = 'https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-6649046802' +VERIFIED_SOURCE_SHA256 = globals().get('VERIFIED_SOURCE_SHA256') +ARCHIVE_BYTES = 2901292 +FILE_CAP, ARCHIVE_CAP, POOL_CAP = 2 * 1024**2, 3 * 1024**2, 4 * 1024**2 +JSON_CAP, ZIP_MEMBER_CAP = 65536, 65536 +BLOCKED = ('AAA Certificate Services', 'emSign ECC Root CA - G3') +MINIMAL_ENV = {'PATH': '/usr/bin:/bin', 'LANG': 'C.UTF-8', 'LC_ALL': 'C.UTF-8', 'TZ': 'UTC'} + + +class Refusal(ValueError): + def __init__(self, message: str) -> None: + self.code = re.sub(r'[^A-Za-z0-9]+', '_', message).strip('_').upper() + if not self.code or len(self.code) > 128: + raise ValueError('invalid authored refusal code') + super().__init__(self.code) + + +def require(value: bool, message: str) -> None: + if not value: + raise Refusal(message) + + +MARKER_ENV = { + 'python_version': '3.13', 'python_full_version': '3.13.0', 'implementation_version': '3.13.0', + 'implementation_name': 'cpython', 'platform_python_implementation': 'CPython', + 'os_name': 'posix', 'sys_platform': 'linux', 'platform_system': 'Linux', + 'platform_machine': 'x86_64', 'extra': '', +} +VERSION_MARKERS = {'python_version', 'python_full_version', 'implementation_version'} + + +def marker_tree(text: str): + tree = ast.parse(text.strip(), mode='eval').body + allowed = (ast.BoolOp, ast.And, ast.Or, ast.Compare, ast.Name, ast.Load, ast.Constant, + ast.Eq, ast.NotEq, ast.Lt, ast.LtE, ast.Gt, ast.GtE, ast.In, ast.NotIn) + require(all(isinstance(node, allowed) for node in ast.walk(tree)), 'unsupported marker syntax') + require(all(node.id in MARKER_ENV for node in ast.walk(tree) if isinstance(node, ast.Name)), + 'unsupported marker variable') + require(all(isinstance(node.value, str) for node in ast.walk(tree) if isinstance(node, ast.Constant)), + 'unsupported marker literal') + return tree + + +def requirement_parts(text: str) -> tuple: + """Bounded syntax normalization, not a resolver or execution of marker code.""" + require(isinstance(text, str) and len(text) <= 2048, 'requirement text bound') + body, separator, marker = text.partition(';') + match = re.fullmatch(r'\s*([A-Za-z0-9_.-]+)(?:\[([^\]]+)\])?\s*(.*?)\s*', body) + require(match is not None and '@' not in body, 'unsupported requirement form') + name = re.sub(r'[-_.]+', '-', match[1]).lower() + extras = tuple(sorted(re.sub(r'[-_.]+', '-', value.strip()).lower() + for value in (match[2] or '').split(',') if value.strip())) + spec = match[3].strip() + if spec.startswith('(') and spec.endswith(')'): + spec = spec[1:-1] + specs = tuple(sorted(value.strip().replace(' ', '') for value in spec.split(',') if value.strip())) + parsed_marker = marker_tree(marker) if separator else None + normalized_marker = ast.dump(parsed_marker, include_attributes=False) if parsed_marker is not None else '' + return name, extras, specs, normalized_marker + + +def marker_active(text: str) -> bool: + _, separator, marker = text.partition(';') + if not separator: + return True + tree = marker_tree(marker) + def value(node): + if isinstance(node, ast.Name): + require(node.id in MARKER_ENV, 'unsupported marker variable') + return MARKER_ENV[node.id] + require(isinstance(node, ast.Constant) and isinstance(node.value, str), 'unsupported marker operand') + return node.value + def evaluate(node): + if isinstance(node, ast.BoolOp): + require(isinstance(node.op, (ast.And, ast.Or)), 'unsupported marker boolean') + return all(evaluate(child) for child in node.values) if isinstance(node.op, ast.And) \ + else any(evaluate(child) for child in node.values) + require(isinstance(node, ast.Compare) and len(node.ops) == len(node.comparators) == 1, + 'unsupported marker comparison') + left, right = value(node.left), value(node.comparators[0]) + op = node.ops[0] + version = any(isinstance(part, ast.Name) and part.id in VERSION_MARKERS + for part in (node.left, node.comparators[0])) + if version and not isinstance(op, (ast.In, ast.NotIn)): + require(bool(re.fullmatch(r'\d+(?:\.\d+)*', left)) + and bool(re.fullmatch(r'\d+(?:\.\d+)*', right)), 'unsupported marker version') + first, second = tuple(map(int, left.split('.'))), tuple(map(int, right.split('.'))) + full_version = any(isinstance(part, ast.Name) and part.id in { + 'python_full_version', 'implementation_version', + } for part in (node.left, node.comparators[0])) + if full_version: + literal = node.comparators[0] if isinstance(node.left, ast.Name) else node.left + require(not isinstance(literal, ast.Constant) or not literal.value.startswith('3.13'), + 'patch sensitive marker requires exact runtime version') + length = max(len(first), len(second)) + left, right = first + (0,) * (length - len(first)), second + (0,) * (length - len(second)) + if isinstance(op, ast.Eq): return left == right + if isinstance(op, ast.NotEq): return left != right + if isinstance(op, ast.Lt): return left < right + if isinstance(op, ast.LtE): return left <= right + if isinstance(op, ast.Gt): return left > right + if isinstance(op, ast.GtE): return left >= right + if isinstance(op, ast.In): return left in right + if isinstance(op, ast.NotIn): return left not in right + raise Refusal('unsupported marker operator') + return bool(evaluate(tree)) + + +def sha(raw: bytes) -> str: + return hashlib.sha256(raw).hexdigest() + + +def utc() -> str: + return dt.datetime.now(dt.timezone.utc).isoformat() + + +def verify_ca_bundle(ca: bytes) -> dict: + """Reuse the independently reviewed clean bundle only when every byte matches.""" + require(sha(ca) == CA_SHA, 'existing clean CA provenance changed') + certs = re.findall(rb'-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----', ca, re.S) + require(len(certs) == 118, 'existing clean CA count changed') + return {'sha256': CA_SHA, 'certificates': 118, 'blocked_subjects': 0, + 'subject_authority': 'previously reviewed exact immutable bundle bytes'} + + +def encoded(value: object) -> bytes: + return (json.dumps(value, sort_keys=True, indent=2) + '\n').encode() + + +def pairs(values: list[tuple[str, object]]) -> dict: + result = {} + for key, value in values: + require(key not in result, 'duplicate JSON key') + result[key] = value + return result + + +def validate_authority(authority: dict, tool: dict) -> None: + require(isinstance(authority, dict) and isinstance(authority.get('info'), dict) + and isinstance(authority.get('urls'), list) and len(authority['urls']) <= 16, + 'versioned metadata structure changed') + info = authority['info'] + normalize = lambda text: re.sub(r'[-_.]+', '-', text).lower() + require(isinstance(info.get('name'), str) and normalize(info['name']) == normalize(tool['name']) + and info.get('version') == tool['version'] and info.get('requires_python') == tool['requires_python'], + 'versioned metadata identity changed') + require(all(isinstance(row, dict) for row in authority['urls']), 'versioned metadata file shape') + matches = [row for row in authority['urls'] if row.get('filename') == tool['filename']] + require(len(matches) == 1, 'versioned metadata archive cardinality') + row = matches[0] + require(row.get('url') == tool['url'] and type(row.get('size')) is int and row['size'] == tool['bytes'] + and isinstance(row.get('digests'), dict) and row['digests'].get('sha256') == tool['sha256'] + and type(row.get('yanked')) is bool and row['yanked'] is False, + 'versioned archive tuple yank state changed') + + +def read_regular(path: Path, cap: int) -> bytes: + fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK) + with os.fdopen(fd, 'rb') as stream: + before = os.fstat(stream.fileno()) + require(stat.S_ISREG(before.st_mode) and before.st_uid == os.geteuid() + and before.st_size <= cap, 'invalid existing data file') + raw = stream.read(cap + 1) + after = os.fstat(stream.fileno()) + require((before.st_dev, before.st_ino, before.st_size, before.st_mtime_ns, before.st_ctime_ns) + == (after.st_dev, after.st_ino, after.st_size, after.st_mtime_ns, after.st_ctime_ns), + 'existing data changed while reading') + require(len(raw) == before.st_size, 'existing data size mismatch') + return raw + + +class NoRedirect(urllib.request.HTTPRedirectHandler): + def redirect_request(self, req, fp, code, msg, headers, newurl): + refusal = Refusal('redirect refused no second request') + refusal.http_status = code + raise refusal + + +class Acquisition: + def __init__(self, admission: str) -> None: + require(bool(re.fullmatch(r'https://github\.com/durable-workflow/sdk-python/issues/184#issuecomment-[1-9][0-9]{5,17}', admission)) + and admission != OLD_ADMISSION, 'new admission required old attempt cannot retry') + self.admission = admission + self.started = time.monotonic() + self.started_utc = utc() + self.io_deadline = self.started + 270 + self.ceiling = self.started + 300 + self.pool = POOL_BASE / ('sdk-python-184-tools-guardian-14adc40b-' + admission.rsplit('-', 1)[-1]) + self.preallocated: dict[str, int] = {} + self.guard_attached = False + self.created = False + self.used = 0 + self.archive_used = 0 + self.requests: list[dict] = [] + self.files: set[str] = set() + self.written_hashes: dict[str, str] = {} + self.written_sizes: dict[str, int] = {} + self.archive_nodes: dict[str, tuple[int, int]] = {} + self.archive_facts: list[dict] = [] + self.archive_opened = self.archive_written = self.archive_removed = 0 + self.current_tool = None + self.dir_ids: dict[str, tuple[int, int]] = {} + self.phase = 'selection-and-existing-trust' + + def attach_guarded_pool(self, proof: dict) -> None: + """Called only after the reviewed launcher receives independent guardian readiness.""" + require(not self.guard_attached and not self.created and not self.files, 'guardian already attached') + require(proof.get('pool') == str(self.pool) and proof.get('ready') is True, + 'guardian pool proof mismatch') + started = proof.get('started_monotonic') + require(type(started) in (int, float) and 0 < started <= time.monotonic() < started + 270, + 'guardian start deadline invalid') + self.started = started + self.started_utc = proof['started_utc'] + parsed = dt.datetime.fromisoformat(self.started_utc) + require(parsed.tzinfo is not None and abs((dt.datetime.now(dt.timezone.utc) - parsed).total_seconds() + - (time.monotonic() - started)) < 2, 'guardian wall clock anchor changed') + self.io_deadline, self.ceiling = started + 270, started + 300 + ids = proof.get('dir_ids') + require(isinstance(ids, dict) and set(ids) == {'.', 'metadata', 'wheelhouse', 'events'}, + 'guardian directory proof shape') + for name in ('.', 'metadata', 'wheelhouse', 'events'): + path = self.pool if name == '.' else self.pool / name + st = path.lstat() + require(stat.S_ISDIR(st.st_mode) and st.st_uid == os.geteuid() + and stat.S_IMODE(st.st_mode) == 0o700 and [st.st_dev, st.st_ino] == ids[name], + 'guardian directory identity changed') + self.dir_ids[name] = (st.st_dev, st.st_ino) + require(set(os.listdir(self.pool)) == {'metadata', 'wheelhouse', 'events'} + and not os.listdir(self.pool / 'metadata') and not os.listdir(self.pool / 'events'), + 'guardian root not initially exclusive') + archives = proof.get('archive_fds') + require(isinstance(archives, dict) and 1 <= len(archives) <= 6 + and set(os.listdir(self.pool / 'wheelhouse')) == set(archives), 'guardian archive set changed') + require(all(type(fd) is int and fd >= 3 for fd in archives.values()) + and len(set(archives.values())) == len(archives), 'guardian descriptor set invalid') + for filename, fd in archives.items(): + require(Path(filename).name == filename and filename.endswith('.whl'), 'guardian archive filename invalid') + node = os.fstat(fd) + named = (self.pool / 'wheelhouse' / filename).lstat() + require(stat.S_ISREG(node.st_mode) and node.st_uid == os.geteuid() and node.st_nlink == 1 + and node.st_size == 0 and stat.S_IMODE(node.st_mode) == 0o600 + and (named.st_dev, named.st_ino) == (node.st_dev, node.st_ino), + 'guardian archive descriptor identity changed') + self.preallocated[filename] = fd + self.archive_nodes['wheelhouse/' + filename] = (node.st_dev, node.st_ino) + self.archive_opened = len(self.archive_nodes) + self.created = self.guard_attached = True + + def prepare_pool(self, tools: list[dict]) -> None: + require(self.guard_attached and set(self.preallocated) == {tool['filename'] for tool in tools}, + 'complete guardian archive custody required') + root = os.open(self.pool, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) + try: + st = os.fstat(root) + require((st.st_dev, st.st_ino) == self.dir_ids['.'], 'guardian root changed before adoption') + for name in ('metadata', 'events'): + st = os.stat(name, dir_fd=root, follow_symlinks=False) + require(stat.S_ISDIR(st.st_mode) and st.st_uid == os.geteuid() + and (st.st_dev, st.st_ino) == self.dir_ids[name], 'pool directory ownership') + finally: + os.close(root) + + def write(self, name: str, raw: bytes) -> None: + require(name not in self.files and len(raw) + self.used <= POOL_CAP - (0 if name == 'failure.json' else 65536), + 'pool output bound or collision') + parts = PurePosixPath(name) + require(not parts.is_absolute() and '..' not in parts.parts and '\\' not in name + and str(parts) == name and len(parts.parts) in (1, 2) + and (len(parts.parts) == 1 or parts.parts[0] in {'metadata', 'wheelhouse', 'events'}), + 'invalid output path') + with ExitStack() as stack: + root_fd = os.open(self.pool, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) + stack.callback(os.close, root_fd) + current = os.fstat(root_fd) + require((current.st_dev, current.st_ino) == self.dir_ids.get('.'), 'pool directory changed') + parent_fd = root_fd + if len(parts.parts) == 2: + parent_fd = os.open(parts.parts[0], os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=root_fd) + stack.callback(os.close, parent_fd) + current = os.fstat(parent_fd) + require((current.st_dev, current.st_ino) == self.dir_ids.get(parts.parts[0]), + 'pool subdirectory changed') + allocated = name.startswith('wheelhouse/') and self.guard_attached + if allocated: + expected = self.archive_nodes.get(name) + named = os.stat(parts.parts[-1], dir_fd=parent_fd, follow_symlinks=False) + require(stat.S_ISREG(named.st_mode) and named.st_nlink == 1 and named.st_uid == os.geteuid() + and (named.st_dev, named.st_ino) == expected and named.st_size == 0, + 'guarded archive name changed before write') + fd = os.dup(self.preallocated[parts.parts[-1]]) + else: + fd = os.open(parts.parts[-1], os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, + 0o600, dir_fd=parent_fd) + try: + node = os.fstat(fd) + if name.startswith('wheelhouse/'): + require(not allocated or (node.st_dev, node.st_ino) == self.archive_nodes[name], + 'guarded archive fd changed before write') + self.archive_nodes[name] = (node.st_dev, node.st_ino) + if not allocated: + self.archive_opened += 1 + else: + os.lseek(fd, 0, os.SEEK_SET) + stream = os.fdopen(fd, 'wb') + except BaseException: + os.close(fd) + raise + stack.callback(stream.close) + require(stream.write(raw) == len(raw), 'short output write') + stream.flush() + os.fchmod(stream.fileno(), 0o400) + os.fsync(stream.fileno()) + self.used += len(raw) + self.files.add(name) + self.written_hashes[name] = sha(raw) + self.written_sizes[name] = len(raw) + if name.startswith('wheelhouse/'): + self.archive_written += 1 + + def remove_new_archives(self) -> dict: + """Unlink only archive names exclusively created by this owner, including partial writes.""" + result = {'complete': False, 'removed': 0, 'remaining_owned': len(self.archive_nodes)} + if not self.created: + require(not self.archive_nodes, 'archive tracking without owned pool') + return dict(result, complete=True) + with ExitStack() as stack: + root = os.open(self.pool, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) + stack.callback(os.close, root) + st = os.fstat(root) + require((st.st_dev, st.st_ino) == self.dir_ids.get('.'), 'cleanup pool directory changed') + directory = os.open('wheelhouse', os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=root) + stack.callback(os.close, directory) + st = os.fstat(directory) + require((st.st_dev, st.st_ino) == self.dir_ids.get('wheelhouse'), 'cleanup archive directory changed') + os.fchmod(directory, 0o700) + for name, identity in list(self.archive_nodes.items()): + leaf = PurePosixPath(name).name + st = os.stat(leaf, dir_fd=directory, follow_symlinks=False) + require(stat.S_ISREG(st.st_mode) and st.st_uid == os.geteuid() and st.st_nlink == 1 + and (st.st_dev, st.st_ino) == identity, 'cleanup archive identity collision') + os.unlink(leaf, dir_fd=directory) + del self.archive_nodes[name] + self.used -= self.written_sizes.pop(name, 0) + self.written_hashes.pop(name, None) + self.files.discard(name) + self.archive_removed += 1 + result['removed'] += 1 + self.archive_used = sum(self.written_sizes.get(name, 0) for name in self.archive_nodes) + result['remaining_owned'] = len(self.archive_nodes) + result['complete'] = not self.archive_nodes and not os.listdir(directory) + return result + + def failure_result(self, error: BaseException) -> dict: + """Keep the original failure primary even if cleanup or diagnostic custody also refuses.""" + code = error.code if isinstance(error, Refusal) else 'EXCEPTION_' + type(error).__name__.upper() + result = {'outcome': 'fail', 'pool': str(self.pool), 'phase': self.phase, + 'failure_type': type(error).__name__, 'refusal_code': code, + 'current_tool': self.current_tool, 'finished_at_utc': utc(), + 'http_requests': self.requests, 'archive_facts': self.archive_facts, + 'elapsed_seconds': time.monotonic() - self.started, 'retries': 0, + 'import_install_container_SDK_build': False, + 'failure_record_written': False, 'custody_seal_complete': False} + try: + result['archive_cleanup'] = self.remove_new_archives() + except BaseException as cleanup_error: + result['archive_cleanup'] = {'complete': False, + 'remaining_owned': len(self.archive_nodes), + 'refusal_code': cleanup_error.code if isinstance(cleanup_error, Refusal) + else 'EXCEPTION_' + type(cleanup_error).__name__.upper()} + result['archive_nodes'] = {'opened': self.archive_opened, 'written': self.archive_written, + 'removed': self.archive_removed, 'remaining_owned': len(self.archive_nodes)} + if self.created and time.monotonic() < self.ceiling: + try: + raw = encoded(dict(result, failure_record_written=True)) + require(len(raw) <= 65536, 'failure diagnostic size bound') + self.write('failure.json', raw) + result['failure_record_written'] = True + except BaseException as record_error: + result['failure_record_refusal_code'] = record_error.code if isinstance(record_error, Refusal) \ + else 'EXCEPTION_' + type(record_error).__name__.upper() + try: + self.seal() + result['custody_seal_complete'] = True + except BaseException as seal_error: + result['custody_seal_refusal_code'] = seal_error.code if isinstance(seal_error, Refusal) \ + else 'EXCEPTION_' + type(seal_error).__name__.upper() + return result + + def check_pool(self) -> list[dict]: + entries = [] + with ExitStack() as stack: + root = os.open(self.pool, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) + stack.callback(os.close, root) + st = os.fstat(root) + require((st.st_dev, st.st_ino) == self.dir_ids.get('.'), 'pool directory changed') + directories = {'metadata', 'wheelhouse', 'events'} + expected_root = directories | {name for name in self.files if '/' not in name} + require(set(os.listdir(root)) == expected_root, 'extra or missing pool output') + for directory in ('.', 'metadata', 'wheelhouse', 'events'): + parent = root + if directory != '.': + parent = os.open(directory, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=root) + stack.callback(os.close, parent) + st = os.fstat(parent) + require((st.st_dev, st.st_ino) == self.dir_ids[directory], 'pool subdirectory changed') + expected = {name for name in self.files if '/' not in name} if directory == '.' else { + name.split('/', 1)[1] for name in self.files if name.startswith(directory + '/')} + if directory != '.': + require(set(os.listdir(parent)) == expected, 'extra or missing pool output') + require(len(expected) <= 128, 'pool file count bound') + for leaf in sorted(expected): + name = leaf if directory == '.' else directory + '/' + leaf + fd = os.open(leaf, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=parent) + with os.fdopen(fd, 'rb') as stream: + st = os.fstat(stream.fileno()) + require(stat.S_ISREG(st.st_mode) and st.st_nlink == 1 and st.st_uid == os.geteuid() + and stat.S_IMODE(st.st_mode) == 0o400 and st.st_size <= POOL_CAP, + 'custody type mode or size mismatch') + raw = stream.read(POOL_CAP + 1) + after = os.fstat(stream.fileno()) + require((st.st_dev, st.st_ino, st.st_size, st.st_mtime_ns, st.st_ctime_ns) + == (after.st_dev, after.st_ino, after.st_size, after.st_mtime_ns, after.st_ctime_ns) + and len(raw) == st.st_size and sha(raw) == self.written_hashes[name], + 'written pool bytes changed') + entries.append({'path': name, 'bytes': len(raw), 'sha256': sha(raw), + 'device': st.st_dev, 'inode': st.st_ino, 'mode': '0400', 'links': st.st_nlink}) + require(sum(row['bytes'] for row in entries) == self.used <= POOL_CAP, 'pool accounting mismatch') + return sorted(entries, key=lambda row: row['path']) + + def seal(self) -> None: + with ExitStack() as stack: + root = os.open(self.pool, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) + stack.callback(os.close, root) + st = os.fstat(root) + require((st.st_dev, st.st_ino) == self.dir_ids.get('.'), 'pool directory changed') + for name in ('metadata', 'wheelhouse', 'events'): + fd = os.open(name, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=root) + stack.callback(os.close, fd) + st = os.fstat(fd) + require((st.st_dev, st.st_ino) == self.dir_ids[name], 'pool subdirectory changed') + os.fchmod(fd, 0o500) + os.fchmod(root, 0o500) + + def get(self, url: str, cap: int, kind: str, size: int | None = None) -> bytes: + require(kind in {'metadata', 'archive'} and 0 < cap <= (JSON_CAP if kind == 'metadata' else FILE_CAP), + 'invalid request kind or bound') + require(kind != 'archive' or type(size) is int and 0 < size <= cap, 'invalid archive request size') + parsed = urllib.parse.urlsplit(url) + allowed = 'pypi.org' if kind == 'metadata' else 'files.pythonhosted.org' + require(parsed.scheme == 'https' and parsed.hostname == allowed and not parsed.username + and not parsed.password and parsed.port is None and not parsed.query and not parsed.fragment, + 'unexpected public authority') + remaining = self.io_deadline - time.monotonic() + require(remaining > 0, 'acquisition I/O deadline') + number = len(self.requests) + 1 + entry = {'number': number, 'kind': kind, 'url': url, 'attempt': 1, 'stage': 'before-open'} + self.requests.append(entry) + self.write(f'events/{number:02d}-request.json', encoded(entry)) + signal.setitimer(signal.ITIMER_REAL, min(20, remaining)) + try: + request = urllib.request.Request(url, headers={ + 'User-Agent': 'sdk-python-source-input/1', 'Accept-Encoding': 'identity', + 'Accept': 'application/json' if kind == 'metadata' else 'application/octet-stream', + }) + with self.opener.open(request, timeout=min(15, remaining)) as response: + entry['stage'] = 'headers-received' + header_length = response.headers.get('Content-Length') + header_encoding = response.headers.get('Content-Encoding', 'identity') + self.write(f'events/{number:02d}-headers.json', encoded({ + 'number': number, 'status': response.status, + 'same_selected_url': response.geturl() == url, + 'content_length': int(header_length) if header_length and header_length.isdecimal() + and len(header_length) < 12 else 'missing-or-invalid', + 'content_encoding': header_encoding if re.fullmatch(r'[A-Za-z0-9_.-]{0,32}', header_encoding) + else 'invalid', + })) + require(response.status == 200 and response.geturl() == url, 'unexpected HTTP result') + require(response.headers.get('Content-Encoding', 'identity') == 'identity', 'encoded body refused') + length = response.headers.get('Content-Length') + if length is not None: + require(length.isdecimal() and int(length) <= cap, 'HTTP body exceeds bound') + if size is not None: + require(length is not None and int(length) == size, 'archive HTTP size changed') + raw = bytearray() + digest = hashlib.sha256() + entry['stage'] = 'body-reading' + while True: + require(time.monotonic() < self.io_deadline, 'acquisition I/O deadline') + chunk = response.read(min(65536, cap + 1 - len(raw))) + if not chunk: + break + raw.extend(chunk) + digest.update(chunk) + entry['bytes_received'] = len(raw) + entry['partial_body_sha256'] = digest.hexdigest() + require(len(raw) <= cap, 'HTTP body exceeds bound') + require(length is None or len(raw) == int(length), 'HTTP body length mismatch') + entry.update(stage='body-complete', bytes_received=len(raw), body_sha256=sha(raw)) + self.write(f'events/{number:02d}-complete.json', encoded(entry)) + return bytes(raw) + except BaseException as error: + code = error.code if isinstance(error, Refusal) else 'EXCEPTION_' + type(error).__name__.upper() + try: + self.write(f'events/{number:02d}-refusal.json', encoded({ + 'number': number, 'stage': entry['stage'], 'bytes_received': entry.get('bytes_received', 0), + 'refusal_code': code, + 'http_status': getattr(error, 'http_status', None) + if isinstance(getattr(error, 'http_status', None), int) + else getattr(error, 'code', None) if type(getattr(error, 'code', None)) is int else None, + })) + except BaseException: + entry['diagnostic_write_failed'] = True + raise + finally: + signal.setitimer(signal.ITIMER_REAL, 0) + + def cached(self, tool: dict) -> tuple[bytes | None, str | None]: + roots = sorted([*Path('/tmp').glob('sdk-python-184-tools*14adc40b-*'), + *POOL_BASE.glob('sdk-python-184-tools*14adc40b-*')]) + require(len(roots) <= 32, 'named cache search bound') + for root in roots: + if root == self.pool: + continue + require(not root.is_symlink() and root.is_dir() and root.stat().st_uid == os.geteuid(), + 'named cache ownership collision') + directory = root / 'wheelhouse' + require(not directory.is_symlink(), 'named cache directory symlink') + path = directory / tool['filename'] + if not path.exists() and not path.is_symlink(): + continue + raw = read_regular(path, FILE_CAP) + require(len(raw) == tool['bytes'] and sha(raw) == tool['sha256'], 'named cache tuple mismatch') + return raw, str(path) + return None, None + + def inspect_wheel(self, raw: bytes, tool: dict, authority: dict) -> dict: + with zipfile.ZipFile(io.BytesIO(raw)) as archive: + entries = archive.infolist() + require(0 < len(entries) <= 4096 and sum(row.file_size for row in entries) <= 64 * 1024**2, + 'ZIP member/count size bound') + names: set[str] = set() + for row in entries: + path = PurePosixPath(row.filename) + require(row.filename not in names and not path.is_absolute() and '..' not in path.parts + and str(path) == row.filename.rstrip('/') and not row.filename.endswith('//') + and len(row.filename.encode('utf-8')) <= 512 + and '\\' not in row.filename and ':' not in row.filename + and not any(ord(c) < 32 for c in row.filename) + and not row.flag_bits & 1, 'unsafe ZIP member') + mode = stat.S_IFMT(row.external_attr >> 16) + require(mode in (0, stat.S_IFDIR) if row.is_dir() else mode in (0, stat.S_IFREG), + 'ZIP special/link member') + names.add(row.filename) + metadata = [name for name in names if name.count('/') == 1 and name.endswith('.dist-info/METADATA')] + wheel = [name for name in names if name.count('/') == 1 and name.endswith('.dist-info/WHEEL')] + require(len(metadata) == len(wheel) == 1, 'ambiguous wheel metadata') + require(metadata[0].rsplit('/', 1)[0] == wheel[0].rsplit('/', 1)[0], 'wheel metadata root mismatch') + for name in metadata + wheel: + require(archive.getinfo(name).file_size <= ZIP_MEMBER_CAP, 'wheel metadata size bound') + msg = email.parser.BytesParser().parsebytes(archive.read(metadata[0])) + whl = email.parser.BytesParser().parsebytes(archive.read(wheel[0])) + require(all(len(msg.get_all(key, [])) == 1 for key in ('Name', 'Version', 'Requires-Python')), + 'wheel critical metadata cardinality') + normalize = lambda value: re.sub(r'[-_.]+', '-', value).lower() + require(normalize(msg['Name']) == normalize(tool['name']) and msg['Version'] == tool['version'], + 'wheel name/version mismatch') + require(msg['Requires-Python'] == tool['requires_python'], 'wheel Python bound mismatch') + actual = sorted(requirement_parts(value) for value in msg.get_all('Requires-Dist', [])) + published = sorted(requirement_parts(value) for value in authority['info'].get('requires_dist') or []) + require(actual == published, 'wheel dependency metadata mismatch') + active = sorted(requirement_parts(value)[:3] for value in msg.get_all('Requires-Dist', []) + if marker_active(value)) + expected_active = sorted(requirement_parts(value)[:3] for value in tool['active_requirements']) + require(active == expected_active, 'approved active dependency graph changed') + require(whl.get_all('Tag', []) == ['py3-none-any'] and whl['Root-Is-Purelib'] == 'true', + 'unexpected tool platform tag') + return {'name': tool['name'], 'version': tool['version'], 'metadata_member': metadata[0], + 'metadata_sha256': sha(archive.read(metadata[0])), 'requires_python': msg['Requires-Python'], + 'requires_dist': msg.get_all('Requires-Dist', []), + 'approved_active_requirements': tool['active_requirements'], + 'wheel_tags': whl.get_all('Tag', []), 'members': len(entries), + 'uncompressed_header_bytes': sum(row.file_size for row in entries), + 'extracted_or_imported': False} + + def retain_and_inspect(self, raw: bytes, tool: dict, authority: dict, cache_source=None) -> dict: + facts = {'name': tool['name'], 'filename': tool['filename'], 'expected_size': tool['bytes'], + 'bytes_received': len(raw), 'hash_started': False, 'hash_complete': False, + 'zip_started': False, 'zip_complete': False, 'cache_source': cache_source} + self.archive_facts.append(facts) + require(0 < len(raw) <= FILE_CAP, 'archive per file bound') + facts['hash_started'] = True + facts['hash_matches'] = sha(raw) == tool['sha256'] + facts['hash_complete'] = True + require(len(raw) == tool['bytes'] and facts['hash_matches'], 'archive bytes/hash mismatch') + require(self.archive_used + len(raw) <= ARCHIVE_CAP, 'combined archive size bound') + self.write('wheelhouse/' + tool['filename'], raw) + self.archive_used += len(raw) + self.phase = 'archive/' + tool['name'] + '/retained-before-semantic-validation' + facts['zip_started'] = True + try: + inspected = self.inspect_wheel(raw, tool, authority) + except BaseException: + try: + facts['archive_cleanup'] = self.remove_new_archives() + except BaseException as error: + facts['cleanup_refusal_code'] = error.code if isinstance(error, Refusal) \ + else 'EXCEPTION_' + type(error).__name__.upper() + raise + facts['zip_complete'] = True + inspected['cache_source'] = cache_source + return inspected + + def run(self) -> dict: + require(isinstance(VERIFIED_SOURCE_SHA256, str) + and bool(re.fullmatch(r'[0-9a-f]{64}', VERIFIED_SOURCE_SHA256)), 'verified source bootstrap required') + require(dict(os.environ) == MINIMAL_ENV, 'closed owner environment required') + require(self.guard_attached, 'independent guardian custody required before network') + selection_raw = read_regular(SELECTION, 32768) + require(sha(selection_raw) == SELECTION_SHA, 'selected tuple source changed') + selection = json.loads(selection_raw, object_pairs_hook=pairs) + require(selection['source'] == SOURCE and len(selection['tools']) == 6, 'wrong selected source/tool set') + tools = selection['tools'] + require(sum(tool['bytes'] for tool in tools) == ARCHIVE_BYTES <= ARCHIVE_CAP, + 'selected total archive size changed') + require(all(0 < tool['bytes'] <= FILE_CAP and Path(tool['filename']).name == tool['filename'] + for tool in tools), 'selected archive bounds') + ca = read_regular(CA_SOURCE, 300000) + trust_proof = verify_ca_bundle(ca) + context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + context.minimum_version = ssl.TLSVersion.TLSv1_2 + context.load_verify_locations(cadata=ca.decode('ascii')) + self.opener = urllib.request.build_opener(urllib.request.ProxyHandler({}), + urllib.request.HTTPSHandler(context=context), NoRedirect()) + require(POOL_BASE.is_dir() and not POOL_BASE.is_symlink() and POOL_BASE.stat().st_uid == os.geteuid(), + 'persistent pool parent unavailable') + self.prepare_pool(tools) + self.write('selection.json', selection_raw) + self.write('ca.pem', ca) + self.write('launch.json', encoded({'started_at_utc': self.started_utc, 'owner_pid': os.getpid(), + 'pool': str(self.pool), 'admission': self.admission, 'source': SOURCE, + 'verified_acquisition_source_sha256': VERIFIED_SOURCE_SHA256, + 'io_seconds': 270, 'total_seconds': 300, + 'hard_cutoff_utc': (dt.datetime.fromisoformat(self.started_utc) + dt.timedelta(seconds=300)).isoformat(), + 'ca_sha256': CA_SHA, 'ca_certificates': 118, 'blocked_CA_subjects': 0, + 'CA_subject_authority': trust_proof['subject_authority'], + 'CA_provenance': {'existing_pinned_wheel_sha256': CA_WHEEL_SHA, + 'sealed_original_input': '/Users/lab3/fleet/tmp/sdk-python-2150-connected-v1-once-01a1a5ef/inputs/wheelhouse/certifi-2026.10.23-py3-none-any.whl', + 'member': 'certifi/cacert.pem', 'member_sha256': CA_SHA}, + 'cache_policy': 'byte-verify existing regular named SDK archive or one request per missing archive', + 'credentials_proxy_default_trust': False})) + print(json.dumps({'phase': 'launched', 'pool': str(self.pool), 'started_at_utc': self.started_utc}), flush=True) + authorities = {} + self.phase = 'official-versioned-metadata-recheck' + for index, tool in enumerate(tools, 1): + self.current_tool = {'index': index, 'name': tool['name'], 'filename': tool['filename']} + self.phase = 'metadata/' + tool['name'] + '/request' + raw = self.get(tool['metadata_source'], JSON_CAP, 'metadata') + self.write('metadata/' + tool['name'] + '.json', raw) + self.phase = 'metadata/' + tool['name'] + '/retained-before-parse' + authority = json.loads(raw, object_pairs_hook=pairs) + validate_authority(authority, tool) + authorities[tool['name']] = authority + actual_metadata = [] + self.phase = 'six-exact-archive-requests' + for index, tool in enumerate(tools, 1): + self.current_tool = {'index': index, 'name': tool['name'], 'filename': tool['filename']} + self.phase = 'archive/' + tool['name'] + '/cache-or-request' + raw, cache_source = self.cached(tool) + if raw is None: + raw = self.get(tool['url'], FILE_CAP, 'archive', tool['bytes']) + actual_metadata.append(self.retain_and_inspect(raw, tool, authorities[tool['name']], cache_source)) + require(self.archive_used == ARCHIVE_BYTES, 'archive total mismatch') + self.write('wheel-metadata.json', encoded({'environment': 'CPython3.13 Linux amd64; no extras', + 'tools': actual_metadata, 'actual_requires_dist_matches_versioned_authority': True, + 'active_dependency_edges_match_approved_metadata_selection': True})) + require(time.monotonic() < self.ceiling, 'total acquisition deadline') + self.phase = 'sealing-custody' + self.write('acquisition-observation.json', encoded({'status': 'collected-before-final-custody-check', 'finished_at_utc': utc(), + 'elapsed_seconds': time.monotonic() - self.started, 'archive_bytes': self.archive_used, + 'http_requests': self.requests, 'archive_requests': sum(row['kind'] == 'archive' for row in self.requests), + 'retries': 0, 'import_install_container_SDK_build': False})) + entries = self.check_pool() + inventory_raw = encoded({'source': SOURCE, 'entries': entries, 'inventory_excludes_self': True}) + self.write('inventory.json', inventory_raw) + self.seal() + self.check_pool() + require(self.used <= POOL_CAP and time.monotonic() < self.ceiling, 'final pool/time bound') + return {'outcome': 'candidate', 'pool': str(self.pool), 'source': SOURCE, 'files': len(self.files), + 'whole_pool_bytes': self.used, 'archive_bytes': self.archive_used, + 'inventory_sha256': sha(inventory_raw), 'finished_at_utc': utc(), + 'elapsed_seconds': time.monotonic() - self.started, + 'import_install_container_SDK_build': False} + + +def alarm(signum, frame): + raise TimeoutError('bounded request deadline') + + +def run_once(admission: str) -> None: + signal.signal(signal.SIGALRM, alarm) + owner = Acquisition(admission) + try: + result = owner.run() + except BaseException as error: + signal.setitimer(signal.ITIMER_REAL, 0) + result = owner.failure_result(error) + print(json.dumps(result), flush=True) + sys.exit(1) + print(json.dumps(result), flush=True) + + +def run_guarded_once(admission: str, proof: dict) -> dict: + """Return a candidate only; independent terminal acceptance is required.""" + signal.signal(signal.SIGALRM, alarm) + owner = Acquisition(admission) + try: + owner.attach_guarded_pool(proof) + result = owner.run() + except BaseException as error: + signal.setitimer(signal.ITIMER_REAL, 0) + result = owner.failure_result(error) + finally: + for fd in set(proof.get('archive_fds', {}).values()): + if type(fd) is int and fd >= 3: + os.close(fd) + return result + + +if __name__ == '__main__': + raise SystemExit('VERIFIED_SOURCE_BOOTSTRAP_AND_NEW_ADMISSION_REQUIRED; proposal not launched') diff --git a/qualification/issue-184/tool-acquisition/184-acquisition-custody-guardian-v2-proposal.py b/qualification/issue-184/tool-acquisition/184-acquisition-custody-guardian-v2-proposal.py new file mode 100644 index 0000000..c171fb1 --- /dev/null +++ b/qualification/issue-184/tool-acquisition/184-acquisition-custody-guardian-v2-proposal.py @@ -0,0 +1,227 @@ +"""Source-only custody primitive. No acquisition launcher or acceptance path exists here. + +The guardian preallocates archive nodes before handing any writable descriptor +to an owner, so owner death cannot leave an unregistered new archive. A future +owner adapter and exact authority/descriptor handoff require separate review. +""" +from __future__ import annotations + +from contextlib import ExitStack +import hashlib +import json +import os +from pathlib import Path +import re +import select +import signal +import stat +import time + +MINIMAL_ENV = {'PATH': '/usr/bin:/bin', 'LANG': 'C.UTF-8', 'LC_ALL': 'C.UTF-8', 'TZ': 'UTC'} +OLD_DECISION = 'https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-6649046802' +SELECTED_SOURCE = '14adc40b00b68cc8a3e1ae5647a885a42f273650' +ARCHIVE_CAP = 3 * 1024**2 + + +class Refusal(ValueError): + pass + + +def require(value: bool, code: str) -> None: + if not value: + raise Refusal(code) + + +def bind_authority(raw: bytes, expected_sha: str, expected_decision: str, expected_inputs: dict) -> dict: + """Expected values must come from the separately reviewed exact future invocation.""" + require(len(raw) <= 32768 and hashlib.sha256(raw).hexdigest() == expected_sha, 'AUTHORITY_BYTES_CHANGED') + authority = json.loads(raw) + require(isinstance(expected_decision, str) and expected_decision != OLD_DECISION + and re.fullmatch(r'https://github\.com/durable-workflow/sdk-python/issues/184#issuecomment-[1-9][0-9]{5,17}', expected_decision) + and authority.get('decision') == expected_decision, 'EXACT_NEW_DECISION_REQUIRED') + require(authority.get('selected_source') == SELECTED_SOURCE + and authority.get('inputs') == expected_inputs, 'EXACT_INPUT_AUTHORITY_CHANGED') + require(authority.get('environment') == MINIMAL_ENV and authority.get('host') == 'lab4' + and authority.get('total_seconds') == 300 and authority.get('io_seconds') == 270 + and authority.get('retries') == 0, 'OPERATION_BOUNDARY_CHANGED') + return authority + + +class ArchiveCustody: + """Owns only a newly exclusive root and exact preallocated archive inodes.""" + def __init__(self, parent: Path, leaf: str, filenames: list[str]): + require(re.fullmatch(r'sdk-python-184-tools-guardian-[a-zA-Z0-9-]{1,96}', leaf) is not None, + 'INVALID_EXCLUSIVE_ROOT_NAME') + require(1 <= len(filenames) <= 6 and len(set(filenames)) == len(filenames) + and all(re.fullmatch(r'[a-zA-Z0-9_.-]{1,160}\.whl', name) for name in filenames), + 'INVALID_ARCHIVE_NAMES') + self.path = parent / leaf + self.stack = ExitStack() + self.archive_nodes: dict[str, dict] = {} + self.root_id = None + self.wheelhouse_id = None + self.created = False + # Directory descriptors cannot be wrapped in streams; keep explicit close custody. + self.parent_fd = os.open(parent, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) + self.stack.callback(os.close, self.parent_fd) + try: + st = os.fstat(self.parent_fd) + require(st.st_uid == os.geteuid() and not st.st_mode & 0o022, 'UNSAFE_POOL_PARENT') + os.mkdir(leaf, 0o700, dir_fd=self.parent_fd) + self.created = True + self.root_fd = os.open(leaf, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=self.parent_fd) + self.stack.callback(os.close, self.root_fd) + st = os.fstat(self.root_fd) + self.root_id = (st.st_dev, st.st_ino) + os.mkdir('wheelhouse', 0o700, dir_fd=self.root_fd) + self.wheelhouse_fd = os.open('wheelhouse', os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=self.root_fd) + self.stack.callback(os.close, self.wheelhouse_fd) + st = os.fstat(self.wheelhouse_fd) + self.wheelhouse_id = (st.st_dev, st.st_ino) + for name in filenames: + fd = os.open(name, os.O_RDWR | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600, + dir_fd=self.wheelhouse_fd) + self.stack.callback(os.close, fd) + st = os.fstat(fd) + self.archive_nodes[name] = {'fd': fd, 'identity': (st.st_dev, st.st_ino)} + except BaseException: + # A failed allocation is never handed to an owner. Preserve failure + # classification even if scope verification itself cannot complete. + if self.created and self.wheelhouse_id is not None: + try: self.cleanup() + except BaseException: pass + self.stack.close() + raise + + def scope(self) -> None: + try: + st = os.stat(self.path.name, dir_fd=self.parent_fd, follow_symlinks=False) + except FileNotFoundError: + raise Refusal('ROOT_SCOPE_MISSING') from None + require(stat.S_ISDIR(st.st_mode) and st.st_uid == os.geteuid() + and (st.st_dev, st.st_ino) == self.root_id, 'ROOT_SCOPE_COLLISION') + try: + st = os.stat('wheelhouse', dir_fd=self.root_fd, follow_symlinks=False) + except FileNotFoundError: + raise Refusal('WHEELHOUSE_SCOPE_MISSING') from None + require(stat.S_ISDIR(st.st_mode) and st.st_uid == os.geteuid() + and (st.st_dev, st.st_ino) == self.wheelhouse_id, 'WHEELHOUSE_SCOPE_COLLISION') + + def node(self, name: str) -> os.stat_result: + self.scope() + expected = self.archive_nodes[name] + st = os.stat(name, dir_fd=self.wheelhouse_fd, follow_symlinks=False) + require(stat.S_ISREG(st.st_mode) and st.st_uid == os.geteuid() and st.st_nlink == 1 + and (st.st_dev, st.st_ino) == expected['identity'], 'ARCHIVE_SCOPE_COLLISION') + descriptor = os.fstat(expected['fd']) + require((descriptor.st_dev, descriptor.st_ino) == expected['identity'], 'ARCHIVE_FD_CHANGED') + return st + + def duplicate_for_owner(self, name: str) -> int: + """Only valid after a future ready/authority handshake; no owner is launched here.""" + self.node(name) + return os.dup(self.archive_nodes[name]['fd']) + + def verify_archive_bytes(self, expected: dict[str, dict]) -> list[dict]: + """Raw tuple check only. This does not accept tool metadata or an acquisition.""" + require(set(expected) == set(self.archive_nodes), 'INCOMPLETE_ARCHIVE_SET') + self.scope() + require(set(os.listdir(self.wheelhouse_fd)) == set(expected), 'EXTRA_OR_MISSING_ARCHIVE_NODE') + rows = [] + for name, wanted in expected.items(): + st = self.node(name) + require(type(wanted.get('bytes')) is int and 0 < wanted['bytes'] <= 2 * 1024**2 + and st.st_size == wanted['bytes'] and stat.S_IMODE(st.st_mode) == 0o400, + 'ARCHIVE_SIZE_OR_MODE_CHANGED') + fd = self.archive_nodes[name]['fd'] + raw = os.pread(fd, wanted['bytes'] + 1, 0) + after = os.fstat(fd) + require((st.st_size, st.st_mtime_ns, st.st_ctime_ns) + == (after.st_size, after.st_mtime_ns, after.st_ctime_ns) + and len(raw) == st.st_size and hashlib.sha256(raw).hexdigest() == wanted['sha256'], + 'ARCHIVE_BYTES_CHANGED') + rows.append({'filename': name, 'bytes': len(raw), 'sha256': wanted['sha256']}) + require(sum(row['bytes'] for row in rows) <= ARCHIVE_CAP, 'ARCHIVE_TOTAL_BOUND') + return rows + + def cleanup(self) -> dict: + """Remove known nodes only; unknown replacements remain explicit nonpasses.""" + result = {'complete': False, 'removed': [], 'already_absent': [], 'collisions': []} + self.scope() + os.fchmod(self.wheelhouse_fd, 0o700) + for name in self.archive_nodes: + try: + self.node(name) + except FileNotFoundError: + if os.fstat(self.archive_nodes[name]['fd']).st_nlink == 0: + result['already_absent'].append(name) + else: + result['collisions'].append({'filename': name, 'code': 'OWNED_NODE_STILL_LINKED'}) + except Refusal as error: + result['collisions'].append({'filename': name, 'code': str(error)}) + else: + os.unlink(name, dir_fd=self.wheelhouse_fd) + result['removed'].append(name) + result['remaining_names'] = sorted(os.listdir(self.wheelhouse_fd)) + self.scope() + result['complete'] = not result['remaining_names'] and not result['collisions'] + return result + + def close(self) -> None: + self.stack.close() + + +def watch_owner_eof(custody: ArchiveCustody, owner_pipe: int, deadline: float, ready_pipe: int, + owner_pidfd: int, expected_pid: int, cleanup_ceiling: float) -> dict: + """Independent process primitive; deadline is anchored before any future allocation. + + No success/preserve message is accepted. Until reviewed owner integration and + terminal acceptance exist, this prototype ALWAYS cleans up on EOF/deadline. + """ + require(dict(os.environ) == MINIMAL_ENV, 'CLOSED_GUARDIAN_ENV_REQUIRED') + require(time.monotonic() < deadline <= time.monotonic() + 300, 'INVALID_ABSOLUTE_DEADLINE') + require(deadline < cleanup_ceiling <= deadline + 30, 'INVALID_CLEANUP_CEILING') + require(os.readlink(f'/proc/self/fd/{owner_pidfd}') == 'anon_inode:[pidfd]', 'OWNER_PIDFD_REQUIRED') + info = Path(f'/proc/self/fdinfo/{owner_pidfd}').read_text() + require(re.search(r'^Pid:\s*' + re.escape(str(expected_pid)) + r'\s*$', info, re.M) is not None + and expected_pid > 1 and expected_pid != os.getpid(), 'EXACT_OWNER_PIDFD_REQUIRED') + custody.scope() + os.write(ready_pipe, b'READY\n') + os.close(ready_pipe) + reason = 'ABSOLUTE_DEADLINE' + while time.monotonic() < deadline: + available, _, _ = select.select([owner_pipe], [], [], max(0, deadline - time.monotonic())) + if available: + data = os.read(owner_pipe, 1) + reason = 'OWNER_EOF' if not data else 'UNSUPPORTED_OWNER_MESSAGE' + break + # A pidfd refers to the created process, so PID reuse cannot retarget this stop. + if not select.select([owner_pidfd], [], [], 0)[0]: + try: + signal.pidfd_send_signal(owner_pidfd, signal.SIGKILL) + except ProcessLookupError: + pass + owner_absent = bool(select.select([owner_pidfd], [], [], max(0, min(5, cleanup_ceiling - time.monotonic())))[0]) + if not owner_absent: + # A surviving process may retain writable descriptors. Keep its named + # nodes in custody for the outer owner; unlinking them is not completion. + result = {'complete': False, 'deferred_until_exact_owner_exit': True, + 'refusal_code': 'OWNER_EXIT_UNCONFIRMED', + 'retained_owned_names': sorted(custody.archive_nodes)} + else: + try: + result = custody.cleanup() + except BaseException as error: + result = {'complete': False, 'refusal_code': str(error) if isinstance(error, Refusal) + else 'EXCEPTION_' + type(error).__name__.upper()} + within_ceiling = time.monotonic() < cleanup_ceiling + terminal_complete = owner_absent and bool(result['complete']) and within_ceiling + return {'reason': reason, 'archive_cleanup': result, 'owner_process_exited': owner_absent, + 'within_cleanup_ceiling': within_ceiling, + 'terminal_cleanup_complete': terminal_complete, + 'outer_owner_custody_required': not terminal_complete, + 'acquisition_accepted': False} + + +if __name__ == '__main__': + raise SystemExit('SOURCE_PROPOSAL_ONLY; owner adapter, launcher, acceptance and exact new authority are pending') diff --git a/qualification/issue-184/tool-acquisition/184-acquisition-custody-guardian-v3-proposal.py b/qualification/issue-184/tool-acquisition/184-acquisition-custody-guardian-v3-proposal.py new file mode 100644 index 0000000..773ab92 --- /dev/null +++ b/qualification/issue-184/tool-acquisition/184-acquisition-custody-guardian-v3-proposal.py @@ -0,0 +1,353 @@ +"""Source-only custody primitive. No acquisition launcher or acceptance path exists here. + +The guardian preallocates archive nodes before handing any writable descriptor +to an owner, so owner death cannot leave an unregistered new archive. A future +owner adapter and exact authority/descriptor handoff require separate review. +""" +from __future__ import annotations + +from contextlib import ExitStack +import hashlib +import json +import os +from pathlib import Path +import re +import select +import signal +import stat +import time + +MINIMAL_ENV = {'PATH': '/usr/bin:/bin', 'LANG': 'C.UTF-8', 'LC_ALL': 'C.UTF-8', 'TZ': 'UTC'} +OLD_DECISION = 'https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-6649046802' +SELECTED_SOURCE = '14adc40b00b68cc8a3e1ae5647a885a42f273650' +ARCHIVE_CAP = 3 * 1024**2 + + +class Refusal(ValueError): + pass + + +class AllocationRefusal(Refusal): + """Partial construction returns custody instead of discarding live handles.""" + def __init__(self, custody, cleanup, cause): + super().__init__('ALLOCATION_REFUSED') + self.custody, self.cleanup = custody, cleanup + self.cause_type = type(cause).__name__ + + +def require(value: bool, code: str) -> None: + if not value: + raise Refusal(code) + + +AUTHORITY_FIELDS = {'decision', 'selected_source', 'inputs', 'environment', 'host', + 'total_seconds', 'io_seconds', 'retries', 'survivor_policy', + 'pool_parent', 'launcher_sha256', 'cpu_affinity'} +AUTHORITY_FIELDS.add('retention_deadline_utc') +INPUT_FIELDS = {'owner', 'bootstrap', 'guardian', 'terminal', 'selection', 'interpreter', 'library', 'ca'} + + +def unique_pairs(values): + result = {} + for key, value in values: + require(key not in result, 'DUPLICATE_AUTHORITY_KEY') + result[key] = value + return result + + +def reject_constant(value): + raise Refusal('NONFINITE_AUTHORITY_NUMBER') + + +def decode_authority(raw: bytes) -> dict: + authority = json.loads(raw.decode('utf-8'), object_pairs_hook=unique_pairs, parse_constant=reject_constant) + require(type(authority) is dict and set(authority) == AUTHORITY_FIELDS, 'EXACT_AUTHORITY_SCHEMA_REQUIRED') + require(type(authority['inputs']) is dict and set(authority['inputs']) == INPUT_FIELDS + and all(type(value) is str and re.fullmatch(r'[0-9a-f]{64}', value) + for value in authority['inputs'].values()), 'EXACT_INPUT_SCHEMA_REQUIRED') + require(type(authority['environment']) is dict and set(authority['environment']) == set(MINIMAL_ENV) + and all(type(value) is str for value in authority['environment'].values()), + 'EXACT_ENVIRONMENT_SCHEMA_REQUIRED') + require(all(type(authority[key]) is int for key in ('total_seconds', 'io_seconds', 'retries')), + 'INTEGER_AUTHORITY_LIMITS_REQUIRED') + require(all(type(authority[key]) is str for key in ('decision', 'selected_source', 'host', + 'survivor_policy', 'pool_parent', 'launcher_sha256')) + and re.fullmatch(r'[0-9a-f]{64}', authority['launcher_sha256']), 'EXACT_AUTHORITY_VALUE_TYPES_REQUIRED') + require(type(authority['retention_deadline_utc']) is str + and re.fullmatch(r'[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z', + authority['retention_deadline_utc']), 'EXACT_RETENTION_DEADLINE_REQUIRED') + affinity = authority['cpu_affinity'] + require(type(affinity) is list and 1 <= len(affinity) <= 2 + and all(type(cpu) is int and cpu >= 0 for cpu in affinity) + and len(set(affinity)) == len(affinity), 'EXACT_CPU_SCHEMA_REQUIRED') + return authority + + +def bind_authority(raw: bytes, expected_sha: str, expected_decision: str, expected_inputs: dict) -> dict: + """Expected values must come from the separately reviewed exact future invocation.""" + require(len(raw) <= 32768 and hashlib.sha256(raw).hexdigest() == expected_sha, 'AUTHORITY_BYTES_CHANGED') + authority = decode_authority(raw) + require(type(expected_inputs) is dict and set(expected_inputs) == INPUT_FIELDS, + 'REVIEWED_INPUT_SCHEMA_REQUIRED') + require(isinstance(expected_decision, str) and expected_decision != OLD_DECISION + and re.fullmatch(r'https://github\.com/durable-workflow/sdk-python/issues/184#issuecomment-[1-9][0-9]{5,17}', expected_decision) + and authority.get('decision') == expected_decision, 'EXACT_NEW_DECISION_REQUIRED') + require(authority.get('selected_source') == SELECTED_SOURCE + and authority.get('inputs') == expected_inputs, 'EXACT_INPUT_AUTHORITY_CHANGED') + require(authority.get('environment') == MINIMAL_ENV and authority.get('host') == 'lab4' + and authority.get('total_seconds') == 300 and authority.get('io_seconds') == 270 + and authority.get('retries') == 0, 'OPERATION_BOUNDARY_CHANGED') + require(authority['survivor_policy'] == 'retain-custody-and-escalate' + and authority['pool_parent'] == '/home/lab4/fleet/tmp', 'EXACT_CUSTODY_POLICY_REQUIRED') + return authority + + +class ArchiveCustody: + """Owns only a newly exclusive root and exact preallocated archive inodes.""" + def __init__(self, parent: Path, leaf: str, filenames: list[str]): + require(re.fullmatch(r'sdk-python-184-tools-guardian-[a-zA-Z0-9-]{1,96}', leaf) is not None, + 'INVALID_EXCLUSIVE_ROOT_NAME') + require(1 <= len(filenames) <= 6 and len(set(filenames)) == len(filenames) + and all(re.fullmatch(r'[a-zA-Z0-9_.-]{1,160}\.whl', name) for name in filenames), + 'INVALID_ARCHIVE_NAMES') + self.path = parent / leaf + self.stack = ExitStack() + self.archive_nodes: dict[str, dict] = {} + self.root_id = None + self.wheelhouse_id = None + self.created = False + self.root_fd = None + self.wheelhouse_fd = None + # Directory descriptors cannot be wrapped in streams; keep explicit close custody. + self.parent_fd = os.open(parent, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) + self.stack.callback(os.close, self.parent_fd) + try: + st = os.fstat(self.parent_fd) + require(st.st_uid == os.geteuid() and not st.st_mode & 0o022, 'UNSAFE_POOL_PARENT') + os.mkdir(leaf, 0o700, dir_fd=self.parent_fd) + self.created = True + st = os.stat(leaf, dir_fd=self.parent_fd, follow_symlinks=False) + self.root_id = (st.st_dev, st.st_ino) + self.root_fd = os.open(leaf, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=self.parent_fd) + self.stack.callback(os.close, self.root_fd) + st = os.fstat(self.root_fd) + require((st.st_dev, st.st_ino) == self.root_id, 'ROOT_ALLOCATION_COLLISION') + os.mkdir('wheelhouse', 0o700, dir_fd=self.root_fd) + st = os.stat('wheelhouse', dir_fd=self.root_fd, follow_symlinks=False) + self.wheelhouse_id = (st.st_dev, st.st_ino) + self.wheelhouse_fd = os.open('wheelhouse', os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=self.root_fd) + self.stack.callback(os.close, self.wheelhouse_fd) + st = os.fstat(self.wheelhouse_fd) + require((st.st_dev, st.st_ino) == self.wheelhouse_id, 'WHEELHOUSE_ALLOCATION_COLLISION') + for name in filenames: + fd = os.open(name, os.O_RDWR | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600, + dir_fd=self.wheelhouse_fd) + self.stack.callback(os.close, fd) + st = os.fstat(fd) + self.archive_nodes[name] = {'fd': fd, 'identity': (st.st_dev, st.st_ino)} + except BaseException as error: + try: + cleanup = self.cleanup() if self.created else { + 'complete': True, 'exclusive_root_created': False, 'outer_owner_custody_required': False} + except BaseException as cleanup_error: + cleanup = {'complete': False, 'outer_owner_custody_required': True, + 'refusal_code': type(cleanup_error).__name__} + # Even an incomplete constructor returns the exact object/handles. + # The caller must retain it until the reported tree custody closes. + raise AllocationRefusal(self, cleanup, error) from error + + def scope(self) -> None: + try: + st = os.stat(self.path.name, dir_fd=self.parent_fd, follow_symlinks=False) + except FileNotFoundError: + raise Refusal('ROOT_SCOPE_MISSING') from None + require(stat.S_ISDIR(st.st_mode) and st.st_uid == os.geteuid() + and (st.st_dev, st.st_ino) == self.root_id, 'ROOT_SCOPE_COLLISION') + try: + st = os.stat('wheelhouse', dir_fd=self.root_fd, follow_symlinks=False) + except FileNotFoundError: + raise Refusal('WHEELHOUSE_SCOPE_MISSING') from None + require(stat.S_ISDIR(st.st_mode) and st.st_uid == os.geteuid() + and (st.st_dev, st.st_ino) == self.wheelhouse_id, 'WHEELHOUSE_SCOPE_COLLISION') + + def node(self, name: str) -> os.stat_result: + self.scope() + expected = self.archive_nodes[name] + st = os.stat(name, dir_fd=self.wheelhouse_fd, follow_symlinks=False) + require(stat.S_ISREG(st.st_mode) and st.st_uid == os.geteuid() and st.st_nlink == 1 + and (st.st_dev, st.st_ino) == expected['identity'], 'ARCHIVE_SCOPE_COLLISION') + descriptor = os.fstat(expected['fd']) + require((descriptor.st_dev, descriptor.st_ino) == expected['identity'], 'ARCHIVE_FD_CHANGED') + return st + + def duplicate_for_owner(self, name: str) -> int: + """Only valid after a future ready/authority handshake; no owner is launched here.""" + self.node(name) + return os.dup(self.archive_nodes[name]['fd']) + + def verify_archive_bytes(self, expected: dict[str, dict]) -> list[dict]: + """Raw tuple check only. This does not accept tool metadata or an acquisition.""" + require(set(expected) == set(self.archive_nodes), 'INCOMPLETE_ARCHIVE_SET') + self.scope() + require(set(os.listdir(self.wheelhouse_fd)) == set(expected), 'EXTRA_OR_MISSING_ARCHIVE_NODE') + rows = [] + for name, wanted in expected.items(): + st = self.node(name) + require(type(wanted.get('bytes')) is int and 0 < wanted['bytes'] <= 2 * 1024**2 + and st.st_size == wanted['bytes'] and stat.S_IMODE(st.st_mode) == 0o400, + 'ARCHIVE_SIZE_OR_MODE_CHANGED') + fd = self.archive_nodes[name]['fd'] + raw = os.pread(fd, wanted['bytes'] + 1, 0) + after = os.fstat(fd) + require((st.st_size, st.st_mtime_ns, st.st_ctime_ns) + == (after.st_size, after.st_mtime_ns, after.st_ctime_ns) + and len(raw) == st.st_size and hashlib.sha256(raw).hexdigest() == wanted['sha256'], + 'ARCHIVE_BYTES_CHANGED') + rows.append({'filename': name, 'bytes': len(raw), 'sha256': wanted['sha256']}) + require(sum(row['bytes'] for row in rows) <= ARCHIVE_CAP, 'ARCHIVE_TOTAL_BOUND') + return rows + + def cleanup_archives(self) -> dict: + """Only the archive namespace; never claims root-tree completion.""" + result = {'complete': False, 'removed': [], 'already_absent': [], 'collisions': []} + self.scope() + os.fchmod(self.wheelhouse_fd, 0o700) + for name in self.archive_nodes: + try: + self.node(name) + except FileNotFoundError: + if os.fstat(self.archive_nodes[name]['fd']).st_nlink == 0: + result['already_absent'].append(name) + else: + result['collisions'].append({'filename': name, 'code': 'OWNED_NODE_STILL_LINKED'}) + except Refusal as error: + result['collisions'].append({'filename': name, 'code': str(error)}) + else: + os.unlink(name, dir_fd=self.wheelhouse_fd) + result['removed'].append(name) + result['remaining_names'] = sorted(os.listdir(self.wheelhouse_fd)) + self.scope() + result['complete'] = not result['remaining_names'] and not result['collisions'] + return result + + def cleanup(self) -> dict: + """Close the whole allocated tree; unknown members retain explicit custody.""" + result = {'complete': False, 'removed_directories': [], 'collisions': [], + 'outer_owner_custody_required': True} + if not self.created: + result.update(complete=True, outer_owner_custody_required=False) + return result + try: + named = os.stat(self.path.name, dir_fd=self.parent_fd, follow_symlinks=False) + except FileNotFoundError: + # A vanished name is completion only if the exact created inode is + # also unlinked; replacement/move is retained uncertain custody. + if self.root_fd is not None and os.fstat(self.root_fd).st_nlink == 0: + result.update(complete=True, outer_owner_custody_required=False) + else: + result['collisions'].append('ROOT_NAME_MISSING_OR_MOVED') + return result + if not (stat.S_ISDIR(named.st_mode) and named.st_uid == os.geteuid() + and (named.st_dev, named.st_ino) == self.root_id): + result['collisions'].append('ROOT_SCOPE_COLLISION') + return result + if self.root_fd is None: + self.root_fd = os.open(self.path.name, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, + dir_fd=self.parent_fd) + self.stack.callback(os.close, self.root_fd) + current = os.fstat(self.root_fd) + require((current.st_dev, current.st_ino) == self.root_id, 'ROOT_FD_COLLISION') + os.fchmod(self.root_fd, 0o700) + root_names = set(os.listdir(self.root_fd)) + result['unknown_root_names'] = sorted(root_names - ({'wheelhouse'} if self.wheelhouse_id else set())) + if self.wheelhouse_id is not None: + try: + st = os.stat('wheelhouse', dir_fd=self.root_fd, follow_symlinks=False) + except FileNotFoundError: + if self.wheelhouse_fd is None or os.fstat(self.wheelhouse_fd).st_nlink != 0: + result['collisions'].append('WHEELHOUSE_NAME_MISSING_OR_MOVED') + else: + if not (stat.S_ISDIR(st.st_mode) and st.st_uid == os.geteuid() + and (st.st_dev, st.st_ino) == self.wheelhouse_id): + result['collisions'].append('WHEELHOUSE_SCOPE_COLLISION') + else: + if self.wheelhouse_fd is None: + self.wheelhouse_fd = os.open('wheelhouse', os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, + dir_fd=self.root_fd) + self.stack.callback(os.close, self.wheelhouse_fd) + archives = self.cleanup_archives() + result['archive_cleanup'] = archives + if archives['complete']: + self.scope() + os.rmdir('wheelhouse', dir_fd=self.root_fd) + result['removed_directories'].append('wheelhouse') + else: + result['collisions'].append('ARCHIVE_NAMESPACE_RETAINED') + result['remaining_root_names'] = sorted(os.listdir(self.root_fd)) + if not result['remaining_root_names'] and not result['collisions']: + st = os.stat(self.path.name, dir_fd=self.parent_fd, follow_symlinks=False) + require((st.st_dev, st.st_ino) == self.root_id, 'ROOT_REMOVAL_COLLISION') + os.rmdir(self.path.name, dir_fd=self.parent_fd) + result['removed_directories'].append('.') + result['complete'] = os.fstat(self.root_fd).st_nlink == 0 + result['outer_owner_custody_required'] = not result['complete'] + return result + + def close(self) -> None: + self.stack.close() + + +def watch_owner_eof(custody: ArchiveCustody, owner_pipe: int, deadline: float, ready_pipe: int, + owner_pidfd: int, expected_pid: int, cleanup_ceiling: float) -> dict: + """Independent process primitive; deadline is anchored before any future allocation. + + No success/preserve message is accepted. Until reviewed owner integration and + terminal acceptance exist, this prototype ALWAYS cleans up on EOF/deadline. + """ + require(dict(os.environ) == MINIMAL_ENV, 'CLOSED_GUARDIAN_ENV_REQUIRED') + require(time.monotonic() < deadline <= time.monotonic() + 300, 'INVALID_ABSOLUTE_DEADLINE') + require(deadline < cleanup_ceiling <= deadline + 30, 'INVALID_CLEANUP_CEILING') + require(os.readlink(f'/proc/self/fd/{owner_pidfd}') == 'anon_inode:[pidfd]', 'OWNER_PIDFD_REQUIRED') + info = Path(f'/proc/self/fdinfo/{owner_pidfd}').read_text() + require(re.search(r'^Pid:\s*' + re.escape(str(expected_pid)) + r'\s*$', info, re.M) is not None + and expected_pid > 1 and expected_pid != os.getpid(), 'EXACT_OWNER_PIDFD_REQUIRED') + custody.scope() + os.write(ready_pipe, b'READY\n') + os.close(ready_pipe) + reason = 'ABSOLUTE_DEADLINE' + while time.monotonic() < deadline: + available, _, _ = select.select([owner_pipe], [], [], max(0, deadline - time.monotonic())) + if available: + data = os.read(owner_pipe, 1) + reason = 'OWNER_EOF' if not data else 'UNSUPPORTED_OWNER_MESSAGE' + break + # A pidfd refers to the created process, so PID reuse cannot retarget this stop. + if not select.select([owner_pidfd], [], [], 0)[0]: + try: + signal.pidfd_send_signal(owner_pidfd, signal.SIGKILL) + except ProcessLookupError: + pass + owner_absent = bool(select.select([owner_pidfd], [], [], max(0, min(5, cleanup_ceiling - time.monotonic())))[0]) + if not owner_absent: + # A surviving process may retain writable descriptors. Keep its named + # nodes in custody for the outer owner; unlinking them is not completion. + result = {'complete': False, 'deferred_until_exact_owner_exit': True, + 'refusal_code': 'OWNER_EXIT_UNCONFIRMED', + 'retained_owned_names': sorted(custody.archive_nodes)} + else: + try: + result = custody.cleanup() + except BaseException as error: + result = {'complete': False, 'refusal_code': str(error) if isinstance(error, Refusal) + else 'EXCEPTION_' + type(error).__name__.upper()} + within_ceiling = time.monotonic() < cleanup_ceiling + terminal_complete = owner_absent and bool(result['complete']) and within_ceiling + return {'reason': reason, 'archive_cleanup': result, 'owner_process_exited': owner_absent, + 'within_cleanup_ceiling': within_ceiling, + 'terminal_cleanup_complete': terminal_complete, + 'outer_owner_custody_required': not terminal_complete, + 'acquisition_accepted': False} + + +if __name__ == '__main__': + raise SystemExit('SOURCE_PROPOSAL_ONLY; owner adapter, launcher, acceptance and exact new authority are pending') diff --git a/qualification/issue-184/tool-acquisition/184-acquisition-custody-guardian-v4-proposal.py b/qualification/issue-184/tool-acquisition/184-acquisition-custody-guardian-v4-proposal.py new file mode 100644 index 0000000..a3d0210 --- /dev/null +++ b/qualification/issue-184/tool-acquisition/184-acquisition-custody-guardian-v4-proposal.py @@ -0,0 +1,387 @@ +"""Source-only custody primitive. No acquisition launcher or acceptance path exists here. + +The guardian preallocates archive nodes before handing any writable descriptor +to an owner, so owner death cannot leave an unregistered new archive. A future +owner adapter and exact authority/descriptor handoff require separate review. +""" +from __future__ import annotations + +from contextlib import ExitStack +import hashlib +import json +import os +from pathlib import Path +import re +import select +import signal +import stat +import time + +MINIMAL_ENV = {'PATH': '/usr/bin:/bin', 'LANG': 'C.UTF-8', 'LC_ALL': 'C.UTF-8', 'TZ': 'UTC'} +OLD_DECISION = 'https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-6649046802' +SELECTED_SOURCE = '14adc40b00b68cc8a3e1ae5647a885a42f273650' +ARCHIVE_CAP = 3 * 1024**2 + + +class Refusal(ValueError): + pass + + +class AllocationRefusal(Refusal): + """Partial construction returns custody instead of discarding live handles.""" + def __init__(self, custody, cleanup, cause): + super().__init__('ALLOCATION_REFUSED') + self.custody, self.cleanup = custody, cleanup + self.cause_type = type(cause).__name__ + + +def require(value: bool, code: str) -> None: + if not value: + raise Refusal(code) + + +AUTHORITY_FIELDS = {'decision', 'selected_source', 'inputs', 'environment', 'host', + 'total_seconds', 'io_seconds', 'retries', 'survivor_policy', + 'pool_parent', 'launcher_sha256', 'cpu_affinity'} +AUTHORITY_FIELDS.add('retention_deadline_utc') +INPUT_FIELDS = {'owner', 'bootstrap', 'guardian', 'terminal', 'selection', 'interpreter', 'library', 'ca'} + + +def unique_pairs(values): + result = {} + for key, value in values: + require(key not in result, 'DUPLICATE_AUTHORITY_KEY') + result[key] = value + return result + + +def reject_constant(value): + raise Refusal('NONFINITE_AUTHORITY_NUMBER') + + +def decode_authority(raw: bytes) -> dict: + authority = json.loads(raw.decode('utf-8'), object_pairs_hook=unique_pairs, parse_constant=reject_constant) + require(type(authority) is dict and set(authority) == AUTHORITY_FIELDS, 'EXACT_AUTHORITY_SCHEMA_REQUIRED') + require(type(authority['inputs']) is dict and set(authority['inputs']) == INPUT_FIELDS + and all(type(value) is str and re.fullmatch(r'[0-9a-f]{64}', value) + for value in authority['inputs'].values()), 'EXACT_INPUT_SCHEMA_REQUIRED') + require(type(authority['environment']) is dict and set(authority['environment']) == set(MINIMAL_ENV) + and all(type(value) is str for value in authority['environment'].values()), + 'EXACT_ENVIRONMENT_SCHEMA_REQUIRED') + require(all(type(authority[key]) is int for key in ('total_seconds', 'io_seconds', 'retries')), + 'INTEGER_AUTHORITY_LIMITS_REQUIRED') + require(all(type(authority[key]) is str for key in ('decision', 'selected_source', 'host', + 'survivor_policy', 'pool_parent', 'launcher_sha256')) + and re.fullmatch(r'[0-9a-f]{64}', authority['launcher_sha256']), 'EXACT_AUTHORITY_VALUE_TYPES_REQUIRED') + require(type(authority['retention_deadline_utc']) is str + and re.fullmatch(r'[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z', + authority['retention_deadline_utc']), 'EXACT_RETENTION_DEADLINE_REQUIRED') + affinity = authority['cpu_affinity'] + require(type(affinity) is list and 1 <= len(affinity) <= 2 + and all(type(cpu) is int and cpu >= 0 for cpu in affinity) + and len(set(affinity)) == len(affinity), 'EXACT_CPU_SCHEMA_REQUIRED') + return authority + + +def bind_authority(raw: bytes, expected_sha: str, expected_decision: str, expected_inputs: dict) -> dict: + """Expected values must come from the separately reviewed exact future invocation.""" + require(len(raw) <= 32768 and hashlib.sha256(raw).hexdigest() == expected_sha, 'AUTHORITY_BYTES_CHANGED') + authority = decode_authority(raw) + require(type(expected_inputs) is dict and set(expected_inputs) == INPUT_FIELDS, + 'REVIEWED_INPUT_SCHEMA_REQUIRED') + require(isinstance(expected_decision, str) and expected_decision != OLD_DECISION + and re.fullmatch(r'https://github\.com/durable-workflow/sdk-python/issues/184#issuecomment-[1-9][0-9]{5,17}', expected_decision) + and authority.get('decision') == expected_decision, 'EXACT_NEW_DECISION_REQUIRED') + require(authority.get('selected_source') == SELECTED_SOURCE + and authority.get('inputs') == expected_inputs, 'EXACT_INPUT_AUTHORITY_CHANGED') + require(authority.get('environment') == MINIMAL_ENV and authority.get('host') == 'lab4' + and authority.get('total_seconds') == 300 and authority.get('io_seconds') == 270 + and authority.get('retries') == 0, 'OPERATION_BOUNDARY_CHANGED') + require(authority['survivor_policy'] == 'retain-custody-and-escalate' + and authority['pool_parent'] == '/home/lab4/fleet/tmp', 'EXACT_CUSTODY_POLICY_REQUIRED') + return authority + + +class ArchiveCustody: + """Owns only a newly exclusive root and exact preallocated archive inodes.""" + def __init__(self, parent: Path, leaf: str, filenames: list[str]): + require(re.fullmatch(r'sdk-python-184-tools-guardian-[a-zA-Z0-9-]{1,96}', leaf) is not None, + 'INVALID_EXCLUSIVE_ROOT_NAME') + require(1 <= len(filenames) <= 6 and len(set(filenames)) == len(filenames) + and all(re.fullmatch(r'[a-zA-Z0-9_.-]{1,160}\.whl', name) for name in filenames), + 'INVALID_ARCHIVE_NAMES') + self.path = parent / leaf + self.stack = ExitStack() + self.archive_nodes: dict[str, dict] = {} + self.root_id = None + self.wheelhouse_id = None + self.created = False + self.allocated_directory_names = set() + self.root_fd = None + self.wheelhouse_fd = None + # Directory descriptors cannot be wrapped in streams; keep explicit close custody. + self.parent_fd = os.open(parent, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) + self.stack.callback(os.close, self.parent_fd) + try: + st = os.fstat(self.parent_fd) + require(st.st_uid == os.geteuid() and not st.st_mode & 0o022, 'UNSAFE_POOL_PARENT') + os.mkdir(leaf, 0o700, dir_fd=self.parent_fd) + self.created = True + st = os.stat(leaf, dir_fd=self.parent_fd, follow_symlinks=False) + self.root_id = (st.st_dev, st.st_ino) + self.root_fd = os.open(leaf, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=self.parent_fd) + self.stack.callback(os.close, self.root_fd) + st = os.fstat(self.root_fd) + require((st.st_dev, st.st_ino) == self.root_id, 'ROOT_ALLOCATION_COLLISION') + os.mkdir('wheelhouse', 0o700, dir_fd=self.root_fd) + self.allocated_directory_names.add('wheelhouse') + st = os.stat('wheelhouse', dir_fd=self.root_fd, follow_symlinks=False) + self.wheelhouse_id = (st.st_dev, st.st_ino) + self.wheelhouse_fd = os.open('wheelhouse', os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=self.root_fd) + self.stack.callback(os.close, self.wheelhouse_fd) + st = os.fstat(self.wheelhouse_fd) + require((st.st_dev, st.st_ino) == self.wheelhouse_id, 'WHEELHOUSE_ALLOCATION_COLLISION') + for name in filenames: + fd = os.open(name, os.O_RDWR | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600, + dir_fd=self.wheelhouse_fd) + self.stack.callback(os.close, fd) + st = os.fstat(fd) + self.archive_nodes[name] = {'fd': fd, 'identity': (st.st_dev, st.st_ino)} + except BaseException as error: + try: + cleanup = self.cleanup() if self.created else { + 'complete': True, 'exclusive_root_created': False, 'outer_owner_custody_required': False} + except BaseException as cleanup_error: + cleanup = {'complete': False, 'outer_owner_custody_required': True, + 'refusal_code': type(cleanup_error).__name__} + # Even an incomplete constructor returns the exact object/handles. + # The caller must retain it until the reported tree custody closes. + raise AllocationRefusal(self, cleanup, error) from error + + def scope(self) -> None: + try: + st = os.stat(self.path.name, dir_fd=self.parent_fd, follow_symlinks=False) + except FileNotFoundError: + raise Refusal('ROOT_SCOPE_MISSING') from None + require(stat.S_ISDIR(st.st_mode) and st.st_uid == os.geteuid() + and (st.st_dev, st.st_ino) == self.root_id, 'ROOT_SCOPE_COLLISION') + try: + st = os.stat('wheelhouse', dir_fd=self.root_fd, follow_symlinks=False) + except FileNotFoundError: + raise Refusal('WHEELHOUSE_SCOPE_MISSING') from None + require(stat.S_ISDIR(st.st_mode) and st.st_uid == os.geteuid() + and (st.st_dev, st.st_ino) == self.wheelhouse_id, 'WHEELHOUSE_SCOPE_COLLISION') + + def node(self, name: str) -> os.stat_result: + self.scope() + expected = self.archive_nodes[name] + st = os.stat(name, dir_fd=self.wheelhouse_fd, follow_symlinks=False) + require(stat.S_ISREG(st.st_mode) and st.st_uid == os.geteuid() and st.st_nlink == 1 + and (st.st_dev, st.st_ino) == expected['identity'], 'ARCHIVE_SCOPE_COLLISION') + descriptor = os.fstat(expected['fd']) + require((descriptor.st_dev, descriptor.st_ino) == expected['identity'], 'ARCHIVE_FD_CHANGED') + return st + + def duplicate_for_owner(self, name: str) -> int: + """Only valid after a future ready/authority handshake; no owner is launched here.""" + self.node(name) + return os.dup(self.archive_nodes[name]['fd']) + + def verify_archive_bytes(self, expected: dict[str, dict]) -> list[dict]: + """Raw tuple check only. This does not accept tool metadata or an acquisition.""" + require(set(expected) == set(self.archive_nodes), 'INCOMPLETE_ARCHIVE_SET') + self.scope() + require(set(os.listdir(self.wheelhouse_fd)) == set(expected), 'EXTRA_OR_MISSING_ARCHIVE_NODE') + rows = [] + for name, wanted in expected.items(): + st = self.node(name) + require(type(wanted.get('bytes')) is int and 0 < wanted['bytes'] <= 2 * 1024**2 + and st.st_size == wanted['bytes'] and stat.S_IMODE(st.st_mode) == 0o400, + 'ARCHIVE_SIZE_OR_MODE_CHANGED') + fd = self.archive_nodes[name]['fd'] + raw = os.pread(fd, wanted['bytes'] + 1, 0) + after = os.fstat(fd) + require((st.st_size, st.st_mtime_ns, st.st_ctime_ns) + == (after.st_size, after.st_mtime_ns, after.st_ctime_ns) + and len(raw) == st.st_size and hashlib.sha256(raw).hexdigest() == wanted['sha256'], + 'ARCHIVE_BYTES_CHANGED') + rows.append({'filename': name, 'bytes': len(raw), 'sha256': wanted['sha256']}) + require(sum(row['bytes'] for row in rows) <= ARCHIVE_CAP, 'ARCHIVE_TOTAL_BOUND') + return rows + + def cleanup_archives(self) -> dict: + """Only the archive namespace; never claims root-tree completion.""" + result = {'complete': False, 'removed': [], 'already_absent': [], 'collisions': []} + self.scope() + os.fchmod(self.wheelhouse_fd, 0o700) + for name in self.archive_nodes: + try: + self.node(name) + except FileNotFoundError: + if os.fstat(self.archive_nodes[name]['fd']).st_nlink == 0: + result['already_absent'].append(name) + else: + result['collisions'].append({'filename': name, 'code': 'OWNED_NODE_STILL_LINKED'}) + except Refusal as error: + result['collisions'].append({'filename': name, 'code': str(error)}) + else: + os.unlink(name, dir_fd=self.wheelhouse_fd) + result['removed'].append(name) + result['remaining_names'] = sorted(os.listdir(self.wheelhouse_fd)) + self.scope() + result['complete'] = not result['remaining_names'] and not result['collisions'] + return result + + def unlink_state(self) -> dict: + """Root absence cannot stand in for unlink proof of tracked descendants.""" + directories = {'.': (self.root_fd, self.root_id)} + if self.wheelhouse_id is not None or 'wheelhouse' in self.allocated_directory_names: + directories['wheelhouse'] = (self.wheelhouse_fd, self.wheelhouse_id) + for name, identity in getattr(self, 'dir_ids', {}).items(): + if name not in directories: + directories[name] = (getattr(self, 'directory_fds', {}).get(name), tuple(identity)) + for name in self.allocated_directory_names: + directories.setdefault(name, (None, None)) + tracked = [(name, fd, identity, stat.S_ISDIR) for name, (fd, identity) in directories.items() + ] + tracked += [('archive:' + name, row['fd'], row['identity'], stat.S_ISREG) + for name, row in self.archive_nodes.items()] + retained = [] + for name, fd, identity, right_type in tracked: + try: + st = os.fstat(fd) + proven = (right_type(st.st_mode) and st.st_uid == os.geteuid() + and (st.st_dev, st.st_ino) == identity and st.st_nlink == 0) + except (OSError, TypeError): + proven = False + if not proven: + retained.append(name) + return {'all_unlinked': bool(self.root_id is not None) and not retained, + 'retained_or_unproven_inodes': sorted(retained)} + + def finish_unlinked(self, result: dict) -> dict: + state = self.unlink_state() + result.update(state) + result['complete'] = state['all_unlinked'] + result['outer_owner_custody_required'] = not result['complete'] + if not result['complete']: + result['refusal_code'] = 'TRACKED_INODES_STILL_LINKED_OR_UNPROVEN' + return result + + def cleanup(self) -> dict: + """Close the whole allocated tree; unknown members retain explicit custody.""" + result = {'complete': False, 'removed_directories': [], 'collisions': [], + 'outer_owner_custody_required': True} + if not self.created: + result.update(complete=True, outer_owner_custody_required=False) + return result + try: + named = os.stat(self.path.name, dir_fd=self.parent_fd, follow_symlinks=False) + except FileNotFoundError: + # A vanished name is completion only if the exact created inode is + # also unlinked; replacement/move is retained uncertain custody. + return self.finish_unlinked(result) + if not (stat.S_ISDIR(named.st_mode) and named.st_uid == os.geteuid() + and (named.st_dev, named.st_ino) == self.root_id): + result['collisions'].append('ROOT_SCOPE_COLLISION') + return result + if self.root_fd is None: + self.root_fd = os.open(self.path.name, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, + dir_fd=self.parent_fd) + self.stack.callback(os.close, self.root_fd) + current = os.fstat(self.root_fd) + require((current.st_dev, current.st_ino) == self.root_id, 'ROOT_FD_COLLISION') + os.fchmod(self.root_fd, 0o700) + root_names = set(os.listdir(self.root_fd)) + result['unknown_root_names'] = sorted(root_names - ({'wheelhouse'} if self.wheelhouse_id else set())) + if self.wheelhouse_id is not None: + try: + st = os.stat('wheelhouse', dir_fd=self.root_fd, follow_symlinks=False) + except FileNotFoundError: + if self.wheelhouse_fd is None or os.fstat(self.wheelhouse_fd).st_nlink != 0: + result['collisions'].append('WHEELHOUSE_NAME_MISSING_OR_MOVED') + else: + if not (stat.S_ISDIR(st.st_mode) and st.st_uid == os.geteuid() + and (st.st_dev, st.st_ino) == self.wheelhouse_id): + result['collisions'].append('WHEELHOUSE_SCOPE_COLLISION') + else: + if self.wheelhouse_fd is None: + self.wheelhouse_fd = os.open('wheelhouse', os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, + dir_fd=self.root_fd) + self.stack.callback(os.close, self.wheelhouse_fd) + archives = self.cleanup_archives() + result['archive_cleanup'] = archives + if archives['complete']: + self.scope() + os.rmdir('wheelhouse', dir_fd=self.root_fd) + result['removed_directories'].append('wheelhouse') + else: + result['collisions'].append('ARCHIVE_NAMESPACE_RETAINED') + result['remaining_root_names'] = sorted(os.listdir(self.root_fd)) + if not result['remaining_root_names'] and not result['collisions']: + st = os.stat(self.path.name, dir_fd=self.parent_fd, follow_symlinks=False) + require((st.st_dev, st.st_ino) == self.root_id, 'ROOT_REMOVAL_COLLISION') + os.rmdir(self.path.name, dir_fd=self.parent_fd) + result['removed_directories'].append('.') + return self.finish_unlinked(result) + result['outer_owner_custody_required'] = not result['complete'] + return result + + def close(self) -> None: + self.stack.close() + + +def watch_owner_eof(custody: ArchiveCustody, owner_pipe: int, deadline: float, ready_pipe: int, + owner_pidfd: int, expected_pid: int, cleanup_ceiling: float) -> dict: + """Independent process primitive; deadline is anchored before any future allocation. + + No success/preserve message is accepted. Until reviewed owner integration and + terminal acceptance exist, this prototype ALWAYS cleans up on EOF/deadline. + """ + require(dict(os.environ) == MINIMAL_ENV, 'CLOSED_GUARDIAN_ENV_REQUIRED') + require(time.monotonic() < deadline <= time.monotonic() + 300, 'INVALID_ABSOLUTE_DEADLINE') + require(deadline < cleanup_ceiling <= deadline + 30, 'INVALID_CLEANUP_CEILING') + require(os.readlink(f'/proc/self/fd/{owner_pidfd}') == 'anon_inode:[pidfd]', 'OWNER_PIDFD_REQUIRED') + info = Path(f'/proc/self/fdinfo/{owner_pidfd}').read_text() + require(re.search(r'^Pid:\s*' + re.escape(str(expected_pid)) + r'\s*$', info, re.M) is not None + and expected_pid > 1 and expected_pid != os.getpid(), 'EXACT_OWNER_PIDFD_REQUIRED') + custody.scope() + os.write(ready_pipe, b'READY\n') + os.close(ready_pipe) + reason = 'ABSOLUTE_DEADLINE' + while time.monotonic() < deadline: + available, _, _ = select.select([owner_pipe], [], [], max(0, deadline - time.monotonic())) + if available: + data = os.read(owner_pipe, 1) + reason = 'OWNER_EOF' if not data else 'UNSUPPORTED_OWNER_MESSAGE' + break + # A pidfd refers to the created process, so PID reuse cannot retarget this stop. + if not select.select([owner_pidfd], [], [], 0)[0]: + try: + signal.pidfd_send_signal(owner_pidfd, signal.SIGKILL) + except ProcessLookupError: + pass + owner_absent = bool(select.select([owner_pidfd], [], [], max(0, min(5, cleanup_ceiling - time.monotonic())))[0]) + if not owner_absent: + # A surviving process may retain writable descriptors. Keep its named + # nodes in custody for the outer owner; unlinking them is not completion. + result = {'complete': False, 'deferred_until_exact_owner_exit': True, + 'refusal_code': 'OWNER_EXIT_UNCONFIRMED', + 'retained_owned_names': sorted(custody.archive_nodes)} + else: + try: + result = custody.cleanup() + except BaseException as error: + result = {'complete': False, 'refusal_code': str(error) if isinstance(error, Refusal) + else 'EXCEPTION_' + type(error).__name__.upper()} + within_ceiling = time.monotonic() < cleanup_ceiling + terminal_complete = owner_absent and bool(result['complete']) and within_ceiling + return {'reason': reason, 'archive_cleanup': result, 'owner_process_exited': owner_absent, + 'within_cleanup_ceiling': within_ceiling, + 'terminal_cleanup_complete': terminal_complete, + 'outer_owner_custody_required': not terminal_complete, + 'acquisition_accepted': False} + + +if __name__ == '__main__': + raise SystemExit('SOURCE_PROPOSAL_ONLY; owner adapter, launcher, acceptance and exact new authority are pending') diff --git a/qualification/issue-184/tool-acquisition/184-acquisition-custody-guardian-v5-proposal.py b/qualification/issue-184/tool-acquisition/184-acquisition-custody-guardian-v5-proposal.py new file mode 100644 index 0000000..94111c1 --- /dev/null +++ b/qualification/issue-184/tool-acquisition/184-acquisition-custody-guardian-v5-proposal.py @@ -0,0 +1,392 @@ +"""Source-only custody primitive. No acquisition launcher or acceptance path exists here. + +The guardian preallocates archive nodes before handing any writable descriptor +to an owner, so owner death cannot leave an unregistered new archive. A future +owner adapter and exact authority/descriptor handoff require separate review. +""" +from __future__ import annotations + +from contextlib import ExitStack +import hashlib +import json +import os +from pathlib import Path +import re +import select +import signal +import stat +import time + +MINIMAL_ENV = {'PATH': '/usr/bin:/bin', 'LANG': 'C.UTF-8', 'LC_ALL': 'C.UTF-8', 'TZ': 'UTC'} +OLD_DECISION = 'https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-6649046802' +SELECTED_SOURCE = '14adc40b00b68cc8a3e1ae5647a885a42f273650' +ARCHIVE_CAP = 3 * 1024**2 + + +class Refusal(ValueError): + pass + + +class AllocationRefusal(Refusal): + """Partial construction returns custody instead of discarding live handles.""" + def __init__(self, custody, cleanup, cause): + super().__init__('ALLOCATION_REFUSED') + self.custody, self.cleanup = custody, cleanup + self.cause_type = type(cause).__name__ + + +def require(value: bool, code: str) -> None: + if not value: + raise Refusal(code) + + +AUTHORITY_FIELDS = {'decision', 'selected_source', 'inputs', 'environment', 'host', + 'total_seconds', 'io_seconds', 'retries', 'survivor_policy', + 'pool_parent', 'launcher_sha256', 'cpu_affinity'} +AUTHORITY_FIELDS.add('retention_deadline_utc') +INPUT_FIELDS = {'owner', 'bootstrap', 'guardian', 'terminal', 'selection', 'interpreter', 'library', 'ca'} + + +def unique_pairs(values): + result = {} + for key, value in values: + require(key not in result, 'DUPLICATE_AUTHORITY_KEY') + result[key] = value + return result + + +def reject_constant(value): + raise Refusal('NONFINITE_AUTHORITY_NUMBER') + + +def decode_authority(raw: bytes) -> dict: + authority = json.loads(raw.decode('utf-8'), object_pairs_hook=unique_pairs, parse_constant=reject_constant) + require(type(authority) is dict and set(authority) == AUTHORITY_FIELDS, 'EXACT_AUTHORITY_SCHEMA_REQUIRED') + require(type(authority['inputs']) is dict and set(authority['inputs']) == INPUT_FIELDS + and all(type(value) is str and re.fullmatch(r'[0-9a-f]{64}', value) + for value in authority['inputs'].values()), 'EXACT_INPUT_SCHEMA_REQUIRED') + require(type(authority['environment']) is dict and set(authority['environment']) == set(MINIMAL_ENV) + and all(type(value) is str for value in authority['environment'].values()), + 'EXACT_ENVIRONMENT_SCHEMA_REQUIRED') + require(all(type(authority[key]) is int for key in ('total_seconds', 'io_seconds', 'retries')), + 'INTEGER_AUTHORITY_LIMITS_REQUIRED') + require(all(type(authority[key]) is str for key in ('decision', 'selected_source', 'host', + 'survivor_policy', 'pool_parent', 'launcher_sha256')) + and re.fullmatch(r'[0-9a-f]{64}', authority['launcher_sha256']), 'EXACT_AUTHORITY_VALUE_TYPES_REQUIRED') + require(type(authority['retention_deadline_utc']) is str + and re.fullmatch(r'[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z', + authority['retention_deadline_utc']), 'EXACT_RETENTION_DEADLINE_REQUIRED') + affinity = authority['cpu_affinity'] + require(type(affinity) is list and 1 <= len(affinity) <= 2 + and all(type(cpu) is int and cpu >= 0 for cpu in affinity) + and len(set(affinity)) == len(affinity), 'EXACT_CPU_SCHEMA_REQUIRED') + return authority + + +def bind_authority(raw: bytes, expected_sha: str, expected_decision: str, expected_inputs: dict) -> dict: + """Expected values must come from the separately reviewed exact future invocation.""" + require(len(raw) <= 32768 and hashlib.sha256(raw).hexdigest() == expected_sha, 'AUTHORITY_BYTES_CHANGED') + authority = decode_authority(raw) + require(type(expected_inputs) is dict and set(expected_inputs) == INPUT_FIELDS, + 'REVIEWED_INPUT_SCHEMA_REQUIRED') + require(isinstance(expected_decision, str) and expected_decision != OLD_DECISION + and re.fullmatch(r'https://github\.com/durable-workflow/sdk-python/issues/184#issuecomment-[1-9][0-9]{5,17}', expected_decision) + and authority.get('decision') == expected_decision, 'EXACT_NEW_DECISION_REQUIRED') + require(authority.get('selected_source') == SELECTED_SOURCE + and authority.get('inputs') == expected_inputs, 'EXACT_INPUT_AUTHORITY_CHANGED') + require(authority.get('environment') == MINIMAL_ENV and authority.get('host') == 'lab4' + and authority.get('total_seconds') == 300 and authority.get('io_seconds') == 270 + and authority.get('retries') == 0, 'OPERATION_BOUNDARY_CHANGED') + require(authority['survivor_policy'] == 'retain-custody-and-escalate' + and authority['pool_parent'] == '/home/lab4/fleet/tmp', 'EXACT_CUSTODY_POLICY_REQUIRED') + return authority + + +class ArchiveCustody: + """Owns only a newly exclusive root and exact preallocated archive inodes.""" + def __init__(self, parent: Path, leaf: str, filenames: list[str]): + require(re.fullmatch(r'sdk-python-184-tools-guardian-[a-zA-Z0-9-]{1,96}', leaf) is not None, + 'INVALID_EXCLUSIVE_ROOT_NAME') + require(1 <= len(filenames) <= 6 and len(set(filenames)) == len(filenames) + and all(re.fullmatch(r'[a-zA-Z0-9_.-]{1,160}\.whl', name) for name in filenames), + 'INVALID_ARCHIVE_NAMES') + self.path = parent / leaf + self.stack = ExitStack() + self.archive_nodes: dict[str, dict] = {} + self.root_id = None + self.wheelhouse_id = None + self.created = False + self.allocated_directory_names = set() + self.root_fd = None + self.wheelhouse_fd = None + # Directory descriptors cannot be wrapped in streams; keep explicit close custody. + self.parent_fd = os.open(parent, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) + self.stack.callback(os.close, self.parent_fd) + try: + st = os.fstat(self.parent_fd) + require(st.st_uid == os.geteuid() and not st.st_mode & 0o022, 'UNSAFE_POOL_PARENT') + os.mkdir(leaf, 0o700, dir_fd=self.parent_fd) + self.created = True + st = os.stat(leaf, dir_fd=self.parent_fd, follow_symlinks=False) + self.root_id = (st.st_dev, st.st_ino) + self.root_fd = os.open(leaf, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=self.parent_fd) + self.stack.callback(os.close, self.root_fd) + st = os.fstat(self.root_fd) + require((st.st_dev, st.st_ino) == self.root_id, 'ROOT_ALLOCATION_COLLISION') + os.mkdir('wheelhouse', 0o700, dir_fd=self.root_fd) + self.allocated_directory_names.add('wheelhouse') + st = os.stat('wheelhouse', dir_fd=self.root_fd, follow_symlinks=False) + self.wheelhouse_id = (st.st_dev, st.st_ino) + self.wheelhouse_fd = os.open('wheelhouse', os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=self.root_fd) + self.stack.callback(os.close, self.wheelhouse_fd) + st = os.fstat(self.wheelhouse_fd) + require((st.st_dev, st.st_ino) == self.wheelhouse_id, 'WHEELHOUSE_ALLOCATION_COLLISION') + for name in filenames: + fd = os.open(name, os.O_RDWR | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600, + dir_fd=self.wheelhouse_fd) + # Raw descriptor custody starts before callback/identity reads. + # Pending identity never permits handoff or named-node removal. + self.archive_nodes[name] = {'fd': fd, 'identity': None} + self.stack.callback(os.close, fd) + st = os.fstat(fd) + self.archive_nodes[name]['identity'] = (st.st_dev, st.st_ino) + except BaseException as error: + try: + cleanup = self.cleanup() if self.created else { + 'complete': True, 'exclusive_root_created': False, 'outer_owner_custody_required': False} + except BaseException as cleanup_error: + cleanup = {'complete': False, 'outer_owner_custody_required': True, + 'refusal_code': type(cleanup_error).__name__} + # Even an incomplete constructor returns the exact object/handles. + # The caller must retain it until the reported tree custody closes. + raise AllocationRefusal(self, cleanup, error) from error + + def scope(self) -> None: + try: + st = os.stat(self.path.name, dir_fd=self.parent_fd, follow_symlinks=False) + except FileNotFoundError: + raise Refusal('ROOT_SCOPE_MISSING') from None + require(stat.S_ISDIR(st.st_mode) and st.st_uid == os.geteuid() + and (st.st_dev, st.st_ino) == self.root_id, 'ROOT_SCOPE_COLLISION') + try: + st = os.stat('wheelhouse', dir_fd=self.root_fd, follow_symlinks=False) + except FileNotFoundError: + raise Refusal('WHEELHOUSE_SCOPE_MISSING') from None + require(stat.S_ISDIR(st.st_mode) and st.st_uid == os.geteuid() + and (st.st_dev, st.st_ino) == self.wheelhouse_id, 'WHEELHOUSE_SCOPE_COLLISION') + + def node(self, name: str) -> os.stat_result: + self.scope() + expected = self.archive_nodes[name] + st = os.stat(name, dir_fd=self.wheelhouse_fd, follow_symlinks=False) + require(expected['identity'] is not None, 'ARCHIVE_IDENTITY_UNPROVEN') + require(stat.S_ISREG(st.st_mode) and st.st_uid == os.geteuid() and st.st_nlink == 1 + and (st.st_dev, st.st_ino) == expected['identity'], 'ARCHIVE_SCOPE_COLLISION') + descriptor = os.fstat(expected['fd']) + require((descriptor.st_dev, descriptor.st_ino) == expected['identity'], 'ARCHIVE_FD_CHANGED') + return st + + def duplicate_for_owner(self, name: str) -> int: + """Only valid after a future ready/authority handshake; no owner is launched here.""" + self.node(name) + return os.dup(self.archive_nodes[name]['fd']) + + def verify_archive_bytes(self, expected: dict[str, dict]) -> list[dict]: + """Raw tuple check only. This does not accept tool metadata or an acquisition.""" + require(set(expected) == set(self.archive_nodes), 'INCOMPLETE_ARCHIVE_SET') + self.scope() + require(set(os.listdir(self.wheelhouse_fd)) == set(expected), 'EXTRA_OR_MISSING_ARCHIVE_NODE') + rows = [] + for name, wanted in expected.items(): + st = self.node(name) + require(type(wanted.get('bytes')) is int and 0 < wanted['bytes'] <= 2 * 1024**2 + and st.st_size == wanted['bytes'] and stat.S_IMODE(st.st_mode) == 0o400, + 'ARCHIVE_SIZE_OR_MODE_CHANGED') + fd = self.archive_nodes[name]['fd'] + raw = os.pread(fd, wanted['bytes'] + 1, 0) + after = os.fstat(fd) + require((st.st_size, st.st_mtime_ns, st.st_ctime_ns) + == (after.st_size, after.st_mtime_ns, after.st_ctime_ns) + and len(raw) == st.st_size and hashlib.sha256(raw).hexdigest() == wanted['sha256'], + 'ARCHIVE_BYTES_CHANGED') + rows.append({'filename': name, 'bytes': len(raw), 'sha256': wanted['sha256']}) + require(sum(row['bytes'] for row in rows) <= ARCHIVE_CAP, 'ARCHIVE_TOTAL_BOUND') + return rows + + def cleanup_archives(self) -> dict: + """Only the archive namespace; never claims root-tree completion.""" + result = {'complete': False, 'removed': [], 'already_absent': [], 'collisions': []} + self.scope() + os.fchmod(self.wheelhouse_fd, 0o700) + for name in self.archive_nodes: + try: + self.node(name) + except FileNotFoundError: + if os.fstat(self.archive_nodes[name]['fd']).st_nlink == 0: + result['already_absent'].append(name) + else: + result['collisions'].append({'filename': name, 'code': 'OWNED_NODE_STILL_LINKED'}) + except Refusal as error: + result['collisions'].append({'filename': name, 'code': str(error)}) + else: + os.unlink(name, dir_fd=self.wheelhouse_fd) + result['removed'].append(name) + result['remaining_names'] = sorted(os.listdir(self.wheelhouse_fd)) + self.scope() + result['complete'] = not result['remaining_names'] and not result['collisions'] + return result + + def unlink_state(self) -> dict: + """Root absence cannot stand in for unlink proof of tracked descendants.""" + directories = {'.': (self.root_fd, self.root_id)} + if self.wheelhouse_id is not None or 'wheelhouse' in self.allocated_directory_names: + directories['wheelhouse'] = (self.wheelhouse_fd, self.wheelhouse_id) + for name, identity in getattr(self, 'dir_ids', {}).items(): + if name not in directories: + directories[name] = (getattr(self, 'directory_fds', {}).get(name), tuple(identity)) + for name in self.allocated_directory_names: + directories.setdefault(name, (None, None)) + tracked = [(name, fd, identity, stat.S_ISDIR, False) for name, (fd, identity) in directories.items() + ] + tracked += [('archive:' + name, row['fd'], row['identity'], stat.S_ISREG, True) + for name, row in self.archive_nodes.items()] + retained = [] + for name, fd, identity, right_type, raw_archive in tracked: + try: + st = os.fstat(fd) + proven = (right_type(st.st_mode) and st.st_uid == os.geteuid() + and ((raw_archive and identity is None) or (st.st_dev, st.st_ino) == identity) + and st.st_nlink == 0) + except (OSError, TypeError): + proven = False + if not proven: + retained.append(name) + return {'all_unlinked': bool(self.root_id is not None) and not retained, + 'retained_or_unproven_inodes': sorted(retained)} + + def finish_unlinked(self, result: dict) -> dict: + state = self.unlink_state() + result.update(state) + result['complete'] = state['all_unlinked'] + result['outer_owner_custody_required'] = not result['complete'] + if not result['complete']: + result['refusal_code'] = 'TRACKED_INODES_STILL_LINKED_OR_UNPROVEN' + return result + + def cleanup(self) -> dict: + """Close the whole allocated tree; unknown members retain explicit custody.""" + result = {'complete': False, 'removed_directories': [], 'collisions': [], + 'outer_owner_custody_required': True} + if not self.created: + result.update(complete=True, outer_owner_custody_required=False) + return result + try: + named = os.stat(self.path.name, dir_fd=self.parent_fd, follow_symlinks=False) + except FileNotFoundError: + # A vanished name is completion only if the exact created inode is + # also unlinked; replacement/move is retained uncertain custody. + return self.finish_unlinked(result) + if not (stat.S_ISDIR(named.st_mode) and named.st_uid == os.geteuid() + and (named.st_dev, named.st_ino) == self.root_id): + result['collisions'].append('ROOT_SCOPE_COLLISION') + return result + if self.root_fd is None: + self.root_fd = os.open(self.path.name, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, + dir_fd=self.parent_fd) + self.stack.callback(os.close, self.root_fd) + current = os.fstat(self.root_fd) + require((current.st_dev, current.st_ino) == self.root_id, 'ROOT_FD_COLLISION') + os.fchmod(self.root_fd, 0o700) + root_names = set(os.listdir(self.root_fd)) + result['unknown_root_names'] = sorted(root_names - ({'wheelhouse'} if self.wheelhouse_id else set())) + if self.wheelhouse_id is not None: + try: + st = os.stat('wheelhouse', dir_fd=self.root_fd, follow_symlinks=False) + except FileNotFoundError: + if self.wheelhouse_fd is None or os.fstat(self.wheelhouse_fd).st_nlink != 0: + result['collisions'].append('WHEELHOUSE_NAME_MISSING_OR_MOVED') + else: + if not (stat.S_ISDIR(st.st_mode) and st.st_uid == os.geteuid() + and (st.st_dev, st.st_ino) == self.wheelhouse_id): + result['collisions'].append('WHEELHOUSE_SCOPE_COLLISION') + else: + if self.wheelhouse_fd is None: + self.wheelhouse_fd = os.open('wheelhouse', os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, + dir_fd=self.root_fd) + self.stack.callback(os.close, self.wheelhouse_fd) + archives = self.cleanup_archives() + result['archive_cleanup'] = archives + if archives['complete']: + self.scope() + os.rmdir('wheelhouse', dir_fd=self.root_fd) + result['removed_directories'].append('wheelhouse') + else: + result['collisions'].append('ARCHIVE_NAMESPACE_RETAINED') + result['remaining_root_names'] = sorted(os.listdir(self.root_fd)) + if not result['remaining_root_names'] and not result['collisions']: + st = os.stat(self.path.name, dir_fd=self.parent_fd, follow_symlinks=False) + require((st.st_dev, st.st_ino) == self.root_id, 'ROOT_REMOVAL_COLLISION') + os.rmdir(self.path.name, dir_fd=self.parent_fd) + result['removed_directories'].append('.') + return self.finish_unlinked(result) + result['outer_owner_custody_required'] = not result['complete'] + return result + + def close(self) -> None: + self.stack.close() + + +def watch_owner_eof(custody: ArchiveCustody, owner_pipe: int, deadline: float, ready_pipe: int, + owner_pidfd: int, expected_pid: int, cleanup_ceiling: float) -> dict: + """Independent process primitive; deadline is anchored before any future allocation. + + No success/preserve message is accepted. Until reviewed owner integration and + terminal acceptance exist, this prototype ALWAYS cleans up on EOF/deadline. + """ + require(dict(os.environ) == MINIMAL_ENV, 'CLOSED_GUARDIAN_ENV_REQUIRED') + require(time.monotonic() < deadline <= time.monotonic() + 300, 'INVALID_ABSOLUTE_DEADLINE') + require(deadline < cleanup_ceiling <= deadline + 30, 'INVALID_CLEANUP_CEILING') + require(os.readlink(f'/proc/self/fd/{owner_pidfd}') == 'anon_inode:[pidfd]', 'OWNER_PIDFD_REQUIRED') + info = Path(f'/proc/self/fdinfo/{owner_pidfd}').read_text() + require(re.search(r'^Pid:\s*' + re.escape(str(expected_pid)) + r'\s*$', info, re.M) is not None + and expected_pid > 1 and expected_pid != os.getpid(), 'EXACT_OWNER_PIDFD_REQUIRED') + custody.scope() + os.write(ready_pipe, b'READY\n') + os.close(ready_pipe) + reason = 'ABSOLUTE_DEADLINE' + while time.monotonic() < deadline: + available, _, _ = select.select([owner_pipe], [], [], max(0, deadline - time.monotonic())) + if available: + data = os.read(owner_pipe, 1) + reason = 'OWNER_EOF' if not data else 'UNSUPPORTED_OWNER_MESSAGE' + break + # A pidfd refers to the created process, so PID reuse cannot retarget this stop. + if not select.select([owner_pidfd], [], [], 0)[0]: + try: + signal.pidfd_send_signal(owner_pidfd, signal.SIGKILL) + except ProcessLookupError: + pass + owner_absent = bool(select.select([owner_pidfd], [], [], max(0, min(5, cleanup_ceiling - time.monotonic())))[0]) + if not owner_absent: + # A surviving process may retain writable descriptors. Keep its named + # nodes in custody for the outer owner; unlinking them is not completion. + result = {'complete': False, 'deferred_until_exact_owner_exit': True, + 'refusal_code': 'OWNER_EXIT_UNCONFIRMED', + 'retained_owned_names': sorted(custody.archive_nodes)} + else: + try: + result = custody.cleanup() + except BaseException as error: + result = {'complete': False, 'refusal_code': str(error) if isinstance(error, Refusal) + else 'EXCEPTION_' + type(error).__name__.upper()} + within_ceiling = time.monotonic() < cleanup_ceiling + terminal_complete = owner_absent and bool(result['complete']) and within_ceiling + return {'reason': reason, 'archive_cleanup': result, 'owner_process_exited': owner_absent, + 'within_cleanup_ceiling': within_ceiling, + 'terminal_cleanup_complete': terminal_complete, + 'outer_owner_custody_required': not terminal_complete, + 'acquisition_accepted': False} + + +if __name__ == '__main__': + raise SystemExit('SOURCE_PROPOSAL_ONLY; owner adapter, launcher, acceptance and exact new authority are pending') diff --git a/qualification/issue-184/tool-acquisition/184-acquisition-custody-guardian-v6-proposal.py b/qualification/issue-184/tool-acquisition/184-acquisition-custody-guardian-v6-proposal.py new file mode 100644 index 0000000..2678597 --- /dev/null +++ b/qualification/issue-184/tool-acquisition/184-acquisition-custody-guardian-v6-proposal.py @@ -0,0 +1,418 @@ +"""Source-only custody primitive. No acquisition launcher or acceptance path exists here. + +The guardian preallocates archive nodes before handing any writable descriptor +to an owner, so owner death cannot leave an unregistered new archive. A future +owner adapter and exact authority/descriptor handoff require separate review. +""" +from __future__ import annotations + +from contextlib import ExitStack +import errno +import hashlib +import json +import os +from pathlib import Path +import re +import select +import signal +import stat +import time + +MINIMAL_ENV = {'PATH': '/usr/bin:/bin', 'LANG': 'C.UTF-8', 'LC_ALL': 'C.UTF-8', 'TZ': 'UTC'} +OLD_DECISION = 'https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-6649046802' +SELECTED_SOURCE = '14adc40b00b68cc8a3e1ae5647a885a42f273650' +ARCHIVE_CAP = 3 * 1024**2 + + +class Refusal(ValueError): + pass + + +class AllocationRefusal(Refusal): + """Partial construction returns custody instead of discarding live handles.""" + def __init__(self, custody, cleanup, cause): + super().__init__('ALLOCATION_REFUSED') + self.custody, self.cleanup = custody, cleanup + self.cause_type = type(cause).__name__ + + +def require(value: bool, code: str) -> None: + if not value: + raise Refusal(code) + + +AUTHORITY_FIELDS = {'decision', 'selected_source', 'inputs', 'environment', 'host', + 'total_seconds', 'io_seconds', 'retries', 'survivor_policy', + 'pool_parent', 'launcher_sha256', 'cpu_affinity'} +AUTHORITY_FIELDS.add('retention_deadline_utc') +INPUT_FIELDS = {'owner', 'bootstrap', 'guardian', 'terminal', 'selection', 'interpreter', 'library', 'ca'} + + +def unique_pairs(values): + result = {} + for key, value in values: + require(key not in result, 'DUPLICATE_AUTHORITY_KEY') + result[key] = value + return result + + +def reject_constant(value): + raise Refusal('NONFINITE_AUTHORITY_NUMBER') + + +def decode_authority(raw: bytes) -> dict: + authority = json.loads(raw.decode('utf-8'), object_pairs_hook=unique_pairs, parse_constant=reject_constant) + require(type(authority) is dict and set(authority) == AUTHORITY_FIELDS, 'EXACT_AUTHORITY_SCHEMA_REQUIRED') + require(type(authority['inputs']) is dict and set(authority['inputs']) == INPUT_FIELDS + and all(type(value) is str and re.fullmatch(r'[0-9a-f]{64}', value) + for value in authority['inputs'].values()), 'EXACT_INPUT_SCHEMA_REQUIRED') + require(type(authority['environment']) is dict and set(authority['environment']) == set(MINIMAL_ENV) + and all(type(value) is str for value in authority['environment'].values()), + 'EXACT_ENVIRONMENT_SCHEMA_REQUIRED') + require(all(type(authority[key]) is int for key in ('total_seconds', 'io_seconds', 'retries')), + 'INTEGER_AUTHORITY_LIMITS_REQUIRED') + require(all(type(authority[key]) is str for key in ('decision', 'selected_source', 'host', + 'survivor_policy', 'pool_parent', 'launcher_sha256')) + and re.fullmatch(r'[0-9a-f]{64}', authority['launcher_sha256']), 'EXACT_AUTHORITY_VALUE_TYPES_REQUIRED') + require(type(authority['retention_deadline_utc']) is str + and re.fullmatch(r'[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z', + authority['retention_deadline_utc']), 'EXACT_RETENTION_DEADLINE_REQUIRED') + affinity = authority['cpu_affinity'] + require(type(affinity) is list and 1 <= len(affinity) <= 2 + and all(type(cpu) is int and cpu >= 0 for cpu in affinity) + and len(set(affinity)) == len(affinity), 'EXACT_CPU_SCHEMA_REQUIRED') + return authority + + +def bind_authority(raw: bytes, expected_sha: str, expected_decision: str, expected_inputs: dict) -> dict: + """Expected values must come from the separately reviewed exact future invocation.""" + require(len(raw) <= 32768 and hashlib.sha256(raw).hexdigest() == expected_sha, 'AUTHORITY_BYTES_CHANGED') + authority = decode_authority(raw) + require(type(expected_inputs) is dict and set(expected_inputs) == INPUT_FIELDS, + 'REVIEWED_INPUT_SCHEMA_REQUIRED') + require(isinstance(expected_decision, str) and expected_decision != OLD_DECISION + and re.fullmatch(r'https://github\.com/durable-workflow/sdk-python/issues/184#issuecomment-[1-9][0-9]{5,17}', expected_decision) + and authority.get('decision') == expected_decision, 'EXACT_NEW_DECISION_REQUIRED') + require(authority.get('selected_source') == SELECTED_SOURCE + and authority.get('inputs') == expected_inputs, 'EXACT_INPUT_AUTHORITY_CHANGED') + require(authority.get('environment') == MINIMAL_ENV and authority.get('host') == 'lab4' + and authority.get('total_seconds') == 300 and authority.get('io_seconds') == 270 + and authority.get('retries') == 0, 'OPERATION_BOUNDARY_CHANGED') + require(authority['survivor_policy'] == 'retain-custody-and-escalate' + and authority['pool_parent'] == '/home/lab4/fleet/tmp', 'EXACT_CUSTODY_POLICY_REQUIRED') + return authority + + +class ArchiveCustody: + """Owns only a newly exclusive root and exact preallocated archive inodes.""" + def __init__(self, parent: Path, leaf: str, filenames: list[str]): + require(re.fullmatch(r'sdk-python-184-tools-guardian-[a-zA-Z0-9-]{1,96}', leaf) is not None, + 'INVALID_EXCLUSIVE_ROOT_NAME') + require(1 <= len(filenames) <= 6 and len(set(filenames)) == len(filenames) + and all(re.fullmatch(r'[a-zA-Z0-9_.-]{1,160}\.whl', name) for name in filenames), + 'INVALID_ARCHIVE_NAMES') + self.path = parent / leaf + self.stack = ExitStack() + self.closed_owned_fds = set() + self.archive_nodes: dict[str, dict] = {} + self.root_id = None + self.wheelhouse_id = None + self.created = False + self.allocated_directory_names = set() + self.root_fd = None + self.wheelhouse_fd = None + # Directory descriptors cannot be wrapped in streams; keep explicit close custody. + self.parent_fd = os.open(parent, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) + try: + self.stack.callback(self.close_owned_fd, self.parent_fd) + st = os.fstat(self.parent_fd) + require(st.st_uid == os.geteuid() and not st.st_mode & 0o022, 'UNSAFE_POOL_PARENT') + os.mkdir(leaf, 0o700, dir_fd=self.parent_fd) + self.created = True + st = os.stat(leaf, dir_fd=self.parent_fd, follow_symlinks=False) + self.root_id = (st.st_dev, st.st_ino) + self.root_fd = os.open(leaf, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=self.parent_fd) + self.stack.callback(self.close_owned_fd, self.root_fd) + st = os.fstat(self.root_fd) + require((st.st_dev, st.st_ino) == self.root_id, 'ROOT_ALLOCATION_COLLISION') + os.mkdir('wheelhouse', 0o700, dir_fd=self.root_fd) + self.allocated_directory_names.add('wheelhouse') + st = os.stat('wheelhouse', dir_fd=self.root_fd, follow_symlinks=False) + self.wheelhouse_id = (st.st_dev, st.st_ino) + self.wheelhouse_fd = os.open('wheelhouse', os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=self.root_fd) + self.stack.callback(self.close_owned_fd, self.wheelhouse_fd) + st = os.fstat(self.wheelhouse_fd) + require((st.st_dev, st.st_ino) == self.wheelhouse_id, 'WHEELHOUSE_ALLOCATION_COLLISION') + for name in filenames: + fd = os.open(name, os.O_RDWR | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600, + dir_fd=self.wheelhouse_fd) + # Raw descriptor custody starts before callback/identity reads. + # Pending identity never permits handoff or named-node removal. + self.archive_nodes[name] = {'fd': fd, 'identity': None} + self.stack.callback(self.close_owned_fd, fd) + st = os.fstat(fd) + self.archive_nodes[name]['identity'] = (st.st_dev, st.st_ino) + except BaseException as error: + try: + cleanup = self.cleanup() if self.created else { + 'complete': True, 'exclusive_root_created': False, 'outer_owner_custody_required': False} + except BaseException as cleanup_error: + cleanup = {'complete': False, 'outer_owner_custody_required': True, + 'refusal_code': type(cleanup_error).__name__} + # Even an incomplete constructor returns the exact object/handles. + # The caller must retain it until the reported tree custody closes. + raise AllocationRefusal(self, cleanup, error) from error + + def scope(self) -> None: + try: + st = os.stat(self.path.name, dir_fd=self.parent_fd, follow_symlinks=False) + except FileNotFoundError: + raise Refusal('ROOT_SCOPE_MISSING') from None + require(stat.S_ISDIR(st.st_mode) and st.st_uid == os.geteuid() + and (st.st_dev, st.st_ino) == self.root_id, 'ROOT_SCOPE_COLLISION') + try: + st = os.stat('wheelhouse', dir_fd=self.root_fd, follow_symlinks=False) + except FileNotFoundError: + raise Refusal('WHEELHOUSE_SCOPE_MISSING') from None + require(stat.S_ISDIR(st.st_mode) and st.st_uid == os.geteuid() + and (st.st_dev, st.st_ino) == self.wheelhouse_id, 'WHEELHOUSE_SCOPE_COLLISION') + + def node(self, name: str) -> os.stat_result: + self.scope() + expected = self.archive_nodes[name] + st = os.stat(name, dir_fd=self.wheelhouse_fd, follow_symlinks=False) + require(expected['identity'] is not None, 'ARCHIVE_IDENTITY_UNPROVEN') + require(stat.S_ISREG(st.st_mode) and st.st_uid == os.geteuid() and st.st_nlink == 1 + and (st.st_dev, st.st_ino) == expected['identity'], 'ARCHIVE_SCOPE_COLLISION') + descriptor = os.fstat(expected['fd']) + require((descriptor.st_dev, descriptor.st_ino) == expected['identity'], 'ARCHIVE_FD_CHANGED') + return st + + def duplicate_for_owner(self, name: str) -> int: + """Only valid after a future ready/authority handshake; no owner is launched here.""" + self.node(name) + return os.dup(self.archive_nodes[name]['fd']) + + def verify_archive_bytes(self, expected: dict[str, dict]) -> list[dict]: + """Raw tuple check only. This does not accept tool metadata or an acquisition.""" + require(set(expected) == set(self.archive_nodes), 'INCOMPLETE_ARCHIVE_SET') + self.scope() + require(set(os.listdir(self.wheelhouse_fd)) == set(expected), 'EXTRA_OR_MISSING_ARCHIVE_NODE') + rows = [] + for name, wanted in expected.items(): + st = self.node(name) + require(type(wanted.get('bytes')) is int and 0 < wanted['bytes'] <= 2 * 1024**2 + and st.st_size == wanted['bytes'] and stat.S_IMODE(st.st_mode) == 0o400, + 'ARCHIVE_SIZE_OR_MODE_CHANGED') + fd = self.archive_nodes[name]['fd'] + raw = os.pread(fd, wanted['bytes'] + 1, 0) + after = os.fstat(fd) + require((st.st_size, st.st_mtime_ns, st.st_ctime_ns) + == (after.st_size, after.st_mtime_ns, after.st_ctime_ns) + and len(raw) == st.st_size and hashlib.sha256(raw).hexdigest() == wanted['sha256'], + 'ARCHIVE_BYTES_CHANGED') + rows.append({'filename': name, 'bytes': len(raw), 'sha256': wanted['sha256']}) + require(sum(row['bytes'] for row in rows) <= ARCHIVE_CAP, 'ARCHIVE_TOTAL_BOUND') + return rows + + def cleanup_archives(self) -> dict: + """Only the archive namespace; never claims root-tree completion.""" + result = {'complete': False, 'removed': [], 'already_absent': [], 'collisions': []} + self.scope() + os.fchmod(self.wheelhouse_fd, 0o700) + for name in self.archive_nodes: + try: + self.node(name) + except FileNotFoundError: + if os.fstat(self.archive_nodes[name]['fd']).st_nlink == 0: + result['already_absent'].append(name) + else: + result['collisions'].append({'filename': name, 'code': 'OWNED_NODE_STILL_LINKED'}) + except Refusal as error: + result['collisions'].append({'filename': name, 'code': str(error)}) + else: + os.unlink(name, dir_fd=self.wheelhouse_fd) + result['removed'].append(name) + result['remaining_names'] = sorted(os.listdir(self.wheelhouse_fd)) + self.scope() + result['complete'] = not result['remaining_names'] and not result['collisions'] + return result + + def unlink_state(self) -> dict: + """Root absence cannot stand in for unlink proof of tracked descendants.""" + directories = {'.': (self.root_fd, self.root_id)} + if self.wheelhouse_id is not None or 'wheelhouse' in self.allocated_directory_names: + directories['wheelhouse'] = (self.wheelhouse_fd, self.wheelhouse_id) + for name, identity in getattr(self, 'dir_ids', {}).items(): + if name not in directories: + directories[name] = (getattr(self, 'directory_fds', {}).get(name), tuple(identity)) + for name in self.allocated_directory_names: + directories.setdefault(name, (None, None)) + tracked = [(name, fd, identity, stat.S_ISDIR, False) for name, (fd, identity) in directories.items() + ] + tracked += [('archive:' + name, row['fd'], row['identity'], stat.S_ISREG, True) + for name, row in self.archive_nodes.items()] + retained = [] + for name, fd, identity, right_type, raw_archive in tracked: + if fd in self.closed_owned_fds: + retained.append(name) + continue + try: + st = os.fstat(fd) + proven = (right_type(st.st_mode) and st.st_uid == os.geteuid() + and ((raw_archive and identity is None) or (st.st_dev, st.st_ino) == identity) + and st.st_nlink == 0) + except (OSError, TypeError): + proven = False + if not proven: + retained.append(name) + return {'all_unlinked': bool(self.root_id is not None) and not retained, + 'retained_or_unproven_inodes': sorted(retained)} + + def finish_unlinked(self, result: dict) -> dict: + state = self.unlink_state() + result.update(state) + result['complete'] = state['all_unlinked'] + result['outer_owner_custody_required'] = not result['complete'] + if not result['complete']: + result['refusal_code'] = 'TRACKED_INODES_STILL_LINKED_OR_UNPROVEN' + return result + + def cleanup(self) -> dict: + """Close the whole allocated tree; unknown members retain explicit custody.""" + result = {'complete': False, 'removed_directories': [], 'collisions': [], + 'outer_owner_custody_required': True} + if not self.created: + result.update(complete=True, outer_owner_custody_required=False) + return result + try: + named = os.stat(self.path.name, dir_fd=self.parent_fd, follow_symlinks=False) + except FileNotFoundError: + # A vanished name is completion only if the exact created inode is + # also unlinked; replacement/move is retained uncertain custody. + return self.finish_unlinked(result) + if not (stat.S_ISDIR(named.st_mode) and named.st_uid == os.geteuid() + and (named.st_dev, named.st_ino) == self.root_id): + result['collisions'].append('ROOT_SCOPE_COLLISION') + return result + if self.root_fd is None: + self.root_fd = os.open(self.path.name, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, + dir_fd=self.parent_fd) + self.stack.callback(self.close_owned_fd, self.root_fd) + current = os.fstat(self.root_fd) + require((current.st_dev, current.st_ino) == self.root_id, 'ROOT_FD_COLLISION') + os.fchmod(self.root_fd, 0o700) + root_names = set(os.listdir(self.root_fd)) + result['unknown_root_names'] = sorted(root_names - ({'wheelhouse'} if self.wheelhouse_id else set())) + if self.wheelhouse_id is not None: + try: + st = os.stat('wheelhouse', dir_fd=self.root_fd, follow_symlinks=False) + except FileNotFoundError: + if self.wheelhouse_fd is None or os.fstat(self.wheelhouse_fd).st_nlink != 0: + result['collisions'].append('WHEELHOUSE_NAME_MISSING_OR_MOVED') + else: + if not (stat.S_ISDIR(st.st_mode) and st.st_uid == os.geteuid() + and (st.st_dev, st.st_ino) == self.wheelhouse_id): + result['collisions'].append('WHEELHOUSE_SCOPE_COLLISION') + else: + if self.wheelhouse_fd is None: + self.wheelhouse_fd = os.open('wheelhouse', os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, + dir_fd=self.root_fd) + self.stack.callback(self.close_owned_fd, self.wheelhouse_fd) + archives = self.cleanup_archives() + result['archive_cleanup'] = archives + if archives['complete']: + self.scope() + os.rmdir('wheelhouse', dir_fd=self.root_fd) + result['removed_directories'].append('wheelhouse') + else: + result['collisions'].append('ARCHIVE_NAMESPACE_RETAINED') + result['remaining_root_names'] = sorted(os.listdir(self.root_fd)) + if not result['remaining_root_names'] and not result['collisions']: + st = os.stat(self.path.name, dir_fd=self.parent_fd, follow_symlinks=False) + require((st.st_dev, st.st_ino) == self.root_id, 'ROOT_REMOVAL_COLLISION') + os.rmdir(self.path.name, dir_fd=self.parent_fd) + result['removed_directories'].append('.') + return self.finish_unlinked(result) + result['outer_owner_custody_required'] = not result['complete'] + return result + + def close_owned_fd(self, fd: int) -> None: + """Callbacks and explicit closure share one descriptor lifetime ledger.""" + if fd in self.closed_owned_fds: + return + try: + os.close(fd) + except OSError as error: + if error.errno != errno.EBADF: + raise + self.closed_owned_fds.add(fd) + + def close(self) -> None: + # Close the newest raw allocation first even if its callback was never + # registered. Closing handles neither deletes nor accepts retained data. + descriptors = [row['fd'] for row in reversed(list(self.archive_nodes.values()))] + descriptors += list(reversed(list(getattr(self, 'directory_fds', {}).values()))) + descriptors += [self.wheelhouse_fd, self.root_fd, self.parent_fd] + try: + for fd in descriptors: + if type(fd) is int: + self.close_owned_fd(fd) + finally: + self.stack.close() + + +def watch_owner_eof(custody: ArchiveCustody, owner_pipe: int, deadline: float, ready_pipe: int, + owner_pidfd: int, expected_pid: int, cleanup_ceiling: float) -> dict: + """Independent process primitive; deadline is anchored before any future allocation. + + No success/preserve message is accepted. Until reviewed owner integration and + terminal acceptance exist, this prototype ALWAYS cleans up on EOF/deadline. + """ + require(dict(os.environ) == MINIMAL_ENV, 'CLOSED_GUARDIAN_ENV_REQUIRED') + require(time.monotonic() < deadline <= time.monotonic() + 300, 'INVALID_ABSOLUTE_DEADLINE') + require(deadline < cleanup_ceiling <= deadline + 30, 'INVALID_CLEANUP_CEILING') + require(os.readlink(f'/proc/self/fd/{owner_pidfd}') == 'anon_inode:[pidfd]', 'OWNER_PIDFD_REQUIRED') + info = Path(f'/proc/self/fdinfo/{owner_pidfd}').read_text() + require(re.search(r'^Pid:\s*' + re.escape(str(expected_pid)) + r'\s*$', info, re.M) is not None + and expected_pid > 1 and expected_pid != os.getpid(), 'EXACT_OWNER_PIDFD_REQUIRED') + custody.scope() + os.write(ready_pipe, b'READY\n') + os.close(ready_pipe) + reason = 'ABSOLUTE_DEADLINE' + while time.monotonic() < deadline: + available, _, _ = select.select([owner_pipe], [], [], max(0, deadline - time.monotonic())) + if available: + data = os.read(owner_pipe, 1) + reason = 'OWNER_EOF' if not data else 'UNSUPPORTED_OWNER_MESSAGE' + break + # A pidfd refers to the created process, so PID reuse cannot retarget this stop. + if not select.select([owner_pidfd], [], [], 0)[0]: + try: + signal.pidfd_send_signal(owner_pidfd, signal.SIGKILL) + except ProcessLookupError: + pass + owner_absent = bool(select.select([owner_pidfd], [], [], max(0, min(5, cleanup_ceiling - time.monotonic())))[0]) + if not owner_absent: + # A surviving process may retain writable descriptors. Keep its named + # nodes in custody for the outer owner; unlinking them is not completion. + result = {'complete': False, 'deferred_until_exact_owner_exit': True, + 'refusal_code': 'OWNER_EXIT_UNCONFIRMED', + 'retained_owned_names': sorted(custody.archive_nodes)} + else: + try: + result = custody.cleanup() + except BaseException as error: + result = {'complete': False, 'refusal_code': str(error) if isinstance(error, Refusal) + else 'EXCEPTION_' + type(error).__name__.upper()} + within_ceiling = time.monotonic() < cleanup_ceiling + terminal_complete = owner_absent and bool(result['complete']) and within_ceiling + return {'reason': reason, 'archive_cleanup': result, 'owner_process_exited': owner_absent, + 'within_cleanup_ceiling': within_ceiling, + 'terminal_cleanup_complete': terminal_complete, + 'outer_owner_custody_required': not terminal_complete, + 'acquisition_accepted': False} + + +if __name__ == '__main__': + raise SystemExit('SOURCE_PROPOSAL_ONLY; owner adapter, launcher, acceptance and exact new authority are pending') diff --git a/qualification/issue-184/tool-acquisition/184-acquisition-custody-guardian-v7-proposal.py b/qualification/issue-184/tool-acquisition/184-acquisition-custody-guardian-v7-proposal.py new file mode 100644 index 0000000..a7bc6a2 --- /dev/null +++ b/qualification/issue-184/tool-acquisition/184-acquisition-custody-guardian-v7-proposal.py @@ -0,0 +1,439 @@ +"""Source-only custody primitive. No acquisition launcher or acceptance path exists here. + +The guardian preallocates archive nodes before handing any writable descriptor +to an owner, so owner death cannot leave an unregistered new archive. A future +owner adapter and exact authority/descriptor handoff require separate review. +""" +from __future__ import annotations + +from contextlib import ExitStack +import errno +import hashlib +import json +import os +from pathlib import Path +import re +import select +import signal +import stat +import time + +MINIMAL_ENV = {'PATH': '/usr/bin:/bin', 'LANG': 'C.UTF-8', 'LC_ALL': 'C.UTF-8', 'TZ': 'UTC'} +OLD_DECISION = 'https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-6649046802' +SELECTED_SOURCE = '14adc40b00b68cc8a3e1ae5647a885a42f273650' +ARCHIVE_CAP = 3 * 1024**2 + + +class Refusal(ValueError): + pass + + +class AllocationRefusal(Refusal): + """Partial construction returns custody instead of discarding live handles.""" + def __init__(self, custody, cleanup, cause): + super().__init__('ALLOCATION_REFUSED') + self.custody, self.cleanup = custody, cleanup + self.cause_type = type(cause).__name__ + + +def require(value: bool, code: str) -> None: + if not value: + raise Refusal(code) + + +AUTHORITY_FIELDS = {'decision', 'selected_source', 'inputs', 'environment', 'host', + 'total_seconds', 'io_seconds', 'retries', 'survivor_policy', + 'pool_parent', 'launcher_sha256', 'cpu_affinity'} +AUTHORITY_FIELDS.add('retention_deadline_utc') +INPUT_FIELDS = {'owner', 'bootstrap', 'guardian', 'terminal', 'selection', 'interpreter', 'library', 'ca'} + + +def unique_pairs(values): + result = {} + for key, value in values: + require(key not in result, 'DUPLICATE_AUTHORITY_KEY') + result[key] = value + return result + + +def reject_constant(value): + raise Refusal('NONFINITE_AUTHORITY_NUMBER') + + +def decode_authority(raw: bytes) -> dict: + authority = json.loads(raw.decode('utf-8'), object_pairs_hook=unique_pairs, parse_constant=reject_constant) + require(type(authority) is dict and set(authority) == AUTHORITY_FIELDS, 'EXACT_AUTHORITY_SCHEMA_REQUIRED') + require(type(authority['inputs']) is dict and set(authority['inputs']) == INPUT_FIELDS + and all(type(value) is str and re.fullmatch(r'[0-9a-f]{64}', value) + for value in authority['inputs'].values()), 'EXACT_INPUT_SCHEMA_REQUIRED') + require(type(authority['environment']) is dict and set(authority['environment']) == set(MINIMAL_ENV) + and all(type(value) is str for value in authority['environment'].values()), + 'EXACT_ENVIRONMENT_SCHEMA_REQUIRED') + require(all(type(authority[key]) is int for key in ('total_seconds', 'io_seconds', 'retries')), + 'INTEGER_AUTHORITY_LIMITS_REQUIRED') + require(all(type(authority[key]) is str for key in ('decision', 'selected_source', 'host', + 'survivor_policy', 'pool_parent', 'launcher_sha256')) + and re.fullmatch(r'[0-9a-f]{64}', authority['launcher_sha256']), 'EXACT_AUTHORITY_VALUE_TYPES_REQUIRED') + require(type(authority['retention_deadline_utc']) is str + and re.fullmatch(r'[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z', + authority['retention_deadline_utc']), 'EXACT_RETENTION_DEADLINE_REQUIRED') + affinity = authority['cpu_affinity'] + require(type(affinity) is list and 1 <= len(affinity) <= 2 + and all(type(cpu) is int and cpu >= 0 for cpu in affinity) + and len(set(affinity)) == len(affinity), 'EXACT_CPU_SCHEMA_REQUIRED') + return authority + + +def bind_authority(raw: bytes, expected_sha: str, expected_decision: str, expected_inputs: dict) -> dict: + """Expected values must come from the separately reviewed exact future invocation.""" + require(len(raw) <= 32768 and hashlib.sha256(raw).hexdigest() == expected_sha, 'AUTHORITY_BYTES_CHANGED') + authority = decode_authority(raw) + require(type(expected_inputs) is dict and set(expected_inputs) == INPUT_FIELDS, + 'REVIEWED_INPUT_SCHEMA_REQUIRED') + require(isinstance(expected_decision, str) and expected_decision != OLD_DECISION + and re.fullmatch(r'https://github\.com/durable-workflow/sdk-python/issues/184#issuecomment-[1-9][0-9]{5,17}', expected_decision) + and authority.get('decision') == expected_decision, 'EXACT_NEW_DECISION_REQUIRED') + require(authority.get('selected_source') == SELECTED_SOURCE + and authority.get('inputs') == expected_inputs, 'EXACT_INPUT_AUTHORITY_CHANGED') + require(authority.get('environment') == MINIMAL_ENV and authority.get('host') == 'lab4' + and authority.get('total_seconds') == 300 and authority.get('io_seconds') == 270 + and authority.get('retries') == 0, 'OPERATION_BOUNDARY_CHANGED') + require(authority['survivor_policy'] == 'retain-custody-and-escalate' + and authority['pool_parent'] == '/home/lab4/fleet/tmp', 'EXACT_CUSTODY_POLICY_REQUIRED') + return authority + + +class ArchiveCustody: + """Owns only a newly exclusive root and exact preallocated archive inodes.""" + def __init__(self, parent: Path, leaf: str, filenames: list[str]): + require(re.fullmatch(r'sdk-python-184-tools-guardian-[a-zA-Z0-9-]{1,96}', leaf) is not None, + 'INVALID_EXCLUSIVE_ROOT_NAME') + require(1 <= len(filenames) <= 6 and len(set(filenames)) == len(filenames) + and all(re.fullmatch(r'[a-zA-Z0-9_.-]{1,160}\.whl', name) for name in filenames), + 'INVALID_ARCHIVE_NAMES') + self.path = parent / leaf + self.stack = ExitStack() + self.closed_owned_fds = set() + self.archive_nodes: dict[str, dict] = {} + self.pending_archive_names = set() + self.root_id = None + self.wheelhouse_id = None + self.created = False + self.allocated_directory_names = set() + self.root_fd = None + self.wheelhouse_fd = None + # Directory descriptors cannot be wrapped in streams; keep explicit close custody. + self.parent_fd = os.open(parent, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) + try: + self.stack.callback(self.close_owned_fd, self.parent_fd) + st = os.fstat(self.parent_fd) + require(st.st_uid == os.geteuid() and not st.st_mode & 0o022, 'UNSAFE_POOL_PARENT') + os.mkdir(leaf, 0o700, dir_fd=self.parent_fd) + self.created = True + st = os.stat(leaf, dir_fd=self.parent_fd, follow_symlinks=False) + self.root_id = (st.st_dev, st.st_ino) + self.root_fd = os.open(leaf, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=self.parent_fd) + self.stack.callback(self.close_owned_fd, self.root_fd) + st = os.fstat(self.root_fd) + require((st.st_dev, st.st_ino) == self.root_id, 'ROOT_ALLOCATION_COLLISION') + os.mkdir('wheelhouse', 0o700, dir_fd=self.root_fd) + self.allocated_directory_names.add('wheelhouse') + st = os.stat('wheelhouse', dir_fd=self.root_fd, follow_symlinks=False) + self.wheelhouse_id = (st.st_dev, st.st_ino) + self.wheelhouse_fd = os.open('wheelhouse', os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=self.root_fd) + self.stack.callback(self.close_owned_fd, self.wheelhouse_fd) + st = os.fstat(self.wheelhouse_fd) + require((st.st_dev, st.st_ino) == self.wheelhouse_id, 'WHEELHOUSE_ALLOCATION_COLLISION') + for name in filenames: + self.allocate_archive(name) + except BaseException as error: + try: + cleanup = self.cleanup() if self.created else { + 'complete': True, 'exclusive_root_created': False, 'outer_owner_custody_required': False} + except BaseException as cleanup_error: + cleanup = {'complete': False, 'outer_owner_custody_required': True, + 'refusal_code': type(cleanup_error).__name__} + # Even an incomplete constructor returns the exact object/handles. + # The caller must retain it until the reported tree custody closes. + raise AllocationRefusal(self, cleanup, error) from error + + def allocate_archive(self, name: str) -> None: + # Record the possible allocation before the syscall. If mapping insertion + # is interrupted, the local descriptor still closes and the unknown node + # cannot disappear from whole-tree completion merely because root moved. + self.pending_archive_names.add(name) + fd = None + adopted = False + try: + fd = os.open(name, os.O_RDWR | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600, + dir_fd=self.wheelhouse_fd) + self.archive_nodes[name] = {'fd': fd, 'identity': None} + adopted = True + self.stack.callback(self.close_owned_fd, fd) + st = os.fstat(fd) + self.archive_nodes[name]['identity'] = (st.st_dev, st.st_ino) + except OSError: + if fd is None: + self.pending_archive_names.discard(name) + raise + finally: + if adopted: + self.pending_archive_names.discard(name) + elif fd is not None: + self.close_owned_fd(fd) + + def scope(self) -> None: + try: + st = os.stat(self.path.name, dir_fd=self.parent_fd, follow_symlinks=False) + except FileNotFoundError: + raise Refusal('ROOT_SCOPE_MISSING') from None + require(stat.S_ISDIR(st.st_mode) and st.st_uid == os.geteuid() + and (st.st_dev, st.st_ino) == self.root_id, 'ROOT_SCOPE_COLLISION') + try: + st = os.stat('wheelhouse', dir_fd=self.root_fd, follow_symlinks=False) + except FileNotFoundError: + raise Refusal('WHEELHOUSE_SCOPE_MISSING') from None + require(stat.S_ISDIR(st.st_mode) and st.st_uid == os.geteuid() + and (st.st_dev, st.st_ino) == self.wheelhouse_id, 'WHEELHOUSE_SCOPE_COLLISION') + + def node(self, name: str) -> os.stat_result: + self.scope() + expected = self.archive_nodes[name] + st = os.stat(name, dir_fd=self.wheelhouse_fd, follow_symlinks=False) + require(expected['identity'] is not None, 'ARCHIVE_IDENTITY_UNPROVEN') + require(stat.S_ISREG(st.st_mode) and st.st_uid == os.geteuid() and st.st_nlink == 1 + and (st.st_dev, st.st_ino) == expected['identity'], 'ARCHIVE_SCOPE_COLLISION') + descriptor = os.fstat(expected['fd']) + require((descriptor.st_dev, descriptor.st_ino) == expected['identity'], 'ARCHIVE_FD_CHANGED') + return st + + def duplicate_for_owner(self, name: str) -> int: + """Only valid after a future ready/authority handshake; no owner is launched here.""" + self.node(name) + return os.dup(self.archive_nodes[name]['fd']) + + def verify_archive_bytes(self, expected: dict[str, dict]) -> list[dict]: + """Raw tuple check only. This does not accept tool metadata or an acquisition.""" + require(set(expected) == set(self.archive_nodes), 'INCOMPLETE_ARCHIVE_SET') + self.scope() + require(set(os.listdir(self.wheelhouse_fd)) == set(expected), 'EXTRA_OR_MISSING_ARCHIVE_NODE') + rows = [] + for name, wanted in expected.items(): + st = self.node(name) + require(type(wanted.get('bytes')) is int and 0 < wanted['bytes'] <= 2 * 1024**2 + and st.st_size == wanted['bytes'] and stat.S_IMODE(st.st_mode) == 0o400, + 'ARCHIVE_SIZE_OR_MODE_CHANGED') + fd = self.archive_nodes[name]['fd'] + raw = os.pread(fd, wanted['bytes'] + 1, 0) + after = os.fstat(fd) + require((st.st_size, st.st_mtime_ns, st.st_ctime_ns) + == (after.st_size, after.st_mtime_ns, after.st_ctime_ns) + and len(raw) == st.st_size and hashlib.sha256(raw).hexdigest() == wanted['sha256'], + 'ARCHIVE_BYTES_CHANGED') + rows.append({'filename': name, 'bytes': len(raw), 'sha256': wanted['sha256']}) + require(sum(row['bytes'] for row in rows) <= ARCHIVE_CAP, 'ARCHIVE_TOTAL_BOUND') + return rows + + def cleanup_archives(self) -> dict: + """Only the archive namespace; never claims root-tree completion.""" + result = {'complete': False, 'removed': [], 'already_absent': [], 'collisions': []} + self.scope() + os.fchmod(self.wheelhouse_fd, 0o700) + for name in self.archive_nodes: + try: + self.node(name) + except FileNotFoundError: + if os.fstat(self.archive_nodes[name]['fd']).st_nlink == 0: + result['already_absent'].append(name) + else: + result['collisions'].append({'filename': name, 'code': 'OWNED_NODE_STILL_LINKED'}) + except Refusal as error: + result['collisions'].append({'filename': name, 'code': str(error)}) + else: + os.unlink(name, dir_fd=self.wheelhouse_fd) + result['removed'].append(name) + result['remaining_names'] = sorted(os.listdir(self.wheelhouse_fd)) + self.scope() + result['complete'] = not result['remaining_names'] and not result['collisions'] + return result + + def unlink_state(self) -> dict: + """Root absence cannot stand in for unlink proof of tracked descendants.""" + directories = {'.': (self.root_fd, self.root_id)} + if self.wheelhouse_id is not None or 'wheelhouse' in self.allocated_directory_names: + directories['wheelhouse'] = (self.wheelhouse_fd, self.wheelhouse_id) + for name, identity in getattr(self, 'dir_ids', {}).items(): + if name not in directories: + directories[name] = (getattr(self, 'directory_fds', {}).get(name), tuple(identity)) + for name in self.allocated_directory_names: + directories.setdefault(name, (None, None)) + tracked = [(name, fd, identity, stat.S_ISDIR, False) for name, (fd, identity) in directories.items() + ] + tracked += [('archive:' + name, row['fd'], row['identity'], stat.S_ISREG, True) + for name, row in self.archive_nodes.items()] + retained = [] + for name, fd, identity, right_type, raw_archive in tracked: + if fd in self.closed_owned_fds: + retained.append(name) + continue + try: + st = os.fstat(fd) + proven = (right_type(st.st_mode) and st.st_uid == os.geteuid() + and ((raw_archive and identity is None) or (st.st_dev, st.st_ino) == identity) + and st.st_nlink == 0) + except (OSError, TypeError): + proven = False + if not proven: + retained.append(name) + retained += ['archive:' + name + ':allocation_unproven' + for name in self.pending_archive_names if name not in self.archive_nodes] + return {'all_unlinked': bool(self.root_id is not None) and not retained, + 'retained_or_unproven_inodes': sorted(retained)} + + def finish_unlinked(self, result: dict) -> dict: + state = self.unlink_state() + result.update(state) + result['complete'] = state['all_unlinked'] + result['outer_owner_custody_required'] = not result['complete'] + if not result['complete']: + result['refusal_code'] = 'TRACKED_INODES_STILL_LINKED_OR_UNPROVEN' + return result + + def cleanup(self) -> dict: + """Close the whole allocated tree; unknown members retain explicit custody.""" + result = {'complete': False, 'removed_directories': [], 'collisions': [], + 'outer_owner_custody_required': True} + if not self.created: + result.update(complete=True, outer_owner_custody_required=False) + return result + try: + named = os.stat(self.path.name, dir_fd=self.parent_fd, follow_symlinks=False) + except FileNotFoundError: + # A vanished name is completion only if the exact created inode is + # also unlinked; replacement/move is retained uncertain custody. + return self.finish_unlinked(result) + if not (stat.S_ISDIR(named.st_mode) and named.st_uid == os.geteuid() + and (named.st_dev, named.st_ino) == self.root_id): + result['collisions'].append('ROOT_SCOPE_COLLISION') + return result + if self.root_fd is None: + self.root_fd = os.open(self.path.name, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, + dir_fd=self.parent_fd) + self.stack.callback(self.close_owned_fd, self.root_fd) + current = os.fstat(self.root_fd) + require((current.st_dev, current.st_ino) == self.root_id, 'ROOT_FD_COLLISION') + os.fchmod(self.root_fd, 0o700) + root_names = set(os.listdir(self.root_fd)) + result['unknown_root_names'] = sorted(root_names - ({'wheelhouse'} if self.wheelhouse_id else set())) + if self.wheelhouse_id is not None: + try: + st = os.stat('wheelhouse', dir_fd=self.root_fd, follow_symlinks=False) + except FileNotFoundError: + if self.wheelhouse_fd is None or os.fstat(self.wheelhouse_fd).st_nlink != 0: + result['collisions'].append('WHEELHOUSE_NAME_MISSING_OR_MOVED') + else: + if not (stat.S_ISDIR(st.st_mode) and st.st_uid == os.geteuid() + and (st.st_dev, st.st_ino) == self.wheelhouse_id): + result['collisions'].append('WHEELHOUSE_SCOPE_COLLISION') + else: + if self.wheelhouse_fd is None: + self.wheelhouse_fd = os.open('wheelhouse', os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, + dir_fd=self.root_fd) + self.stack.callback(self.close_owned_fd, self.wheelhouse_fd) + archives = self.cleanup_archives() + result['archive_cleanup'] = archives + if archives['complete']: + self.scope() + os.rmdir('wheelhouse', dir_fd=self.root_fd) + result['removed_directories'].append('wheelhouse') + else: + result['collisions'].append('ARCHIVE_NAMESPACE_RETAINED') + result['remaining_root_names'] = sorted(os.listdir(self.root_fd)) + if not result['remaining_root_names'] and not result['collisions']: + st = os.stat(self.path.name, dir_fd=self.parent_fd, follow_symlinks=False) + require((st.st_dev, st.st_ino) == self.root_id, 'ROOT_REMOVAL_COLLISION') + os.rmdir(self.path.name, dir_fd=self.parent_fd) + result['removed_directories'].append('.') + return self.finish_unlinked(result) + result['outer_owner_custody_required'] = not result['complete'] + return result + + def close_owned_fd(self, fd: int) -> None: + """Callbacks and explicit closure share one descriptor lifetime ledger.""" + if fd in self.closed_owned_fds: + return + try: + os.close(fd) + except OSError as error: + if error.errno != errno.EBADF: + raise + self.closed_owned_fds.add(fd) + + def close(self) -> None: + # Close the newest raw allocation first even if its callback was never + # registered. Closing handles neither deletes nor accepts retained data. + descriptors = [row['fd'] for row in reversed(list(self.archive_nodes.values()))] + descriptors += list(reversed(list(getattr(self, 'directory_fds', {}).values()))) + descriptors += [self.wheelhouse_fd, self.root_fd, self.parent_fd] + try: + for fd in descriptors: + if type(fd) is int: + self.close_owned_fd(fd) + finally: + self.stack.close() + + +def watch_owner_eof(custody: ArchiveCustody, owner_pipe: int, deadline: float, ready_pipe: int, + owner_pidfd: int, expected_pid: int, cleanup_ceiling: float) -> dict: + """Independent process primitive; deadline is anchored before any future allocation. + + No success/preserve message is accepted. Until reviewed owner integration and + terminal acceptance exist, this prototype ALWAYS cleans up on EOF/deadline. + """ + require(dict(os.environ) == MINIMAL_ENV, 'CLOSED_GUARDIAN_ENV_REQUIRED') + require(time.monotonic() < deadline <= time.monotonic() + 300, 'INVALID_ABSOLUTE_DEADLINE') + require(deadline < cleanup_ceiling <= deadline + 30, 'INVALID_CLEANUP_CEILING') + require(os.readlink(f'/proc/self/fd/{owner_pidfd}') == 'anon_inode:[pidfd]', 'OWNER_PIDFD_REQUIRED') + info = Path(f'/proc/self/fdinfo/{owner_pidfd}').read_text() + require(re.search(r'^Pid:\s*' + re.escape(str(expected_pid)) + r'\s*$', info, re.M) is not None + and expected_pid > 1 and expected_pid != os.getpid(), 'EXACT_OWNER_PIDFD_REQUIRED') + custody.scope() + os.write(ready_pipe, b'READY\n') + os.close(ready_pipe) + reason = 'ABSOLUTE_DEADLINE' + while time.monotonic() < deadline: + available, _, _ = select.select([owner_pipe], [], [], max(0, deadline - time.monotonic())) + if available: + data = os.read(owner_pipe, 1) + reason = 'OWNER_EOF' if not data else 'UNSUPPORTED_OWNER_MESSAGE' + break + # A pidfd refers to the created process, so PID reuse cannot retarget this stop. + if not select.select([owner_pidfd], [], [], 0)[0]: + try: + signal.pidfd_send_signal(owner_pidfd, signal.SIGKILL) + except ProcessLookupError: + pass + owner_absent = bool(select.select([owner_pidfd], [], [], max(0, min(5, cleanup_ceiling - time.monotonic())))[0]) + if not owner_absent: + # A surviving process may retain writable descriptors. Keep its named + # nodes in custody for the outer owner; unlinking them is not completion. + result = {'complete': False, 'deferred_until_exact_owner_exit': True, + 'refusal_code': 'OWNER_EXIT_UNCONFIRMED', + 'retained_owned_names': sorted(custody.archive_nodes)} + else: + try: + result = custody.cleanup() + except BaseException as error: + result = {'complete': False, 'refusal_code': str(error) if isinstance(error, Refusal) + else 'EXCEPTION_' + type(error).__name__.upper()} + within_ceiling = time.monotonic() < cleanup_ceiling + terminal_complete = owner_absent and bool(result['complete']) and within_ceiling + return {'reason': reason, 'archive_cleanup': result, 'owner_process_exited': owner_absent, + 'within_cleanup_ceiling': within_ceiling, + 'terminal_cleanup_complete': terminal_complete, + 'outer_owner_custody_required': not terminal_complete, + 'acquisition_accepted': False} + + +if __name__ == '__main__': + raise SystemExit('SOURCE_PROPOSAL_ONLY; owner adapter, launcher, acceptance and exact new authority are pending') diff --git a/qualification/issue-184/tool-acquisition/184-acquisition-guardian-v3-controls-result.json b/qualification/issue-184/tool-acquisition/184-acquisition-guardian-v3-controls-result.json new file mode 100644 index 0000000..c0320da --- /dev/null +++ b/qualification/issue-184/tool-acquisition/184-acquisition-guardian-v3-controls-result.json @@ -0,0 +1,225 @@ +{ + "cases": 53, + "failed": 0, + "new_general_decision": null, + "passed": 53, + "predecessor_sha256": "afc5f198de12d4b26520957bc563ff0122f8535838a1cbc64c40272c5f3f7f70", + "real_network_requests": 0, + "results": [ + { + "id": "exact-schema-positive", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-duplicate-identical", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-duplicate-conflicting", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-duplicate-escaped", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-extra-top-field", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-missing-survivor_policy", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-missing-pool_parent", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-missing-launcher_sha256", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-missing-cpu_affinity", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-missing-retention_deadline_utc", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-nested-duplicate-inputs", + "outcome": "pass" + }, + { + "id": "refuse-nested-schema-inputs-extra", + "outcome": "pass" + }, + { + "id": "refuse-nested-schema-inputs-missing", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-nested-duplicate-environment", + "outcome": "pass" + }, + { + "id": "refuse-nested-schema-environment-extra", + "outcome": "pass" + }, + { + "id": "refuse-nested-schema-environment-missing", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-float-total_seconds", + "outcome": "pass" + }, + { + "id": "refuse-total_seconds-bool", + "outcome": "pass" + }, + { + "id": "refuse-total_seconds-string", + "outcome": "pass" + }, + { + "id": "refuse-total_seconds-nan", + "outcome": "pass" + }, + { + "id": "refuse-total_seconds-positive-infinity", + "outcome": "pass" + }, + { + "id": "refuse-total_seconds-negative-infinity", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-float-io_seconds", + "outcome": "pass" + }, + { + "id": "refuse-io_seconds-bool", + "outcome": "pass" + }, + { + "id": "refuse-io_seconds-string", + "outcome": "pass" + }, + { + "id": "refuse-io_seconds-nan", + "outcome": "pass" + }, + { + "id": "refuse-io_seconds-positive-infinity", + "outcome": "pass" + }, + { + "id": "refuse-io_seconds-negative-infinity", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-float-retries", + "outcome": "pass" + }, + { + "id": "refuse-retries-bool", + "outcome": "pass" + }, + { + "id": "refuse-retries-string", + "outcome": "pass" + }, + { + "id": "refuse-retries-nan", + "outcome": "pass" + }, + { + "id": "refuse-retries-positive-infinity", + "outcome": "pass" + }, + { + "id": "refuse-retries-negative-infinity", + "outcome": "pass" + }, + { + "id": "refuse-cpu-schema-[]", + "outcome": "pass" + }, + { + "id": "refuse-cpu-schema-[True]", + "outcome": "pass" + }, + { + "id": "refuse-cpu-schema-[0, 0]", + "outcome": "pass" + }, + { + "id": "refuse-cpu-schema-[0.0]", + "outcome": "pass" + }, + { + "id": "refuse-cpu-schema-['0']", + "outcome": "pass" + }, + { + "id": "refuse-cpu-schema-[0, 1, 2]", + "outcome": "pass" + }, + { + "id": "nonfinite-at-unexpected-depth-refuses", + "outcome": "pass" + }, + { + "id": "invalid-utf8-refuses", + "outcome": "pass" + }, + { + "id": "sha-check-preserved", + "outcome": "pass" + }, + { + "id": "predecessor-tree-normal", + "outcome": "pass" + }, + { + "id": "successor-tree-normal", + "outcome": "pass" + }, + { + "id": "predecessor-tree-normal-unknown", + "outcome": "pass" + }, + { + "id": "successor-tree-normal-unknown", + "outcome": "pass" + }, + { + "id": "predecessor-tree-after-root", + "outcome": "pass" + }, + { + "id": "successor-tree-after-root", + "outcome": "pass" + }, + { + "id": "predecessor-tree-after-wheelhouse", + "outcome": "pass" + }, + { + "id": "successor-tree-after-wheelhouse", + "outcome": "pass" + }, + { + "id": "predecessor-tree-after-root-unknown", + "outcome": "pass" + }, + { + "id": "successor-tree-after-root-unknown", + "outcome": "pass" + } + ], + "scope": "guardian authority/root-tree predecessor countercontrols only; no admitted data API", + "selected_archives_acquired": 0, + "source_sha256": "79ddc82b0095872885aceb943cbf9daa1eb3a05048bc08f9ee9cd093511de187" +} diff --git a/qualification/issue-184/tool-acquisition/184-acquisition-guardian-v3-controls.py b/qualification/issue-184/tool-acquisition/184-acquisition-guardian-v3-controls.py new file mode 100644 index 0000000..80dbdbb --- /dev/null +++ b/qualification/issue-184/tool-acquisition/184-acquisition-guardian-v3-controls.py @@ -0,0 +1,180 @@ +"""Narrow predecessor countercontrols; no data acquisition or admitted start.""" +from __future__ import annotations + +import copy +import hashlib +import importlib.util +import json +import os +from pathlib import Path +import socket +import sys +import tempfile +from unittest.mock import patch + +sys.dont_write_bytecode = True +BASE = Path('/tmp/sdk-python-handoff') +REF = 'https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-9999999999' +results = [] + + +def load(name, version): + path = BASE / f'184-acquisition-custody-guardian-{version}-proposal.py' + raw = path.read_bytes() + if version == 'v2': + assert hashlib.sha256(raw).hexdigest() == 'afc5f198de12d4b26520957bc563ff0122f8535838a1cbc64c40272c5f3f7f70' + spec = importlib.util.spec_from_file_location(name, path) + module = importlib.util.module_from_spec(spec); spec.loader.exec_module(module) + return module + + +old = load('trusted_frozen_v2_countercontrol', 'v2') +new = load('trusted_v3_correction_control', 'v3') +inputs = {key: 'a' * 64 for key in new.INPUT_FIELDS} +authority = dict(decision=REF, selected_source=new.SELECTED_SOURCE, inputs=inputs, + environment=new.MINIMAL_ENV, host='lab4', total_seconds=300, io_seconds=270, + retries=0, survivor_policy='retain-custody-and-escalate', + pool_parent='/home/lab4/fleet/tmp', launcher_sha256='b' * 64, cpu_affinity=[0, 1], + retention_deadline_utc='2099-01-01T00:00:00Z') + + +def bound(module, raw): + return module.bind_authority(raw, hashlib.sha256(raw).hexdigest(), REF, inputs) + + +def parser_refuses(raw, predecessor=True): + if predecessor: + assert bound(old, raw)['decision'] == REF, 'predecessor acceptance not reproduced' + try: + bound(new, raw) + except (ValueError, UnicodeError): + return + raise AssertionError('ambiguous authority accepted') + + +def sha_refuses(): + try: new.bind_authority(encoded(authority), '0' * 64, REF, inputs) + except new.Refusal as error: assert str(error) == 'AUTHORITY_BYTES_CHANGED' + else: raise AssertionError('changed authority SHA accepted') + + +def encoded(value): + return json.dumps(value, separators=(',', ':')).encode() + + +def mutate(key, value): + row = copy.deepcopy(authority); row[key] = value + return encoded(row) + + +def allocation(module, stage, unknown=False): + with tempfile.TemporaryDirectory(prefix='sdk-python-184-guardian-tree-controls-') as temp: + parent = Path(temp); leaf = 'sdk-python-184-tools-guardian-source-control' + root = parent / leaf + custody = None + real_mkdir = os.mkdir + def mkdir(path, mode=0o777, *, dir_fd=None): + if path == 'wheelhouse' and stage == 'after-root': + if unknown: (root / 'unknown-root-member').write_bytes(b'untouched') + raise OSError('synthetic second mkdir refusal') + return real_mkdir(path, mode, dir_fd=dir_fd) + real_open = os.open + def opened(path, flags, mode=0o777, *, dir_fd=None): + if path == 'one.whl' and stage == 'after-wheelhouse': + raise OSError('synthetic first archive refusal') + return real_open(path, flags, mode, dir_fd=dir_fd) + try: + with patch.object(module.os, 'mkdir', side_effect=mkdir), patch.object(module.os, 'open', side_effect=opened): + try: custody = module.ArchiveCustody(parent, leaf, ['one.whl']) + except module.Refusal as error: + if module is new: + assert isinstance(error, new.AllocationRefusal) + custody = error.custody + assert error.cleanup['complete'] is (not unknown) + assert error.cleanup['outer_owner_custody_required'] is unknown + if unknown: + assert root.exists() and (root / 'unknown-root-member').read_bytes() == b'untouched' + assert os.fstat(custody.parent_fd).st_uid == os.geteuid() + else: assert not root.exists() + else: raise + except OSError: + assert module is old and root.exists(), 'predecessor residue not reproduced' + else: + assert stage == 'normal' + if unknown: (root / 'unknown-root-member').write_bytes(b'untouched') + result = custody.cleanup() + if module is old: + assert result['complete'] and root.exists() and (root / 'wheelhouse').exists() + elif unknown: + assert not result['complete'] and result['outer_owner_custody_required'] + assert (root / 'unknown-root-member').read_bytes() == b'untouched' + assert not (root / 'wheelhouse').exists() + else: + assert result['complete'] and not result['outer_owner_custody_required'] + assert not root.exists(), 'TemporaryDirectory must not conceal retained directories' + finally: + if custody is not None: custody.close() + + +def check(name, operation): + try: operation() + except BaseException as error: + results.append({'id': name, 'outcome': 'fail', 'type': type(error).__name__, 'detail': str(error)[:250]}) + else: results.append({'id': name, 'outcome': 'pass'}) + + +with patch.object(socket, 'create_connection', side_effect=AssertionError('NO_NETWORK_IN_SOURCE_CONTROLS')): + check('exact-schema-positive', lambda: bound(new, encoded(authority))) + for name, prefix in [('identical', '"decision":"' + REF + '",'), + ('conflicting', '"decision":"earlier-conflict",'), + ('escaped', '"decis\\u0069on":"earlier-conflict",')]: + check('old-accept-new-refuse-duplicate-' + name, + lambda prefix=prefix: parser_refuses(b'{' + prefix.encode() + encoded(authority)[1:])) + row = copy.deepcopy(authority); row['unreviewed_future_semantics'] = {} + check('old-accept-new-refuse-extra-top-field', lambda: parser_refuses(encoded(row))) + for key in ('survivor_policy', 'pool_parent', 'launcher_sha256', 'cpu_affinity', 'retention_deadline_utc'): + missing = copy.deepcopy(authority); del missing[key] + check('old-accept-new-refuse-missing-' + key, lambda missing=missing: parser_refuses(encoded(missing))) + for key, nested in [('inputs', inputs), ('environment', new.MINIMAL_ENV)]: + first = next(iter(nested)); duplicated = encoded(authority).replace( + ('"' + key + '":{').encode(), ('"' + key + '":{"' + first + '":"conflict",').encode(), 1) + check('old-accept-new-refuse-nested-duplicate-' + key, lambda raw=duplicated: parser_refuses(raw)) + for extra in (True, False): + value = dict(nested) + if extra: value['unknown'] = 'x' + else: del value[first] + check('refuse-nested-schema-' + key + ('-extra' if extra else '-missing'), + lambda key=key, value=value: parser_refuses(mutate(key, value), predecessor=False)) + for key in ('total_seconds', 'io_seconds', 'retries'): + check('old-accept-new-refuse-float-' + key, lambda key=key: parser_refuses(mutate(key, float(authority[key])))) + for kind, value in [('bool', False), ('string', str(authority[key])), ('nan', float('nan')), + ('positive-infinity', float('inf')), ('negative-infinity', -float('inf'))]: + check('refuse-' + key + '-' + kind, + lambda key=key, value=value: parser_refuses(mutate(key, value), predecessor=(key == 'retries' and value is False))) + for value in ([], [True], [0, 0], [0.0], ['0'], [0, 1, 2]): + check('refuse-cpu-schema-' + repr(value), lambda value=value: parser_refuses(mutate('cpu_affinity', value))) + check('nonfinite-at-unexpected-depth-refuses', lambda: parser_refuses( + encoded(authority).replace(b'"cpu_affinity":[0,1]', b'"cpu_affinity":[NaN]'))) + check('invalid-utf8-refuses', lambda: parser_refuses(b'\xff', predecessor=False)) + check('sha-check-preserved', sha_refuses) + for stage, unknown in [('normal', False), ('normal', True), ('after-root', False), + ('after-wheelhouse', False), ('after-root', True)]: + for module, label in ((old, 'predecessor'), (new, 'successor')): + check(label + '-tree-' + stage + ('-unknown' if unknown else ''), + lambda module=module, stage=stage, unknown=unknown: allocation(module, stage, unknown)) + +report = {'scope': 'guardian authority/root-tree predecessor countercontrols only; no admitted data API', + 'source_sha256': hashlib.sha256((BASE / '184-acquisition-custody-guardian-v3-proposal.py').read_bytes()).hexdigest(), + 'predecessor_sha256': 'afc5f198de12d4b26520957bc563ff0122f8535838a1cbc64c40272c5f3f7f70', + 'cases': len(results), 'passed': sum(row['outcome'] == 'pass' for row in results), + 'failed': sum(row['outcome'] == 'fail' for row in results), 'results': results, + 'real_network_requests': 0, 'selected_archives_acquired': 0, 'new_general_decision': None} +raw = (json.dumps(report, sort_keys=True, indent=2) + '\n').encode() +path = BASE / '184-acquisition-guardian-v3-controls-result.json' +if path.exists(): + if path.read_bytes() != raw: + print(json.dumps(report, indent=2)); raise SystemExit('CONTROL_RESULT_COLLISION; prior result retained') +else: + fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o400) + with os.fdopen(fd, 'wb') as stream: assert stream.write(raw) == len(raw) +print(json.dumps(report, indent=2)); raise SystemExit(bool(report['failed'])) diff --git a/qualification/issue-184/tool-acquisition/184-acquisition-guardian-v4-controls-result.json b/qualification/issue-184/tool-acquisition/184-acquisition-guardian-v4-controls-result.json new file mode 100644 index 0000000..b04112f --- /dev/null +++ b/qualification/issue-184/tool-acquisition/184-acquisition-guardian-v4-controls-result.json @@ -0,0 +1,261 @@ +{ + "cases": 62, + "failed": 0, + "new_general_decision": null, + "passed": 62, + "predecessor_sha256": "afc5f198de12d4b26520957bc563ff0122f8535838a1cbc64c40272c5f3f7f70", + "real_network_requests": 0, + "results": [ + { + "id": "predecessor-false-complete-allocation-before-child-identity", + "outcome": "pass" + }, + { + "id": "successor-retains-allocation-before-child-identity", + "outcome": "pass" + }, + { + "id": "predecessor-false-complete-removed-root-linked-wheelhouse", + "outcome": "pass" + }, + { + "id": "successor-retains-removed-root-linked-wheelhouse", + "outcome": "pass" + }, + { + "id": "predecessor-false-complete-removed-root-linked-archive", + "outcome": "pass" + }, + { + "id": "successor-retains-removed-root-linked-archive", + "outcome": "pass" + }, + { + "id": "predecessor-false-complete-removed-root-linked-extra-directory", + "outcome": "pass" + }, + { + "id": "successor-retains-removed-root-linked-extra-directory", + "outcome": "pass" + }, + { + "id": "fully-unlinked-root-children-and-archives-completes", + "outcome": "pass" + }, + { + "id": "exact-schema-positive", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-duplicate-identical", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-duplicate-conflicting", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-duplicate-escaped", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-extra-top-field", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-missing-survivor_policy", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-missing-pool_parent", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-missing-launcher_sha256", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-missing-cpu_affinity", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-missing-retention_deadline_utc", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-nested-duplicate-inputs", + "outcome": "pass" + }, + { + "id": "refuse-nested-schema-inputs-extra", + "outcome": "pass" + }, + { + "id": "refuse-nested-schema-inputs-missing", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-nested-duplicate-environment", + "outcome": "pass" + }, + { + "id": "refuse-nested-schema-environment-extra", + "outcome": "pass" + }, + { + "id": "refuse-nested-schema-environment-missing", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-float-total_seconds", + "outcome": "pass" + }, + { + "id": "refuse-total_seconds-bool", + "outcome": "pass" + }, + { + "id": "refuse-total_seconds-string", + "outcome": "pass" + }, + { + "id": "refuse-total_seconds-nan", + "outcome": "pass" + }, + { + "id": "refuse-total_seconds-positive-infinity", + "outcome": "pass" + }, + { + "id": "refuse-total_seconds-negative-infinity", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-float-io_seconds", + "outcome": "pass" + }, + { + "id": "refuse-io_seconds-bool", + "outcome": "pass" + }, + { + "id": "refuse-io_seconds-string", + "outcome": "pass" + }, + { + "id": "refuse-io_seconds-nan", + "outcome": "pass" + }, + { + "id": "refuse-io_seconds-positive-infinity", + "outcome": "pass" + }, + { + "id": "refuse-io_seconds-negative-infinity", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-float-retries", + "outcome": "pass" + }, + { + "id": "refuse-retries-bool", + "outcome": "pass" + }, + { + "id": "refuse-retries-string", + "outcome": "pass" + }, + { + "id": "refuse-retries-nan", + "outcome": "pass" + }, + { + "id": "refuse-retries-positive-infinity", + "outcome": "pass" + }, + { + "id": "refuse-retries-negative-infinity", + "outcome": "pass" + }, + { + "id": "refuse-cpu-schema-[]", + "outcome": "pass" + }, + { + "id": "refuse-cpu-schema-[True]", + "outcome": "pass" + }, + { + "id": "refuse-cpu-schema-[0, 0]", + "outcome": "pass" + }, + { + "id": "refuse-cpu-schema-[0.0]", + "outcome": "pass" + }, + { + "id": "refuse-cpu-schema-['0']", + "outcome": "pass" + }, + { + "id": "refuse-cpu-schema-[0, 1, 2]", + "outcome": "pass" + }, + { + "id": "nonfinite-at-unexpected-depth-refuses", + "outcome": "pass" + }, + { + "id": "invalid-utf8-refuses", + "outcome": "pass" + }, + { + "id": "sha-check-preserved", + "outcome": "pass" + }, + { + "id": "predecessor-tree-normal", + "outcome": "pass" + }, + { + "id": "successor-tree-normal", + "outcome": "pass" + }, + { + "id": "predecessor-tree-normal-unknown", + "outcome": "pass" + }, + { + "id": "successor-tree-normal-unknown", + "outcome": "pass" + }, + { + "id": "predecessor-tree-after-root", + "outcome": "pass" + }, + { + "id": "successor-tree-after-root", + "outcome": "pass" + }, + { + "id": "predecessor-tree-after-wheelhouse", + "outcome": "pass" + }, + { + "id": "successor-tree-after-wheelhouse", + "outcome": "pass" + }, + { + "id": "predecessor-tree-after-root-unknown", + "outcome": "pass" + }, + { + "id": "successor-tree-after-root-unknown", + "outcome": "pass" + } + ], + "scope": "guardian authority/root-tree predecessor countercontrols only; no admitted data API", + "selected_archives_acquired": 0, + "source_sha256": "d52bb389127cb81b5b0303b3a593ee1af74f8c4835793d67f08d0bf182e2101c" +} diff --git a/qualification/issue-184/tool-acquisition/184-acquisition-guardian-v4-controls.py b/qualification/issue-184/tool-acquisition/184-acquisition-guardian-v4-controls.py new file mode 100644 index 0000000..e267f11 --- /dev/null +++ b/qualification/issue-184/tool-acquisition/184-acquisition-guardian-v4-controls.py @@ -0,0 +1,259 @@ +"""Narrow predecessor countercontrols; no data acquisition or admitted start.""" +from __future__ import annotations + +import copy +import hashlib +import importlib.util +import json +import os +from pathlib import Path +import socket +import sys +import tempfile +from unittest.mock import patch + +sys.dont_write_bytecode = True +BASE = Path('/tmp/sdk-python-handoff') +REF = 'https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-9999999999' +results = [] + + +def load(name, version): + path = BASE / f'184-acquisition-custody-guardian-{version}-proposal.py' + raw = path.read_bytes() + if version == 'v2': + assert hashlib.sha256(raw).hexdigest() == 'afc5f198de12d4b26520957bc563ff0122f8535838a1cbc64c40272c5f3f7f70' + spec = importlib.util.spec_from_file_location(name, path) + module = importlib.util.module_from_spec(spec); spec.loader.exec_module(module) + return module + + +old = load('trusted_frozen_v2_countercontrol', 'v2') +new = load('trusted_v4_correction_control', 'v4') +linked_predecessor = load('trusted_frozen_v3_link_countercontrol', 'v3') +assert hashlib.sha256((BASE/'184-acquisition-custody-guardian-v3-proposal.py').read_bytes()).hexdigest() == '79ddc82b0095872885aceb943cbf9daa1eb3a05048bc08f9ee9cd093511de187' +inputs = {key: 'a' * 64 for key in new.INPUT_FIELDS} +authority = dict(decision=REF, selected_source=new.SELECTED_SOURCE, inputs=inputs, + environment=new.MINIMAL_ENV, host='lab4', total_seconds=300, io_seconds=270, + retries=0, survivor_policy='retain-custody-and-escalate', + pool_parent='/home/lab4/fleet/tmp', launcher_sha256='b' * 64, cpu_affinity=[0, 1], + retention_deadline_utc='2099-01-01T00:00:00Z') + + +def bound(module, raw): + return module.bind_authority(raw, hashlib.sha256(raw).hexdigest(), REF, inputs) + + +def parser_refuses(raw, predecessor=True): + if predecessor: + assert bound(old, raw)['decision'] == REF, 'predecessor acceptance not reproduced' + try: + bound(new, raw) + except (ValueError, UnicodeError): + return + raise AssertionError('ambiguous authority accepted') + + +def sha_refuses(): + try: new.bind_authority(encoded(authority), '0' * 64, REF, inputs) + except new.Refusal as error: assert str(error) == 'AUTHORITY_BYTES_CHANGED' + else: raise AssertionError('changed authority SHA accepted') + + +def encoded(value): + return json.dumps(value, separators=(',', ':')).encode() + + +def mutate(key, value): + row = copy.deepcopy(authority); row[key] = value + return encoded(row) + + +def allocation(module, stage, unknown=False): + with tempfile.TemporaryDirectory(prefix='sdk-python-184-guardian-tree-controls-') as temp: + parent = Path(temp); leaf = 'sdk-python-184-tools-guardian-source-control' + root = parent / leaf + custody = None + real_mkdir = os.mkdir + def mkdir(path, mode=0o777, *, dir_fd=None): + if path == 'wheelhouse' and stage == 'after-root': + if unknown: (root / 'unknown-root-member').write_bytes(b'untouched') + raise OSError('synthetic second mkdir refusal') + return real_mkdir(path, mode, dir_fd=dir_fd) + real_open = os.open + def opened(path, flags, mode=0o777, *, dir_fd=None): + if path == 'one.whl' and stage == 'after-wheelhouse': + raise OSError('synthetic first archive refusal') + return real_open(path, flags, mode, dir_fd=dir_fd) + try: + with patch.object(module.os, 'mkdir', side_effect=mkdir), patch.object(module.os, 'open', side_effect=opened): + try: custody = module.ArchiveCustody(parent, leaf, ['one.whl']) + except module.Refusal as error: + if module is new: + assert isinstance(error, new.AllocationRefusal) + custody = error.custody + assert error.cleanup['complete'] is (not unknown) + assert error.cleanup['outer_owner_custody_required'] is unknown + if unknown: + assert root.exists() and (root / 'unknown-root-member').read_bytes() == b'untouched' + assert os.fstat(custody.parent_fd).st_uid == os.geteuid() + else: assert not root.exists() + else: raise + except OSError: + assert module is old and root.exists(), 'predecessor residue not reproduced' + else: + assert stage == 'normal' + if unknown: (root / 'unknown-root-member').write_bytes(b'untouched') + result = custody.cleanup() + if module is old: + assert result['complete'] and root.exists() and (root / 'wheelhouse').exists() + elif unknown: + assert not result['complete'] and result['outer_owner_custody_required'] + assert (root / 'unknown-root-member').read_bytes() == b'untouched' + assert not (root / 'wheelhouse').exists() + else: + assert result['complete'] and not result['outer_owner_custody_required'] + assert not root.exists(), 'TemporaryDirectory must not conceal retained directories' + finally: + if custody is not None: custody.close() + + +def removed_root_with_tracked_nodes(module, scenario): + with tempfile.TemporaryDirectory(prefix='sdk-python-184-linked-descendant-control-') as temp: + parent=Path(temp);custody=module.ArchiveCustody(parent, + 'sdk-python-184-tools-guardian-linked-control',['one.whl']) + extra_fd=None + try: + archive=custody.path/'wheelhouse'/'one.whl';archive.write_bytes(b'original synthetic bytes') + if scenario=='wheelhouse': + (custody.path/'wheelhouse').rename(parent/'moved-wheelhouse') + retained=parent/'moved-wheelhouse'/'one.whl' + else: + if scenario=='archive': + archive.rename(parent/'moved-archive.whl');retained=parent/'moved-archive.whl' + else:archive.unlink();retained=None + (custody.path/'wheelhouse').rmdir() + if scenario=='extra-directory': + (custody.path/'metadata').mkdir() + extra_fd=os.open(custody.path/'metadata',os.O_RDONLY|os.O_DIRECTORY) + st=os.fstat(extra_fd) + custody.dir_ids={'metadata':[st.st_dev,st.st_ino]} + custody.directory_fds={'metadata':extra_fd} + (custody.path/'metadata').rename(parent/'moved-metadata') + custody.path.rmdir() + assert os.fstat(custody.root_fd).st_nlink==0 + result=custody.cleanup() + if scenario=='fully-unlinked': + assert module is new and result['complete'] and result['all_unlinked'] + assert not result['outer_owner_custody_required'] + assert not result['retained_or_unproven_inodes'] + elif module is linked_predecessor: + assert result['complete'] and not result['outer_owner_custody_required'], 'old false completion not reproduced' + else: + assert not result['complete'] and result['outer_owner_custody_required'] + assert result['refusal_code']=='TRACKED_INODES_STILL_LINKED_OR_UNPROVEN' + expected='wheelhouse' if scenario=='wheelhouse' else 'archive:one.whl' if scenario=='archive' else 'metadata' + assert expected in result['retained_or_unproven_inodes'] + if retained is not None:assert retained.read_bytes()==b'original synthetic bytes' + if scenario=='extra-directory':assert (parent/'moved-metadata').is_dir() + finally: + if extra_fd is not None:os.close(extra_fd) + custody.close() + + +def allocation_identity_gap(module): + with tempfile.TemporaryDirectory(prefix='sdk-python-184-allocation-identity-control-') as temp: + parent=Path(temp);root=parent/'sdk-python-184-tools-guardian-identity-control' + real_stat=os.stat;faulted=False;held=None + def stat_before_identity(path,*args,**kwargs): + nonlocal faulted + if path=='wheelhouse' and not faulted: + faulted=True + (root/'wheelhouse').rename(parent/'moved-before-identity');root.rmdir() + raise OSError('synthetic identity read refusal') + return real_stat(path,*args,**kwargs) + try: + with patch.object(module.os,'stat',side_effect=stat_before_identity): + try:module.ArchiveCustody(parent,root.name,['one.whl']) + except module.AllocationRefusal as error:held=error.custody;result=error.cleanup + else:raise AssertionError('allocation identity fault absent') + assert faulted and (parent/'moved-before-identity').is_dir() + if module is linked_predecessor: + assert result['complete'] and not result['outer_owner_custody_required'] + else: + assert not result['complete'] and result['outer_owner_custody_required'] + assert result['refusal_code']=='TRACKED_INODES_STILL_LINKED_OR_UNPROVEN' + assert 'wheelhouse' in result['retained_or_unproven_inodes'] + finally: + if held is not None:held.close() + + +def check(name, operation): + try: operation() + except BaseException as error: + results.append({'id': name, 'outcome': 'fail', 'type': type(error).__name__, 'detail': str(error)[:250]}) + else: results.append({'id': name, 'outcome': 'pass'}) + + +with patch.object(socket, 'create_connection', side_effect=AssertionError('NO_NETWORK_IN_SOURCE_CONTROLS')): + check('predecessor-false-complete-allocation-before-child-identity',lambda:allocation_identity_gap(linked_predecessor)) + check('successor-retains-allocation-before-child-identity',lambda:allocation_identity_gap(new)) + for scenario in ('wheelhouse','archive','extra-directory'): + for module,label in ((linked_predecessor,'predecessor-false-complete'),(new,'successor-retains')): + check(label+'-removed-root-linked-'+scenario, + lambda module=module,scenario=scenario:removed_root_with_tracked_nodes(module,scenario)) + check('fully-unlinked-root-children-and-archives-completes',lambda:removed_root_with_tracked_nodes(new,'fully-unlinked')) + check('exact-schema-positive', lambda: bound(new, encoded(authority))) + for name, prefix in [('identical', '"decision":"' + REF + '",'), + ('conflicting', '"decision":"earlier-conflict",'), + ('escaped', '"decis\\u0069on":"earlier-conflict",')]: + check('old-accept-new-refuse-duplicate-' + name, + lambda prefix=prefix: parser_refuses(b'{' + prefix.encode() + encoded(authority)[1:])) + row = copy.deepcopy(authority); row['unreviewed_future_semantics'] = {} + check('old-accept-new-refuse-extra-top-field', lambda: parser_refuses(encoded(row))) + for key in ('survivor_policy', 'pool_parent', 'launcher_sha256', 'cpu_affinity', 'retention_deadline_utc'): + missing = copy.deepcopy(authority); del missing[key] + check('old-accept-new-refuse-missing-' + key, lambda missing=missing: parser_refuses(encoded(missing))) + for key, nested in [('inputs', inputs), ('environment', new.MINIMAL_ENV)]: + first = next(iter(nested)); duplicated = encoded(authority).replace( + ('"' + key + '":{').encode(), ('"' + key + '":{"' + first + '":"conflict",').encode(), 1) + check('old-accept-new-refuse-nested-duplicate-' + key, lambda raw=duplicated: parser_refuses(raw)) + for extra in (True, False): + value = dict(nested) + if extra: value['unknown'] = 'x' + else: del value[first] + check('refuse-nested-schema-' + key + ('-extra' if extra else '-missing'), + lambda key=key, value=value: parser_refuses(mutate(key, value), predecessor=False)) + for key in ('total_seconds', 'io_seconds', 'retries'): + check('old-accept-new-refuse-float-' + key, lambda key=key: parser_refuses(mutate(key, float(authority[key])))) + for kind, value in [('bool', False), ('string', str(authority[key])), ('nan', float('nan')), + ('positive-infinity', float('inf')), ('negative-infinity', -float('inf'))]: + check('refuse-' + key + '-' + kind, + lambda key=key, value=value: parser_refuses(mutate(key, value), predecessor=(key == 'retries' and value is False))) + for value in ([], [True], [0, 0], [0.0], ['0'], [0, 1, 2]): + check('refuse-cpu-schema-' + repr(value), lambda value=value: parser_refuses(mutate('cpu_affinity', value))) + check('nonfinite-at-unexpected-depth-refuses', lambda: parser_refuses( + encoded(authority).replace(b'"cpu_affinity":[0,1]', b'"cpu_affinity":[NaN]'))) + check('invalid-utf8-refuses', lambda: parser_refuses(b'\xff', predecessor=False)) + check('sha-check-preserved', sha_refuses) + for stage, unknown in [('normal', False), ('normal', True), ('after-root', False), + ('after-wheelhouse', False), ('after-root', True)]: + for module, label in ((old, 'predecessor'), (new, 'successor')): + check(label + '-tree-' + stage + ('-unknown' if unknown else ''), + lambda module=module, stage=stage, unknown=unknown: allocation(module, stage, unknown)) + +report = {'scope': 'guardian authority/root-tree predecessor countercontrols only; no admitted data API', + 'source_sha256': hashlib.sha256((BASE / '184-acquisition-custody-guardian-v4-proposal.py').read_bytes()).hexdigest(), + 'predecessor_sha256': 'afc5f198de12d4b26520957bc563ff0122f8535838a1cbc64c40272c5f3f7f70', + 'cases': len(results), 'passed': sum(row['outcome'] == 'pass' for row in results), + 'failed': sum(row['outcome'] == 'fail' for row in results), 'results': results, + 'real_network_requests': 0, 'selected_archives_acquired': 0, 'new_general_decision': None} +raw = (json.dumps(report, sort_keys=True, indent=2) + '\n').encode() +path = BASE / '184-acquisition-guardian-v4-controls-result.json' +if path.exists(): + if path.read_bytes() != raw: + print(json.dumps(report, indent=2)); raise SystemExit('CONTROL_RESULT_COLLISION; prior result retained') +else: + fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o400) + with os.fdopen(fd, 'wb') as stream: assert stream.write(raw) == len(raw) +print(json.dumps(report, indent=2)); raise SystemExit(bool(report['failed'])) diff --git a/qualification/issue-184/tool-acquisition/184-acquisition-guardian-v5-controls-result.json b/qualification/issue-184/tool-acquisition/184-acquisition-guardian-v5-controls-result.json new file mode 100644 index 0000000..6593373 --- /dev/null +++ b/qualification/issue-184/tool-acquisition/184-acquisition-guardian-v5-controls-result.json @@ -0,0 +1,277 @@ +{ + "cases": 66, + "failed": 0, + "new_general_decision": null, + "passed": 66, + "predecessor_sha256": "afc5f198de12d4b26520957bc563ff0122f8535838a1cbc64c40272c5f3f7f70", + "real_network_requests": 0, + "results": [ + { + "id": "v4-false-complete-opened-archive-before-identity", + "outcome": "pass" + }, + { + "id": "v5-retains-opened-archive-before-identity", + "outcome": "pass" + }, + { + "id": "v5-fully-unlinked-partial-archive-root-absent-positive", + "outcome": "pass" + }, + { + "id": "v5-fully-unlinked-partial-archive-root-present-positive", + "outcome": "pass" + }, + { + "id": "predecessor-false-complete-allocation-before-child-identity", + "outcome": "pass" + }, + { + "id": "successor-retains-allocation-before-child-identity", + "outcome": "pass" + }, + { + "id": "predecessor-false-complete-removed-root-linked-wheelhouse", + "outcome": "pass" + }, + { + "id": "successor-retains-removed-root-linked-wheelhouse", + "outcome": "pass" + }, + { + "id": "predecessor-false-complete-removed-root-linked-archive", + "outcome": "pass" + }, + { + "id": "successor-retains-removed-root-linked-archive", + "outcome": "pass" + }, + { + "id": "predecessor-false-complete-removed-root-linked-extra-directory", + "outcome": "pass" + }, + { + "id": "successor-retains-removed-root-linked-extra-directory", + "outcome": "pass" + }, + { + "id": "fully-unlinked-root-children-and-archives-completes", + "outcome": "pass" + }, + { + "id": "exact-schema-positive", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-duplicate-identical", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-duplicate-conflicting", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-duplicate-escaped", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-extra-top-field", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-missing-survivor_policy", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-missing-pool_parent", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-missing-launcher_sha256", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-missing-cpu_affinity", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-missing-retention_deadline_utc", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-nested-duplicate-inputs", + "outcome": "pass" + }, + { + "id": "refuse-nested-schema-inputs-extra", + "outcome": "pass" + }, + { + "id": "refuse-nested-schema-inputs-missing", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-nested-duplicate-environment", + "outcome": "pass" + }, + { + "id": "refuse-nested-schema-environment-extra", + "outcome": "pass" + }, + { + "id": "refuse-nested-schema-environment-missing", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-float-total_seconds", + "outcome": "pass" + }, + { + "id": "refuse-total_seconds-bool", + "outcome": "pass" + }, + { + "id": "refuse-total_seconds-string", + "outcome": "pass" + }, + { + "id": "refuse-total_seconds-nan", + "outcome": "pass" + }, + { + "id": "refuse-total_seconds-positive-infinity", + "outcome": "pass" + }, + { + "id": "refuse-total_seconds-negative-infinity", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-float-io_seconds", + "outcome": "pass" + }, + { + "id": "refuse-io_seconds-bool", + "outcome": "pass" + }, + { + "id": "refuse-io_seconds-string", + "outcome": "pass" + }, + { + "id": "refuse-io_seconds-nan", + "outcome": "pass" + }, + { + "id": "refuse-io_seconds-positive-infinity", + "outcome": "pass" + }, + { + "id": "refuse-io_seconds-negative-infinity", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-float-retries", + "outcome": "pass" + }, + { + "id": "refuse-retries-bool", + "outcome": "pass" + }, + { + "id": "refuse-retries-string", + "outcome": "pass" + }, + { + "id": "refuse-retries-nan", + "outcome": "pass" + }, + { + "id": "refuse-retries-positive-infinity", + "outcome": "pass" + }, + { + "id": "refuse-retries-negative-infinity", + "outcome": "pass" + }, + { + "id": "refuse-cpu-schema-[]", + "outcome": "pass" + }, + { + "id": "refuse-cpu-schema-[True]", + "outcome": "pass" + }, + { + "id": "refuse-cpu-schema-[0, 0]", + "outcome": "pass" + }, + { + "id": "refuse-cpu-schema-[0.0]", + "outcome": "pass" + }, + { + "id": "refuse-cpu-schema-['0']", + "outcome": "pass" + }, + { + "id": "refuse-cpu-schema-[0, 1, 2]", + "outcome": "pass" + }, + { + "id": "nonfinite-at-unexpected-depth-refuses", + "outcome": "pass" + }, + { + "id": "invalid-utf8-refuses", + "outcome": "pass" + }, + { + "id": "sha-check-preserved", + "outcome": "pass" + }, + { + "id": "predecessor-tree-normal", + "outcome": "pass" + }, + { + "id": "successor-tree-normal", + "outcome": "pass" + }, + { + "id": "predecessor-tree-normal-unknown", + "outcome": "pass" + }, + { + "id": "successor-tree-normal-unknown", + "outcome": "pass" + }, + { + "id": "predecessor-tree-after-root", + "outcome": "pass" + }, + { + "id": "successor-tree-after-root", + "outcome": "pass" + }, + { + "id": "predecessor-tree-after-wheelhouse", + "outcome": "pass" + }, + { + "id": "successor-tree-after-wheelhouse", + "outcome": "pass" + }, + { + "id": "predecessor-tree-after-root-unknown", + "outcome": "pass" + }, + { + "id": "successor-tree-after-root-unknown", + "outcome": "pass" + } + ], + "scope": "guardian authority/root-tree predecessor countercontrols only; no admitted data API", + "selected_archives_acquired": 0, + "source_sha256": "4c92d889d89dbef2502ef5e9bc923d00c0314821b42aa77d4c0ee92080aba41e" +} diff --git a/qualification/issue-184/tool-acquisition/184-acquisition-guardian-v5-controls.py b/qualification/issue-184/tool-acquisition/184-acquisition-guardian-v5-controls.py new file mode 100644 index 0000000..4921ede --- /dev/null +++ b/qualification/issue-184/tool-acquisition/184-acquisition-guardian-v5-controls.py @@ -0,0 +1,307 @@ +"""Narrow predecessor countercontrols; no data acquisition or admitted start.""" +from __future__ import annotations + +import copy +import hashlib +import importlib.util +import json +import os +from pathlib import Path +import socket +import sys +import tempfile +from unittest.mock import patch + +sys.dont_write_bytecode = True +BASE = Path('/tmp/sdk-python-handoff') +REF = 'https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-9999999999' +results = [] + + +def load(name, version): + path = BASE / f'184-acquisition-custody-guardian-{version}-proposal.py' + raw = path.read_bytes() + if version == 'v2': + assert hashlib.sha256(raw).hexdigest() == 'afc5f198de12d4b26520957bc563ff0122f8535838a1cbc64c40272c5f3f7f70' + spec = importlib.util.spec_from_file_location(name, path) + module = importlib.util.module_from_spec(spec); spec.loader.exec_module(module) + return module + + +old = load('trusted_frozen_v2_countercontrol', 'v2') +new = load('trusted_v5_correction_control', 'v5') +archive_predecessor = load('trusted_frozen_v4_archive_countercontrol', 'v4') +assert hashlib.sha256((BASE/'184-acquisition-custody-guardian-v4-proposal.py').read_bytes()).hexdigest() == 'd52bb389127cb81b5b0303b3a593ee1af74f8c4835793d67f08d0bf182e2101c' +linked_predecessor = load('trusted_frozen_v3_link_countercontrol', 'v3') +assert hashlib.sha256((BASE/'184-acquisition-custody-guardian-v3-proposal.py').read_bytes()).hexdigest() == '79ddc82b0095872885aceb943cbf9daa1eb3a05048bc08f9ee9cd093511de187' +inputs = {key: 'a' * 64 for key in new.INPUT_FIELDS} +authority = dict(decision=REF, selected_source=new.SELECTED_SOURCE, inputs=inputs, + environment=new.MINIMAL_ENV, host='lab4', total_seconds=300, io_seconds=270, + retries=0, survivor_policy='retain-custody-and-escalate', + pool_parent='/home/lab4/fleet/tmp', launcher_sha256='b' * 64, cpu_affinity=[0, 1], + retention_deadline_utc='2099-01-01T00:00:00Z') + + +def bound(module, raw): + return module.bind_authority(raw, hashlib.sha256(raw).hexdigest(), REF, inputs) + + +def parser_refuses(raw, predecessor=True): + if predecessor: + assert bound(old, raw)['decision'] == REF, 'predecessor acceptance not reproduced' + try: + bound(new, raw) + except (ValueError, UnicodeError): + return + raise AssertionError('ambiguous authority accepted') + + +def sha_refuses(): + try: new.bind_authority(encoded(authority), '0' * 64, REF, inputs) + except new.Refusal as error: assert str(error) == 'AUTHORITY_BYTES_CHANGED' + else: raise AssertionError('changed authority SHA accepted') + + +def encoded(value): + return json.dumps(value, separators=(',', ':')).encode() + + +def mutate(key, value): + row = copy.deepcopy(authority); row[key] = value + return encoded(row) + + +def allocation(module, stage, unknown=False): + with tempfile.TemporaryDirectory(prefix='sdk-python-184-guardian-tree-controls-') as temp: + parent = Path(temp); leaf = 'sdk-python-184-tools-guardian-source-control' + root = parent / leaf + custody = None + real_mkdir = os.mkdir + def mkdir(path, mode=0o777, *, dir_fd=None): + if path == 'wheelhouse' and stage == 'after-root': + if unknown: (root / 'unknown-root-member').write_bytes(b'untouched') + raise OSError('synthetic second mkdir refusal') + return real_mkdir(path, mode, dir_fd=dir_fd) + real_open = os.open + def opened(path, flags, mode=0o777, *, dir_fd=None): + if path == 'one.whl' and stage == 'after-wheelhouse': + raise OSError('synthetic first archive refusal') + return real_open(path, flags, mode, dir_fd=dir_fd) + try: + with patch.object(module.os, 'mkdir', side_effect=mkdir), patch.object(module.os, 'open', side_effect=opened): + try: custody = module.ArchiveCustody(parent, leaf, ['one.whl']) + except module.Refusal as error: + if module is new: + assert isinstance(error, new.AllocationRefusal) + custody = error.custody + assert error.cleanup['complete'] is (not unknown) + assert error.cleanup['outer_owner_custody_required'] is unknown + if unknown: + assert root.exists() and (root / 'unknown-root-member').read_bytes() == b'untouched' + assert os.fstat(custody.parent_fd).st_uid == os.geteuid() + else: assert not root.exists() + else: raise + except OSError: + assert module is old and root.exists(), 'predecessor residue not reproduced' + else: + assert stage == 'normal' + if unknown: (root / 'unknown-root-member').write_bytes(b'untouched') + result = custody.cleanup() + if module is old: + assert result['complete'] and root.exists() and (root / 'wheelhouse').exists() + elif unknown: + assert not result['complete'] and result['outer_owner_custody_required'] + assert (root / 'unknown-root-member').read_bytes() == b'untouched' + assert not (root / 'wheelhouse').exists() + else: + assert result['complete'] and not result['outer_owner_custody_required'] + assert not root.exists(), 'TemporaryDirectory must not conceal retained directories' + finally: + if custody is not None: custody.close() + + +def removed_root_with_tracked_nodes(module, scenario): + with tempfile.TemporaryDirectory(prefix='sdk-python-184-linked-descendant-control-') as temp: + parent=Path(temp);custody=module.ArchiveCustody(parent, + 'sdk-python-184-tools-guardian-linked-control',['one.whl']) + extra_fd=None + try: + archive=custody.path/'wheelhouse'/'one.whl';archive.write_bytes(b'original synthetic bytes') + if scenario=='wheelhouse': + (custody.path/'wheelhouse').rename(parent/'moved-wheelhouse') + retained=parent/'moved-wheelhouse'/'one.whl' + else: + if scenario=='archive': + archive.rename(parent/'moved-archive.whl');retained=parent/'moved-archive.whl' + else:archive.unlink();retained=None + (custody.path/'wheelhouse').rmdir() + if scenario=='extra-directory': + (custody.path/'metadata').mkdir() + extra_fd=os.open(custody.path/'metadata',os.O_RDONLY|os.O_DIRECTORY) + st=os.fstat(extra_fd) + custody.dir_ids={'metadata':[st.st_dev,st.st_ino]} + custody.directory_fds={'metadata':extra_fd} + (custody.path/'metadata').rename(parent/'moved-metadata') + custody.path.rmdir() + assert os.fstat(custody.root_fd).st_nlink==0 + result=custody.cleanup() + if scenario=='fully-unlinked': + assert module is new and result['complete'] and result['all_unlinked'] + assert not result['outer_owner_custody_required'] + assert not result['retained_or_unproven_inodes'] + elif module is linked_predecessor: + assert result['complete'] and not result['outer_owner_custody_required'], 'old false completion not reproduced' + else: + assert not result['complete'] and result['outer_owner_custody_required'] + assert result['refusal_code']=='TRACKED_INODES_STILL_LINKED_OR_UNPROVEN' + expected='wheelhouse' if scenario=='wheelhouse' else 'archive:one.whl' if scenario=='archive' else 'metadata' + assert expected in result['retained_or_unproven_inodes'] + if retained is not None:assert retained.read_bytes()==b'original synthetic bytes' + if scenario=='extra-directory':assert (parent/'moved-metadata').is_dir() + finally: + if extra_fd is not None:os.close(extra_fd) + custody.close() + + +def allocation_identity_gap(module): + with tempfile.TemporaryDirectory(prefix='sdk-python-184-allocation-identity-control-') as temp: + parent=Path(temp);root=parent/'sdk-python-184-tools-guardian-identity-control' + real_stat=os.stat;faulted=False;held=None + def stat_before_identity(path,*args,**kwargs): + nonlocal faulted + if path=='wheelhouse' and not faulted: + faulted=True + (root/'wheelhouse').rename(parent/'moved-before-identity');root.rmdir() + raise OSError('synthetic identity read refusal') + return real_stat(path,*args,**kwargs) + try: + with patch.object(module.os,'stat',side_effect=stat_before_identity): + try:module.ArchiveCustody(parent,root.name,['one.whl']) + except module.AllocationRefusal as error:held=error.custody;result=error.cleanup + else:raise AssertionError('allocation identity fault absent') + assert faulted and (parent/'moved-before-identity').is_dir() + if module is linked_predecessor: + assert result['complete'] and not result['outer_owner_custody_required'] + else: + assert not result['complete'] and result['outer_owner_custody_required'] + assert result['refusal_code']=='TRACKED_INODES_STILL_LINKED_OR_UNPROVEN' + assert 'wheelhouse' in result['retained_or_unproven_inodes'] + finally: + if held is not None:held.close() + + +def archive_before_identity(module,fully_unlinked=False,remove_root=True): + with tempfile.TemporaryDirectory(prefix='sdk-python-184-raw-archive-control-') as temp: + parent=Path(temp);root=parent/'sdk-python-184-tools-guardian-raw-archive-control' + opened=[];real_open=os.open;real_fstat=os.fstat;held=None;faulted=False + moved=parent/'moved-before-archive-identity.whl' + def exclusive_open(path,*args,**kwargs): + fd=real_open(path,*args,**kwargs) + if path=='one.whl':opened.append(fd) + return fd + def identity_read(fd): + nonlocal faulted + if fd in opened and not faulted: + faulted=True + os.write(fd,b'synthetic allocated bytes') + (root/'wheelhouse'/'one.whl').rename(moved) + if fully_unlinked:moved.unlink() + if remove_root:(root/'wheelhouse').rmdir();root.rmdir() + raise OSError('synthetic archive identity refusal') + return real_fstat(fd) + try: + with patch.object(module.os,'open',side_effect=exclusive_open),patch.object(module.os,'fstat',side_effect=identity_read): + try:module.ArchiveCustody(parent,root.name,['one.whl']) + except module.AllocationRefusal as error:held=error.custody;result=error.cleanup + else:raise AssertionError('archive identity fault absent') + assert faulted and len(opened)==1 + if module is archive_predecessor: + assert not held.archive_nodes and result['complete'] and not result['outer_owner_custody_required'] + assert moved.read_bytes()==b'synthetic allocated bytes' and moved.stat().st_nlink==1 + else: + assert held.archive_nodes['one.whl']=={'fd':opened[0],'identity':None} + if fully_unlinked: + assert result['complete'] and result['all_unlinked'] and not result['outer_owner_custody_required'] + assert real_fstat(opened[0]).st_nlink==0 and not root.exists() + else: + assert not result['complete'] and result['outer_owner_custody_required'] + assert result['refusal_code']=='TRACKED_INODES_STILL_LINKED_OR_UNPROVEN' + assert 'archive:one.whl' in result['retained_or_unproven_inodes'] + assert moved.read_bytes()==b'synthetic allocated bytes' and real_fstat(opened[0]).st_nlink==1 + finally: + if held is not None:held.close() + + +def check(name, operation): + try: operation() + except BaseException as error: + results.append({'id': name, 'outcome': 'fail', 'type': type(error).__name__, 'detail': str(error)[:250]}) + else: results.append({'id': name, 'outcome': 'pass'}) + + +with patch.object(socket, 'create_connection', side_effect=AssertionError('NO_NETWORK_IN_SOURCE_CONTROLS')): + check('v4-false-complete-opened-archive-before-identity',lambda:archive_before_identity(archive_predecessor)) + check('v5-retains-opened-archive-before-identity',lambda:archive_before_identity(new)) + check('v5-fully-unlinked-partial-archive-root-absent-positive',lambda:archive_before_identity(new,True,True)) + check('v5-fully-unlinked-partial-archive-root-present-positive',lambda:archive_before_identity(new,True,False)) + check('predecessor-false-complete-allocation-before-child-identity',lambda:allocation_identity_gap(linked_predecessor)) + check('successor-retains-allocation-before-child-identity',lambda:allocation_identity_gap(new)) + for scenario in ('wheelhouse','archive','extra-directory'): + for module,label in ((linked_predecessor,'predecessor-false-complete'),(new,'successor-retains')): + check(label+'-removed-root-linked-'+scenario, + lambda module=module,scenario=scenario:removed_root_with_tracked_nodes(module,scenario)) + check('fully-unlinked-root-children-and-archives-completes',lambda:removed_root_with_tracked_nodes(new,'fully-unlinked')) + check('exact-schema-positive', lambda: bound(new, encoded(authority))) + for name, prefix in [('identical', '"decision":"' + REF + '",'), + ('conflicting', '"decision":"earlier-conflict",'), + ('escaped', '"decis\\u0069on":"earlier-conflict",')]: + check('old-accept-new-refuse-duplicate-' + name, + lambda prefix=prefix: parser_refuses(b'{' + prefix.encode() + encoded(authority)[1:])) + row = copy.deepcopy(authority); row['unreviewed_future_semantics'] = {} + check('old-accept-new-refuse-extra-top-field', lambda: parser_refuses(encoded(row))) + for key in ('survivor_policy', 'pool_parent', 'launcher_sha256', 'cpu_affinity', 'retention_deadline_utc'): + missing = copy.deepcopy(authority); del missing[key] + check('old-accept-new-refuse-missing-' + key, lambda missing=missing: parser_refuses(encoded(missing))) + for key, nested in [('inputs', inputs), ('environment', new.MINIMAL_ENV)]: + first = next(iter(nested)); duplicated = encoded(authority).replace( + ('"' + key + '":{').encode(), ('"' + key + '":{"' + first + '":"conflict",').encode(), 1) + check('old-accept-new-refuse-nested-duplicate-' + key, lambda raw=duplicated: parser_refuses(raw)) + for extra in (True, False): + value = dict(nested) + if extra: value['unknown'] = 'x' + else: del value[first] + check('refuse-nested-schema-' + key + ('-extra' if extra else '-missing'), + lambda key=key, value=value: parser_refuses(mutate(key, value), predecessor=False)) + for key in ('total_seconds', 'io_seconds', 'retries'): + check('old-accept-new-refuse-float-' + key, lambda key=key: parser_refuses(mutate(key, float(authority[key])))) + for kind, value in [('bool', False), ('string', str(authority[key])), ('nan', float('nan')), + ('positive-infinity', float('inf')), ('negative-infinity', -float('inf'))]: + check('refuse-' + key + '-' + kind, + lambda key=key, value=value: parser_refuses(mutate(key, value), predecessor=(key == 'retries' and value is False))) + for value in ([], [True], [0, 0], [0.0], ['0'], [0, 1, 2]): + check('refuse-cpu-schema-' + repr(value), lambda value=value: parser_refuses(mutate('cpu_affinity', value))) + check('nonfinite-at-unexpected-depth-refuses', lambda: parser_refuses( + encoded(authority).replace(b'"cpu_affinity":[0,1]', b'"cpu_affinity":[NaN]'))) + check('invalid-utf8-refuses', lambda: parser_refuses(b'\xff', predecessor=False)) + check('sha-check-preserved', sha_refuses) + for stage, unknown in [('normal', False), ('normal', True), ('after-root', False), + ('after-wheelhouse', False), ('after-root', True)]: + for module, label in ((old, 'predecessor'), (new, 'successor')): + check(label + '-tree-' + stage + ('-unknown' if unknown else ''), + lambda module=module, stage=stage, unknown=unknown: allocation(module, stage, unknown)) + +report = {'scope': 'guardian authority/root-tree predecessor countercontrols only; no admitted data API', + 'source_sha256': hashlib.sha256((BASE / '184-acquisition-custody-guardian-v5-proposal.py').read_bytes()).hexdigest(), + 'predecessor_sha256': 'afc5f198de12d4b26520957bc563ff0122f8535838a1cbc64c40272c5f3f7f70', + 'cases': len(results), 'passed': sum(row['outcome'] == 'pass' for row in results), + 'failed': sum(row['outcome'] == 'fail' for row in results), 'results': results, + 'real_network_requests': 0, 'selected_archives_acquired': 0, 'new_general_decision': None} +raw = (json.dumps(report, sort_keys=True, indent=2) + '\n').encode() +path = BASE / '184-acquisition-guardian-v5-controls-result.json' +if path.exists(): + if path.read_bytes() != raw: + print(json.dumps(report, indent=2)); raise SystemExit('CONTROL_RESULT_COLLISION; prior result retained') +else: + fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o400) + with os.fdopen(fd, 'wb') as stream: assert stream.write(raw) == len(raw) +print(json.dumps(report, indent=2)); raise SystemExit(bool(report['failed'])) diff --git a/qualification/issue-184/tool-acquisition/184-acquisition-guardian-v6-controls-result.json b/qualification/issue-184/tool-acquisition/184-acquisition-guardian-v6-controls-result.json new file mode 100644 index 0000000..31682c1 --- /dev/null +++ b/qualification/issue-184/tool-acquisition/184-acquisition-guardian-v6-controls-result.json @@ -0,0 +1,305 @@ +{ + "cases": 73, + "failed": 0, + "new_general_decision": null, + "passed": 73, + "predecessor_sha256": "afc5f198de12d4b26520957bc563ff0122f8535838a1cbc64c40272c5f3f7f70", + "real_network_requests": 0, + "results": [ + { + "id": "v5-raw-fd-leak-MemoryError", + "outcome": "pass" + }, + { + "id": "v6-raw-fd-close-idempotent-MemoryError", + "outcome": "pass" + }, + { + "id": "v5-raw-fd-leak-KeyboardInterrupt", + "outcome": "pass" + }, + { + "id": "v6-raw-fd-close-idempotent-KeyboardInterrupt", + "outcome": "pass" + }, + { + "id": "v6-parent-callback-failure-closes-known-fd", + "outcome": "pass" + }, + { + "id": "v6-root-callback-failure-closes-known-fd", + "outcome": "pass" + }, + { + "id": "v6-wheelhouse-callback-failure-closes-known-fd", + "outcome": "pass" + }, + { + "id": "v4-false-complete-opened-archive-before-identity", + "outcome": "pass" + }, + { + "id": "v5-retains-opened-archive-before-identity", + "outcome": "pass" + }, + { + "id": "v5-fully-unlinked-partial-archive-root-absent-positive", + "outcome": "pass" + }, + { + "id": "v5-fully-unlinked-partial-archive-root-present-positive", + "outcome": "pass" + }, + { + "id": "predecessor-false-complete-allocation-before-child-identity", + "outcome": "pass" + }, + { + "id": "successor-retains-allocation-before-child-identity", + "outcome": "pass" + }, + { + "id": "predecessor-false-complete-removed-root-linked-wheelhouse", + "outcome": "pass" + }, + { + "id": "successor-retains-removed-root-linked-wheelhouse", + "outcome": "pass" + }, + { + "id": "predecessor-false-complete-removed-root-linked-archive", + "outcome": "pass" + }, + { + "id": "successor-retains-removed-root-linked-archive", + "outcome": "pass" + }, + { + "id": "predecessor-false-complete-removed-root-linked-extra-directory", + "outcome": "pass" + }, + { + "id": "successor-retains-removed-root-linked-extra-directory", + "outcome": "pass" + }, + { + "id": "fully-unlinked-root-children-and-archives-completes", + "outcome": "pass" + }, + { + "id": "exact-schema-positive", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-duplicate-identical", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-duplicate-conflicting", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-duplicate-escaped", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-extra-top-field", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-missing-survivor_policy", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-missing-pool_parent", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-missing-launcher_sha256", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-missing-cpu_affinity", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-missing-retention_deadline_utc", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-nested-duplicate-inputs", + "outcome": "pass" + }, + { + "id": "refuse-nested-schema-inputs-extra", + "outcome": "pass" + }, + { + "id": "refuse-nested-schema-inputs-missing", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-nested-duplicate-environment", + "outcome": "pass" + }, + { + "id": "refuse-nested-schema-environment-extra", + "outcome": "pass" + }, + { + "id": "refuse-nested-schema-environment-missing", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-float-total_seconds", + "outcome": "pass" + }, + { + "id": "refuse-total_seconds-bool", + "outcome": "pass" + }, + { + "id": "refuse-total_seconds-string", + "outcome": "pass" + }, + { + "id": "refuse-total_seconds-nan", + "outcome": "pass" + }, + { + "id": "refuse-total_seconds-positive-infinity", + "outcome": "pass" + }, + { + "id": "refuse-total_seconds-negative-infinity", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-float-io_seconds", + "outcome": "pass" + }, + { + "id": "refuse-io_seconds-bool", + "outcome": "pass" + }, + { + "id": "refuse-io_seconds-string", + "outcome": "pass" + }, + { + "id": "refuse-io_seconds-nan", + "outcome": "pass" + }, + { + "id": "refuse-io_seconds-positive-infinity", + "outcome": "pass" + }, + { + "id": "refuse-io_seconds-negative-infinity", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-float-retries", + "outcome": "pass" + }, + { + "id": "refuse-retries-bool", + "outcome": "pass" + }, + { + "id": "refuse-retries-string", + "outcome": "pass" + }, + { + "id": "refuse-retries-nan", + "outcome": "pass" + }, + { + "id": "refuse-retries-positive-infinity", + "outcome": "pass" + }, + { + "id": "refuse-retries-negative-infinity", + "outcome": "pass" + }, + { + "id": "refuse-cpu-schema-[]", + "outcome": "pass" + }, + { + "id": "refuse-cpu-schema-[True]", + "outcome": "pass" + }, + { + "id": "refuse-cpu-schema-[0, 0]", + "outcome": "pass" + }, + { + "id": "refuse-cpu-schema-[0.0]", + "outcome": "pass" + }, + { + "id": "refuse-cpu-schema-['0']", + "outcome": "pass" + }, + { + "id": "refuse-cpu-schema-[0, 1, 2]", + "outcome": "pass" + }, + { + "id": "nonfinite-at-unexpected-depth-refuses", + "outcome": "pass" + }, + { + "id": "invalid-utf8-refuses", + "outcome": "pass" + }, + { + "id": "sha-check-preserved", + "outcome": "pass" + }, + { + "id": "predecessor-tree-normal", + "outcome": "pass" + }, + { + "id": "successor-tree-normal", + "outcome": "pass" + }, + { + "id": "predecessor-tree-normal-unknown", + "outcome": "pass" + }, + { + "id": "successor-tree-normal-unknown", + "outcome": "pass" + }, + { + "id": "predecessor-tree-after-root", + "outcome": "pass" + }, + { + "id": "successor-tree-after-root", + "outcome": "pass" + }, + { + "id": "predecessor-tree-after-wheelhouse", + "outcome": "pass" + }, + { + "id": "successor-tree-after-wheelhouse", + "outcome": "pass" + }, + { + "id": "predecessor-tree-after-root-unknown", + "outcome": "pass" + }, + { + "id": "successor-tree-after-root-unknown", + "outcome": "pass" + } + ], + "scope": "guardian authority/root-tree predecessor countercontrols only; no admitted data API", + "selected_archives_acquired": 0, + "source_sha256": "cbb1672855a0dec48c7d952f02be49ab3c0935d9bd7b1cf467638d73fc543e4c" +} diff --git a/qualification/issue-184/tool-acquisition/184-acquisition-guardian-v6-controls.py b/qualification/issue-184/tool-acquisition/184-acquisition-guardian-v6-controls.py new file mode 100644 index 0000000..3d1202d --- /dev/null +++ b/qualification/issue-184/tool-acquisition/184-acquisition-guardian-v6-controls.py @@ -0,0 +1,363 @@ +"""Narrow predecessor countercontrols; no data acquisition or admitted start.""" +from __future__ import annotations + +import copy +import hashlib +import importlib.util +import json +import os +from pathlib import Path +import socket +import sys +import tempfile +from unittest.mock import patch + +sys.dont_write_bytecode = True +BASE = Path('/tmp/sdk-python-handoff') +REF = 'https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-9999999999' +results = [] + + +def load(name, version): + path = BASE / f'184-acquisition-custody-guardian-{version}-proposal.py' + raw = path.read_bytes() + if version == 'v2': + assert hashlib.sha256(raw).hexdigest() == 'afc5f198de12d4b26520957bc563ff0122f8535838a1cbc64c40272c5f3f7f70' + spec = importlib.util.spec_from_file_location(name, path) + module = importlib.util.module_from_spec(spec); spec.loader.exec_module(module) + return module + + +old = load('trusted_frozen_v2_countercontrol', 'v2') +new = load('trusted_v6_correction_control', 'v6') +callback_predecessor = load('trusted_frozen_v5_callback_countercontrol', 'v5') +assert hashlib.sha256((BASE/'184-acquisition-custody-guardian-v5-proposal.py').read_bytes()).hexdigest() == '4c92d889d89dbef2502ef5e9bc923d00c0314821b42aa77d4c0ee92080aba41e' +archive_predecessor = load('trusted_frozen_v4_archive_countercontrol', 'v4') +assert hashlib.sha256((BASE/'184-acquisition-custody-guardian-v4-proposal.py').read_bytes()).hexdigest() == 'd52bb389127cb81b5b0303b3a593ee1af74f8c4835793d67f08d0bf182e2101c' +linked_predecessor = load('trusted_frozen_v3_link_countercontrol', 'v3') +assert hashlib.sha256((BASE/'184-acquisition-custody-guardian-v3-proposal.py').read_bytes()).hexdigest() == '79ddc82b0095872885aceb943cbf9daa1eb3a05048bc08f9ee9cd093511de187' +inputs = {key: 'a' * 64 for key in new.INPUT_FIELDS} +authority = dict(decision=REF, selected_source=new.SELECTED_SOURCE, inputs=inputs, + environment=new.MINIMAL_ENV, host='lab4', total_seconds=300, io_seconds=270, + retries=0, survivor_policy='retain-custody-and-escalate', + pool_parent='/home/lab4/fleet/tmp', launcher_sha256='b' * 64, cpu_affinity=[0, 1], + retention_deadline_utc='2099-01-01T00:00:00Z') + + +def bound(module, raw): + return module.bind_authority(raw, hashlib.sha256(raw).hexdigest(), REF, inputs) + + +def parser_refuses(raw, predecessor=True): + if predecessor: + assert bound(old, raw)['decision'] == REF, 'predecessor acceptance not reproduced' + try: + bound(new, raw) + except (ValueError, UnicodeError): + return + raise AssertionError('ambiguous authority accepted') + + +def sha_refuses(): + try: new.bind_authority(encoded(authority), '0' * 64, REF, inputs) + except new.Refusal as error: assert str(error) == 'AUTHORITY_BYTES_CHANGED' + else: raise AssertionError('changed authority SHA accepted') + + +def encoded(value): + return json.dumps(value, separators=(',', ':')).encode() + + +def mutate(key, value): + row = copy.deepcopy(authority); row[key] = value + return encoded(row) + + +def allocation(module, stage, unknown=False): + with tempfile.TemporaryDirectory(prefix='sdk-python-184-guardian-tree-controls-') as temp: + parent = Path(temp); leaf = 'sdk-python-184-tools-guardian-source-control' + root = parent / leaf + custody = None + real_mkdir = os.mkdir + def mkdir(path, mode=0o777, *, dir_fd=None): + if path == 'wheelhouse' and stage == 'after-root': + if unknown: (root / 'unknown-root-member').write_bytes(b'untouched') + raise OSError('synthetic second mkdir refusal') + return real_mkdir(path, mode, dir_fd=dir_fd) + real_open = os.open + def opened(path, flags, mode=0o777, *, dir_fd=None): + if path == 'one.whl' and stage == 'after-wheelhouse': + raise OSError('synthetic first archive refusal') + return real_open(path, flags, mode, dir_fd=dir_fd) + try: + with patch.object(module.os, 'mkdir', side_effect=mkdir), patch.object(module.os, 'open', side_effect=opened): + try: custody = module.ArchiveCustody(parent, leaf, ['one.whl']) + except module.Refusal as error: + if module is new: + assert isinstance(error, new.AllocationRefusal) + custody = error.custody + assert error.cleanup['complete'] is (not unknown) + assert error.cleanup['outer_owner_custody_required'] is unknown + if unknown: + assert root.exists() and (root / 'unknown-root-member').read_bytes() == b'untouched' + assert os.fstat(custody.parent_fd).st_uid == os.geteuid() + else: assert not root.exists() + else: raise + except OSError: + assert module is old and root.exists(), 'predecessor residue not reproduced' + else: + assert stage == 'normal' + if unknown: (root / 'unknown-root-member').write_bytes(b'untouched') + result = custody.cleanup() + if module is old: + assert result['complete'] and root.exists() and (root / 'wheelhouse').exists() + elif unknown: + assert not result['complete'] and result['outer_owner_custody_required'] + assert (root / 'unknown-root-member').read_bytes() == b'untouched' + assert not (root / 'wheelhouse').exists() + else: + assert result['complete'] and not result['outer_owner_custody_required'] + assert not root.exists(), 'TemporaryDirectory must not conceal retained directories' + finally: + if custody is not None: custody.close() + + +def removed_root_with_tracked_nodes(module, scenario): + with tempfile.TemporaryDirectory(prefix='sdk-python-184-linked-descendant-control-') as temp: + parent=Path(temp);custody=module.ArchiveCustody(parent, + 'sdk-python-184-tools-guardian-linked-control',['one.whl']) + extra_fd=None + try: + archive=custody.path/'wheelhouse'/'one.whl';archive.write_bytes(b'original synthetic bytes') + if scenario=='wheelhouse': + (custody.path/'wheelhouse').rename(parent/'moved-wheelhouse') + retained=parent/'moved-wheelhouse'/'one.whl' + else: + if scenario=='archive': + archive.rename(parent/'moved-archive.whl');retained=parent/'moved-archive.whl' + else:archive.unlink();retained=None + (custody.path/'wheelhouse').rmdir() + if scenario=='extra-directory': + (custody.path/'metadata').mkdir() + extra_fd=os.open(custody.path/'metadata',os.O_RDONLY|os.O_DIRECTORY) + st=os.fstat(extra_fd) + custody.dir_ids={'metadata':[st.st_dev,st.st_ino]} + custody.directory_fds={'metadata':extra_fd} + (custody.path/'metadata').rename(parent/'moved-metadata') + custody.path.rmdir() + assert os.fstat(custody.root_fd).st_nlink==0 + result=custody.cleanup() + if scenario=='fully-unlinked': + assert module is new and result['complete'] and result['all_unlinked'] + assert not result['outer_owner_custody_required'] + assert not result['retained_or_unproven_inodes'] + elif module is linked_predecessor: + assert result['complete'] and not result['outer_owner_custody_required'], 'old false completion not reproduced' + else: + assert not result['complete'] and result['outer_owner_custody_required'] + assert result['refusal_code']=='TRACKED_INODES_STILL_LINKED_OR_UNPROVEN' + expected='wheelhouse' if scenario=='wheelhouse' else 'archive:one.whl' if scenario=='archive' else 'metadata' + assert expected in result['retained_or_unproven_inodes'] + if retained is not None:assert retained.read_bytes()==b'original synthetic bytes' + if scenario=='extra-directory':assert (parent/'moved-metadata').is_dir() + finally: + if extra_fd is not None:os.close(extra_fd) + custody.close() + + +def allocation_identity_gap(module): + with tempfile.TemporaryDirectory(prefix='sdk-python-184-allocation-identity-control-') as temp: + parent=Path(temp);root=parent/'sdk-python-184-tools-guardian-identity-control' + real_stat=os.stat;faulted=False;held=None + def stat_before_identity(path,*args,**kwargs): + nonlocal faulted + if path=='wheelhouse' and not faulted: + faulted=True + (root/'wheelhouse').rename(parent/'moved-before-identity');root.rmdir() + raise OSError('synthetic identity read refusal') + return real_stat(path,*args,**kwargs) + try: + with patch.object(module.os,'stat',side_effect=stat_before_identity): + try:module.ArchiveCustody(parent,root.name,['one.whl']) + except module.AllocationRefusal as error:held=error.custody;result=error.cleanup + else:raise AssertionError('allocation identity fault absent') + assert faulted and (parent/'moved-before-identity').is_dir() + if module is linked_predecessor: + assert result['complete'] and not result['outer_owner_custody_required'] + else: + assert not result['complete'] and result['outer_owner_custody_required'] + assert result['refusal_code']=='TRACKED_INODES_STILL_LINKED_OR_UNPROVEN' + assert 'wheelhouse' in result['retained_or_unproven_inodes'] + finally: + if held is not None:held.close() + + +def archive_before_identity(module,fully_unlinked=False,remove_root=True): + with tempfile.TemporaryDirectory(prefix='sdk-python-184-raw-archive-control-') as temp: + parent=Path(temp);root=parent/'sdk-python-184-tools-guardian-raw-archive-control' + opened=[];real_open=os.open;real_fstat=os.fstat;held=None;faulted=False + moved=parent/'moved-before-archive-identity.whl' + def exclusive_open(path,*args,**kwargs): + fd=real_open(path,*args,**kwargs) + if path=='one.whl':opened.append(fd) + return fd + def identity_read(fd): + nonlocal faulted + if fd in opened and not faulted: + faulted=True + os.write(fd,b'synthetic allocated bytes') + (root/'wheelhouse'/'one.whl').rename(moved) + if fully_unlinked:moved.unlink() + if remove_root:(root/'wheelhouse').rmdir();root.rmdir() + raise OSError('synthetic archive identity refusal') + return real_fstat(fd) + try: + with patch.object(module.os,'open',side_effect=exclusive_open),patch.object(module.os,'fstat',side_effect=identity_read): + try:module.ArchiveCustody(parent,root.name,['one.whl']) + except module.AllocationRefusal as error:held=error.custody;result=error.cleanup + else:raise AssertionError('archive identity fault absent') + assert faulted and len(opened)==1 + if module is archive_predecessor: + assert not held.archive_nodes and result['complete'] and not result['outer_owner_custody_required'] + assert moved.read_bytes()==b'synthetic allocated bytes' and moved.stat().st_nlink==1 + else: + assert held.archive_nodes['one.whl']=={'fd':opened[0],'identity':None} + if fully_unlinked: + assert result['complete'] and result['all_unlinked'] and not result['outer_owner_custody_required'] + assert real_fstat(opened[0]).st_nlink==0 and not root.exists() + else: + assert not result['complete'] and result['outer_owner_custody_required'] + assert result['refusal_code']=='TRACKED_INODES_STILL_LINKED_OR_UNPROVEN' + assert 'archive:one.whl' in result['retained_or_unproven_inodes'] + assert moved.read_bytes()==b'synthetic allocated bytes' and real_fstat(opened[0]).st_nlink==1 + finally: + if held is not None:held.close() + + +def callback_registration_failure(module,interruption,stage='archive'): + with tempfile.TemporaryDirectory(prefix='sdk-python-184-callback-close-control-') as temp: + parent=Path(temp);root=parent/'sdk-python-184-tools-guardian-callback-control' + opened=[];real_open=os.open;real_callback=module.ExitStack.callback;held=None + def allocated_open(path,*args,**kwargs): + fd=real_open(path,*args,**kwargs) + target=(path=='one.whl' if stage=='archive' else path==root.name if stage=='root' + else path=='wheelhouse' if stage=='wheelhouse' else path==parent) + if target:opened.append(fd) + return fd + def callback(stack,function,*args,**kwargs): + if args and args[0] in opened:raise interruption('synthetic callback registration failure') + return real_callback(stack,function,*args,**kwargs) + try: + with patch.object(module.os,'open',side_effect=allocated_open),patch.object(module.ExitStack,'callback',new=callback): + try:module.ArchiveCustody(parent,root.name,['one.whl']) + except module.AllocationRefusal as error:held=error.custody;result=error.cleanup + else:raise AssertionError('callback registration fault absent') + assert held is not None and len(opened)==1 + fd=opened[0] + if stage=='archive': + assert not result['complete'] and result['outer_owner_custody_required'] + assert held.archive_nodes['one.whl']=={'fd':fd,'identity':None} + assert (root/'wheelhouse'/'one.whl').exists() + held.close() + if module is callback_predecessor: + assert os.fstat(fd).st_nlink==1, 'predecessor callback leak not reproduced' + os.close(fd) + else: + try:os.fstat(fd) + except OSError as error:assert error.errno==9 + else:raise AssertionError('raw descriptor survived custody.close') + # A second close must not close a new unrelated fd at that number. + unrelated=os.open(parent/'unrelated',os.O_RDWR|os.O_CREAT|os.O_EXCL,0o600) + reused=os.dup2(unrelated,fd) + try: + (parent/'unrelated').unlink() + held.close();assert os.fstat(reused).st_nlink==0 + if stage=='archive': + assert (root/'wheelhouse'/'one.whl').exists() + assert not result['complete'] and result['outer_owner_custody_required'] + assert 'archive:one.whl' in held.unlink_state()['retained_or_unproven_inodes'] + finally: + os.close(reused) + if unrelated!=reused:os.close(unrelated) + finally: + if held is not None:held.close() + + +def check(name, operation): + try: operation() + except BaseException as error: + results.append({'id': name, 'outcome': 'fail', 'type': type(error).__name__, 'detail': str(error)[:250]}) + else: results.append({'id': name, 'outcome': 'pass'}) + + +with patch.object(socket, 'create_connection', side_effect=AssertionError('NO_NETWORK_IN_SOURCE_CONTROLS')): + for interruption in (MemoryError,KeyboardInterrupt): + for module,label in ((callback_predecessor,'v5-raw-fd-leak'),(new,'v6-raw-fd-close-idempotent')): + check(label+'-'+interruption.__name__,lambda module=module,interruption=interruption:callback_registration_failure(module,interruption)) + for stage in ('parent','root','wheelhouse'): + check('v6-'+stage+'-callback-failure-closes-known-fd',lambda stage=stage:callback_registration_failure(new,MemoryError,stage)) + check('v4-false-complete-opened-archive-before-identity',lambda:archive_before_identity(archive_predecessor)) + check('v5-retains-opened-archive-before-identity',lambda:archive_before_identity(new)) + check('v5-fully-unlinked-partial-archive-root-absent-positive',lambda:archive_before_identity(new,True,True)) + check('v5-fully-unlinked-partial-archive-root-present-positive',lambda:archive_before_identity(new,True,False)) + check('predecessor-false-complete-allocation-before-child-identity',lambda:allocation_identity_gap(linked_predecessor)) + check('successor-retains-allocation-before-child-identity',lambda:allocation_identity_gap(new)) + for scenario in ('wheelhouse','archive','extra-directory'): + for module,label in ((linked_predecessor,'predecessor-false-complete'),(new,'successor-retains')): + check(label+'-removed-root-linked-'+scenario, + lambda module=module,scenario=scenario:removed_root_with_tracked_nodes(module,scenario)) + check('fully-unlinked-root-children-and-archives-completes',lambda:removed_root_with_tracked_nodes(new,'fully-unlinked')) + check('exact-schema-positive', lambda: bound(new, encoded(authority))) + for name, prefix in [('identical', '"decision":"' + REF + '",'), + ('conflicting', '"decision":"earlier-conflict",'), + ('escaped', '"decis\\u0069on":"earlier-conflict",')]: + check('old-accept-new-refuse-duplicate-' + name, + lambda prefix=prefix: parser_refuses(b'{' + prefix.encode() + encoded(authority)[1:])) + row = copy.deepcopy(authority); row['unreviewed_future_semantics'] = {} + check('old-accept-new-refuse-extra-top-field', lambda: parser_refuses(encoded(row))) + for key in ('survivor_policy', 'pool_parent', 'launcher_sha256', 'cpu_affinity', 'retention_deadline_utc'): + missing = copy.deepcopy(authority); del missing[key] + check('old-accept-new-refuse-missing-' + key, lambda missing=missing: parser_refuses(encoded(missing))) + for key, nested in [('inputs', inputs), ('environment', new.MINIMAL_ENV)]: + first = next(iter(nested)); duplicated = encoded(authority).replace( + ('"' + key + '":{').encode(), ('"' + key + '":{"' + first + '":"conflict",').encode(), 1) + check('old-accept-new-refuse-nested-duplicate-' + key, lambda raw=duplicated: parser_refuses(raw)) + for extra in (True, False): + value = dict(nested) + if extra: value['unknown'] = 'x' + else: del value[first] + check('refuse-nested-schema-' + key + ('-extra' if extra else '-missing'), + lambda key=key, value=value: parser_refuses(mutate(key, value), predecessor=False)) + for key in ('total_seconds', 'io_seconds', 'retries'): + check('old-accept-new-refuse-float-' + key, lambda key=key: parser_refuses(mutate(key, float(authority[key])))) + for kind, value in [('bool', False), ('string', str(authority[key])), ('nan', float('nan')), + ('positive-infinity', float('inf')), ('negative-infinity', -float('inf'))]: + check('refuse-' + key + '-' + kind, + lambda key=key, value=value: parser_refuses(mutate(key, value), predecessor=(key == 'retries' and value is False))) + for value in ([], [True], [0, 0], [0.0], ['0'], [0, 1, 2]): + check('refuse-cpu-schema-' + repr(value), lambda value=value: parser_refuses(mutate('cpu_affinity', value))) + check('nonfinite-at-unexpected-depth-refuses', lambda: parser_refuses( + encoded(authority).replace(b'"cpu_affinity":[0,1]', b'"cpu_affinity":[NaN]'))) + check('invalid-utf8-refuses', lambda: parser_refuses(b'\xff', predecessor=False)) + check('sha-check-preserved', sha_refuses) + for stage, unknown in [('normal', False), ('normal', True), ('after-root', False), + ('after-wheelhouse', False), ('after-root', True)]: + for module, label in ((old, 'predecessor'), (new, 'successor')): + check(label + '-tree-' + stage + ('-unknown' if unknown else ''), + lambda module=module, stage=stage, unknown=unknown: allocation(module, stage, unknown)) + +report = {'scope': 'guardian authority/root-tree predecessor countercontrols only; no admitted data API', + 'source_sha256': hashlib.sha256((BASE / '184-acquisition-custody-guardian-v6-proposal.py').read_bytes()).hexdigest(), + 'predecessor_sha256': 'afc5f198de12d4b26520957bc563ff0122f8535838a1cbc64c40272c5f3f7f70', + 'cases': len(results), 'passed': sum(row['outcome'] == 'pass' for row in results), + 'failed': sum(row['outcome'] == 'fail' for row in results), 'results': results, + 'real_network_requests': 0, 'selected_archives_acquired': 0, 'new_general_decision': None} +raw = (json.dumps(report, sort_keys=True, indent=2) + '\n').encode() +path = BASE / '184-acquisition-guardian-v6-controls-result.json' +if path.exists(): + if path.read_bytes() != raw: + print(json.dumps(report, indent=2)); raise SystemExit('CONTROL_RESULT_COLLISION; prior result retained') +else: + fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o400) + with os.fdopen(fd, 'wb') as stream: assert stream.write(raw) == len(raw) +print(json.dumps(report, indent=2)); raise SystemExit(bool(report['failed'])) diff --git a/qualification/issue-184/tool-acquisition/184-acquisition-guardian-v7-controls-result.json b/qualification/issue-184/tool-acquisition/184-acquisition-guardian-v7-controls-result.json new file mode 100644 index 0000000..380abfa --- /dev/null +++ b/qualification/issue-184/tool-acquisition/184-acquisition-guardian-v7-controls-result.json @@ -0,0 +1,337 @@ +{ + "cases": 81, + "failed": 0, + "new_general_decision": null, + "passed": 81, + "predecessor_sha256": "afc5f198de12d4b26520957bc563ff0122f8535838a1cbc64c40272c5f3f7f70", + "real_network_requests": 0, + "results": [ + { + "id": "v6-leak-archive-before-ledger-MemoryError-root-present", + "outcome": "pass" + }, + { + "id": "v7-close-retain-unknown-archive-before-ledger-MemoryError-root-present", + "outcome": "pass" + }, + { + "id": "v6-leak-archive-before-ledger-MemoryError-root-removed", + "outcome": "pass" + }, + { + "id": "v7-close-retain-unknown-archive-before-ledger-MemoryError-root-removed", + "outcome": "pass" + }, + { + "id": "v6-leak-archive-before-ledger-KeyboardInterrupt-root-present", + "outcome": "pass" + }, + { + "id": "v7-close-retain-unknown-archive-before-ledger-KeyboardInterrupt-root-present", + "outcome": "pass" + }, + { + "id": "v6-leak-archive-before-ledger-KeyboardInterrupt-root-removed", + "outcome": "pass" + }, + { + "id": "v7-close-retain-unknown-archive-before-ledger-KeyboardInterrupt-root-removed", + "outcome": "pass" + }, + { + "id": "v5-raw-fd-leak-MemoryError", + "outcome": "pass" + }, + { + "id": "v6-raw-fd-close-idempotent-MemoryError", + "outcome": "pass" + }, + { + "id": "v5-raw-fd-leak-KeyboardInterrupt", + "outcome": "pass" + }, + { + "id": "v6-raw-fd-close-idempotent-KeyboardInterrupt", + "outcome": "pass" + }, + { + "id": "v6-parent-callback-failure-closes-known-fd", + "outcome": "pass" + }, + { + "id": "v6-root-callback-failure-closes-known-fd", + "outcome": "pass" + }, + { + "id": "v6-wheelhouse-callback-failure-closes-known-fd", + "outcome": "pass" + }, + { + "id": "v4-false-complete-opened-archive-before-identity", + "outcome": "pass" + }, + { + "id": "v5-retains-opened-archive-before-identity", + "outcome": "pass" + }, + { + "id": "v5-fully-unlinked-partial-archive-root-absent-positive", + "outcome": "pass" + }, + { + "id": "v5-fully-unlinked-partial-archive-root-present-positive", + "outcome": "pass" + }, + { + "id": "predecessor-false-complete-allocation-before-child-identity", + "outcome": "pass" + }, + { + "id": "successor-retains-allocation-before-child-identity", + "outcome": "pass" + }, + { + "id": "predecessor-false-complete-removed-root-linked-wheelhouse", + "outcome": "pass" + }, + { + "id": "successor-retains-removed-root-linked-wheelhouse", + "outcome": "pass" + }, + { + "id": "predecessor-false-complete-removed-root-linked-archive", + "outcome": "pass" + }, + { + "id": "successor-retains-removed-root-linked-archive", + "outcome": "pass" + }, + { + "id": "predecessor-false-complete-removed-root-linked-extra-directory", + "outcome": "pass" + }, + { + "id": "successor-retains-removed-root-linked-extra-directory", + "outcome": "pass" + }, + { + "id": "fully-unlinked-root-children-and-archives-completes", + "outcome": "pass" + }, + { + "id": "exact-schema-positive", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-duplicate-identical", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-duplicate-conflicting", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-duplicate-escaped", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-extra-top-field", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-missing-survivor_policy", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-missing-pool_parent", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-missing-launcher_sha256", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-missing-cpu_affinity", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-missing-retention_deadline_utc", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-nested-duplicate-inputs", + "outcome": "pass" + }, + { + "id": "refuse-nested-schema-inputs-extra", + "outcome": "pass" + }, + { + "id": "refuse-nested-schema-inputs-missing", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-nested-duplicate-environment", + "outcome": "pass" + }, + { + "id": "refuse-nested-schema-environment-extra", + "outcome": "pass" + }, + { + "id": "refuse-nested-schema-environment-missing", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-float-total_seconds", + "outcome": "pass" + }, + { + "id": "refuse-total_seconds-bool", + "outcome": "pass" + }, + { + "id": "refuse-total_seconds-string", + "outcome": "pass" + }, + { + "id": "refuse-total_seconds-nan", + "outcome": "pass" + }, + { + "id": "refuse-total_seconds-positive-infinity", + "outcome": "pass" + }, + { + "id": "refuse-total_seconds-negative-infinity", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-float-io_seconds", + "outcome": "pass" + }, + { + "id": "refuse-io_seconds-bool", + "outcome": "pass" + }, + { + "id": "refuse-io_seconds-string", + "outcome": "pass" + }, + { + "id": "refuse-io_seconds-nan", + "outcome": "pass" + }, + { + "id": "refuse-io_seconds-positive-infinity", + "outcome": "pass" + }, + { + "id": "refuse-io_seconds-negative-infinity", + "outcome": "pass" + }, + { + "id": "old-accept-new-refuse-float-retries", + "outcome": "pass" + }, + { + "id": "refuse-retries-bool", + "outcome": "pass" + }, + { + "id": "refuse-retries-string", + "outcome": "pass" + }, + { + "id": "refuse-retries-nan", + "outcome": "pass" + }, + { + "id": "refuse-retries-positive-infinity", + "outcome": "pass" + }, + { + "id": "refuse-retries-negative-infinity", + "outcome": "pass" + }, + { + "id": "refuse-cpu-schema-[]", + "outcome": "pass" + }, + { + "id": "refuse-cpu-schema-[True]", + "outcome": "pass" + }, + { + "id": "refuse-cpu-schema-[0, 0]", + "outcome": "pass" + }, + { + "id": "refuse-cpu-schema-[0.0]", + "outcome": "pass" + }, + { + "id": "refuse-cpu-schema-['0']", + "outcome": "pass" + }, + { + "id": "refuse-cpu-schema-[0, 1, 2]", + "outcome": "pass" + }, + { + "id": "nonfinite-at-unexpected-depth-refuses", + "outcome": "pass" + }, + { + "id": "invalid-utf8-refuses", + "outcome": "pass" + }, + { + "id": "sha-check-preserved", + "outcome": "pass" + }, + { + "id": "predecessor-tree-normal", + "outcome": "pass" + }, + { + "id": "successor-tree-normal", + "outcome": "pass" + }, + { + "id": "predecessor-tree-normal-unknown", + "outcome": "pass" + }, + { + "id": "successor-tree-normal-unknown", + "outcome": "pass" + }, + { + "id": "predecessor-tree-after-root", + "outcome": "pass" + }, + { + "id": "successor-tree-after-root", + "outcome": "pass" + }, + { + "id": "predecessor-tree-after-wheelhouse", + "outcome": "pass" + }, + { + "id": "successor-tree-after-wheelhouse", + "outcome": "pass" + }, + { + "id": "predecessor-tree-after-root-unknown", + "outcome": "pass" + }, + { + "id": "successor-tree-after-root-unknown", + "outcome": "pass" + } + ], + "scope": "guardian authority/root-tree predecessor countercontrols only; no admitted data API", + "selected_archives_acquired": 0, + "source_sha256": "032a2da74a26ea8ed036428d5470420dd49f099b7fe61cfcf4d7ab216d720066" +} diff --git a/qualification/issue-184/tool-acquisition/184-acquisition-guardian-v7-controls.py b/qualification/issue-184/tool-acquisition/184-acquisition-guardian-v7-controls.py new file mode 100644 index 0000000..53265eb --- /dev/null +++ b/qualification/issue-184/tool-acquisition/184-acquisition-guardian-v7-controls.py @@ -0,0 +1,417 @@ +"""Narrow predecessor countercontrols; no data acquisition or admitted start.""" +from __future__ import annotations + +import copy +import ast +import hashlib +import importlib.util +import json +import os +from pathlib import Path +import socket +import sys +import tempfile +from unittest.mock import patch + +sys.dont_write_bytecode = True +BASE = Path('/tmp/sdk-python-handoff') +REF = 'https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-9999999999' +results = [] + + +def load(name, version): + path = BASE / f'184-acquisition-custody-guardian-{version}-proposal.py' + raw = path.read_bytes() + if version == 'v2': + assert hashlib.sha256(raw).hexdigest() == 'afc5f198de12d4b26520957bc563ff0122f8535838a1cbc64c40272c5f3f7f70' + spec = importlib.util.spec_from_file_location(name, path) + module = importlib.util.module_from_spec(spec); spec.loader.exec_module(module) + return module + + +old = load('trusted_frozen_v2_countercontrol', 'v2') +new = load('trusted_v7_correction_control', 'v7') +ledger_predecessor = load('trusted_frozen_v6_ledger_countercontrol', 'v6') +assert hashlib.sha256((BASE/'184-acquisition-custody-guardian-v6-proposal.py').read_bytes()).hexdigest() == 'cbb1672855a0dec48c7d952f02be49ab3c0935d9bd7b1cf467638d73fc543e4c' +callback_predecessor = load('trusted_frozen_v5_callback_countercontrol', 'v5') +assert hashlib.sha256((BASE/'184-acquisition-custody-guardian-v5-proposal.py').read_bytes()).hexdigest() == '4c92d889d89dbef2502ef5e9bc923d00c0314821b42aa77d4c0ee92080aba41e' +archive_predecessor = load('trusted_frozen_v4_archive_countercontrol', 'v4') +assert hashlib.sha256((BASE/'184-acquisition-custody-guardian-v4-proposal.py').read_bytes()).hexdigest() == 'd52bb389127cb81b5b0303b3a593ee1af74f8c4835793d67f08d0bf182e2101c' +linked_predecessor = load('trusted_frozen_v3_link_countercontrol', 'v3') +assert hashlib.sha256((BASE/'184-acquisition-custody-guardian-v3-proposal.py').read_bytes()).hexdigest() == '79ddc82b0095872885aceb943cbf9daa1eb3a05048bc08f9ee9cd093511de187' +inputs = {key: 'a' * 64 for key in new.INPUT_FIELDS} +authority = dict(decision=REF, selected_source=new.SELECTED_SOURCE, inputs=inputs, + environment=new.MINIMAL_ENV, host='lab4', total_seconds=300, io_seconds=270, + retries=0, survivor_policy='retain-custody-and-escalate', + pool_parent='/home/lab4/fleet/tmp', launcher_sha256='b' * 64, cpu_affinity=[0, 1], + retention_deadline_utc='2099-01-01T00:00:00Z') + + +def bound(module, raw): + return module.bind_authority(raw, hashlib.sha256(raw).hexdigest(), REF, inputs) + + +def parser_refuses(raw, predecessor=True): + if predecessor: + assert bound(old, raw)['decision'] == REF, 'predecessor acceptance not reproduced' + try: + bound(new, raw) + except (ValueError, UnicodeError): + return + raise AssertionError('ambiguous authority accepted') + + +def sha_refuses(): + try: new.bind_authority(encoded(authority), '0' * 64, REF, inputs) + except new.Refusal as error: assert str(error) == 'AUTHORITY_BYTES_CHANGED' + else: raise AssertionError('changed authority SHA accepted') + + +def encoded(value): + return json.dumps(value, separators=(',', ':')).encode() + + +def mutate(key, value): + row = copy.deepcopy(authority); row[key] = value + return encoded(row) + + +def allocation(module, stage, unknown=False): + with tempfile.TemporaryDirectory(prefix='sdk-python-184-guardian-tree-controls-') as temp: + parent = Path(temp); leaf = 'sdk-python-184-tools-guardian-source-control' + root = parent / leaf + custody = None + real_mkdir = os.mkdir + def mkdir(path, mode=0o777, *, dir_fd=None): + if path == 'wheelhouse' and stage == 'after-root': + if unknown: (root / 'unknown-root-member').write_bytes(b'untouched') + raise OSError('synthetic second mkdir refusal') + return real_mkdir(path, mode, dir_fd=dir_fd) + real_open = os.open + def opened(path, flags, mode=0o777, *, dir_fd=None): + if path == 'one.whl' and stage == 'after-wheelhouse': + raise OSError('synthetic first archive refusal') + return real_open(path, flags, mode, dir_fd=dir_fd) + try: + with patch.object(module.os, 'mkdir', side_effect=mkdir), patch.object(module.os, 'open', side_effect=opened): + try: custody = module.ArchiveCustody(parent, leaf, ['one.whl']) + except module.Refusal as error: + if module is new: + assert isinstance(error, new.AllocationRefusal) + custody = error.custody + assert error.cleanup['complete'] is (not unknown) + assert error.cleanup['outer_owner_custody_required'] is unknown + if unknown: + assert root.exists() and (root / 'unknown-root-member').read_bytes() == b'untouched' + assert os.fstat(custody.parent_fd).st_uid == os.geteuid() + else: assert not root.exists() + else: raise + except OSError: + assert module is old and root.exists(), 'predecessor residue not reproduced' + else: + assert stage == 'normal' + if unknown: (root / 'unknown-root-member').write_bytes(b'untouched') + result = custody.cleanup() + if module is old: + assert result['complete'] and root.exists() and (root / 'wheelhouse').exists() + elif unknown: + assert not result['complete'] and result['outer_owner_custody_required'] + assert (root / 'unknown-root-member').read_bytes() == b'untouched' + assert not (root / 'wheelhouse').exists() + else: + assert result['complete'] and not result['outer_owner_custody_required'] + assert not root.exists(), 'TemporaryDirectory must not conceal retained directories' + finally: + if custody is not None: custody.close() + + +def removed_root_with_tracked_nodes(module, scenario): + with tempfile.TemporaryDirectory(prefix='sdk-python-184-linked-descendant-control-') as temp: + parent=Path(temp);custody=module.ArchiveCustody(parent, + 'sdk-python-184-tools-guardian-linked-control',['one.whl']) + extra_fd=None + try: + archive=custody.path/'wheelhouse'/'one.whl';archive.write_bytes(b'original synthetic bytes') + if scenario=='wheelhouse': + (custody.path/'wheelhouse').rename(parent/'moved-wheelhouse') + retained=parent/'moved-wheelhouse'/'one.whl' + else: + if scenario=='archive': + archive.rename(parent/'moved-archive.whl');retained=parent/'moved-archive.whl' + else:archive.unlink();retained=None + (custody.path/'wheelhouse').rmdir() + if scenario=='extra-directory': + (custody.path/'metadata').mkdir() + extra_fd=os.open(custody.path/'metadata',os.O_RDONLY|os.O_DIRECTORY) + st=os.fstat(extra_fd) + custody.dir_ids={'metadata':[st.st_dev,st.st_ino]} + custody.directory_fds={'metadata':extra_fd} + (custody.path/'metadata').rename(parent/'moved-metadata') + custody.path.rmdir() + assert os.fstat(custody.root_fd).st_nlink==0 + result=custody.cleanup() + if scenario=='fully-unlinked': + assert module is new and result['complete'] and result['all_unlinked'] + assert not result['outer_owner_custody_required'] + assert not result['retained_or_unproven_inodes'] + elif module is linked_predecessor: + assert result['complete'] and not result['outer_owner_custody_required'], 'old false completion not reproduced' + else: + assert not result['complete'] and result['outer_owner_custody_required'] + assert result['refusal_code']=='TRACKED_INODES_STILL_LINKED_OR_UNPROVEN' + expected='wheelhouse' if scenario=='wheelhouse' else 'archive:one.whl' if scenario=='archive' else 'metadata' + assert expected in result['retained_or_unproven_inodes'] + if retained is not None:assert retained.read_bytes()==b'original synthetic bytes' + if scenario=='extra-directory':assert (parent/'moved-metadata').is_dir() + finally: + if extra_fd is not None:os.close(extra_fd) + custody.close() + + +def allocation_identity_gap(module): + with tempfile.TemporaryDirectory(prefix='sdk-python-184-allocation-identity-control-') as temp: + parent=Path(temp);root=parent/'sdk-python-184-tools-guardian-identity-control' + real_stat=os.stat;faulted=False;held=None + def stat_before_identity(path,*args,**kwargs): + nonlocal faulted + if path=='wheelhouse' and not faulted: + faulted=True + (root/'wheelhouse').rename(parent/'moved-before-identity');root.rmdir() + raise OSError('synthetic identity read refusal') + return real_stat(path,*args,**kwargs) + try: + with patch.object(module.os,'stat',side_effect=stat_before_identity): + try:module.ArchiveCustody(parent,root.name,['one.whl']) + except module.AllocationRefusal as error:held=error.custody;result=error.cleanup + else:raise AssertionError('allocation identity fault absent') + assert faulted and (parent/'moved-before-identity').is_dir() + if module is linked_predecessor: + assert result['complete'] and not result['outer_owner_custody_required'] + else: + assert not result['complete'] and result['outer_owner_custody_required'] + assert result['refusal_code']=='TRACKED_INODES_STILL_LINKED_OR_UNPROVEN' + assert 'wheelhouse' in result['retained_or_unproven_inodes'] + finally: + if held is not None:held.close() + + +def archive_before_identity(module,fully_unlinked=False,remove_root=True): + with tempfile.TemporaryDirectory(prefix='sdk-python-184-raw-archive-control-') as temp: + parent=Path(temp);root=parent/'sdk-python-184-tools-guardian-raw-archive-control' + opened=[];real_open=os.open;real_fstat=os.fstat;held=None;faulted=False + moved=parent/'moved-before-archive-identity.whl' + def exclusive_open(path,*args,**kwargs): + fd=real_open(path,*args,**kwargs) + if path=='one.whl':opened.append(fd) + return fd + def identity_read(fd): + nonlocal faulted + if fd in opened and not faulted: + faulted=True + os.write(fd,b'synthetic allocated bytes') + (root/'wheelhouse'/'one.whl').rename(moved) + if fully_unlinked:moved.unlink() + if remove_root:(root/'wheelhouse').rmdir();root.rmdir() + raise OSError('synthetic archive identity refusal') + return real_fstat(fd) + try: + with patch.object(module.os,'open',side_effect=exclusive_open),patch.object(module.os,'fstat',side_effect=identity_read): + try:module.ArchiveCustody(parent,root.name,['one.whl']) + except module.AllocationRefusal as error:held=error.custody;result=error.cleanup + else:raise AssertionError('archive identity fault absent') + assert faulted and len(opened)==1 + if module is archive_predecessor: + assert not held.archive_nodes and result['complete'] and not result['outer_owner_custody_required'] + assert moved.read_bytes()==b'synthetic allocated bytes' and moved.stat().st_nlink==1 + else: + assert held.archive_nodes['one.whl']=={'fd':opened[0],'identity':None} + if fully_unlinked: + assert result['complete'] and result['all_unlinked'] and not result['outer_owner_custody_required'] + assert real_fstat(opened[0]).st_nlink==0 and not root.exists() + else: + assert not result['complete'] and result['outer_owner_custody_required'] + assert result['refusal_code']=='TRACKED_INODES_STILL_LINKED_OR_UNPROVEN' + assert 'archive:one.whl' in result['retained_or_unproven_inodes'] + assert moved.read_bytes()==b'synthetic allocated bytes' and real_fstat(opened[0]).st_nlink==1 + finally: + if held is not None:held.close() + + +def callback_registration_failure(module,interruption,stage='archive'): + with tempfile.TemporaryDirectory(prefix='sdk-python-184-callback-close-control-') as temp: + parent=Path(temp);root=parent/'sdk-python-184-tools-guardian-callback-control' + opened=[];real_open=os.open;real_callback=module.ExitStack.callback;held=None + def allocated_open(path,*args,**kwargs): + fd=real_open(path,*args,**kwargs) + target=(path=='one.whl' if stage=='archive' else path==root.name if stage=='root' + else path=='wheelhouse' if stage=='wheelhouse' else path==parent) + if target:opened.append(fd) + return fd + def callback(stack,function,*args,**kwargs): + if args and args[0] in opened:raise interruption('synthetic callback registration failure') + return real_callback(stack,function,*args,**kwargs) + try: + with patch.object(module.os,'open',side_effect=allocated_open),patch.object(module.ExitStack,'callback',new=callback): + try:module.ArchiveCustody(parent,root.name,['one.whl']) + except module.AllocationRefusal as error:held=error.custody;result=error.cleanup + else:raise AssertionError('callback registration fault absent') + assert held is not None and len(opened)==1 + fd=opened[0] + if stage=='archive': + assert not result['complete'] and result['outer_owner_custody_required'] + assert held.archive_nodes['one.whl']=={'fd':fd,'identity':None} + assert (root/'wheelhouse'/'one.whl').exists() + held.close() + if module is callback_predecessor: + assert os.fstat(fd).st_nlink==1, 'predecessor callback leak not reproduced' + os.close(fd) + else: + try:os.fstat(fd) + except OSError as error:assert error.errno==9 + else:raise AssertionError('raw descriptor survived custody.close') + # A second close must not close a new unrelated fd at that number. + unrelated=os.open(parent/'unrelated',os.O_RDWR|os.O_CREAT|os.O_EXCL,0o600) + reused=os.dup2(unrelated,fd) + try: + (parent/'unrelated').unlink() + held.close();assert os.fstat(reused).st_nlink==0 + if stage=='archive': + assert (root/'wheelhouse'/'one.whl').exists() + assert not result['complete'] and result['outer_owner_custody_required'] + assert 'archive:one.whl' in held.unlink_state()['retained_or_unproven_inodes'] + finally: + os.close(reused) + if unrelated!=reused:os.close(unrelated) + finally: + if held is not None:held.close() + + +def check(name, operation): + try: operation() + except BaseException as error: + results.append({'id': name, 'outcome': 'fail', 'type': type(error).__name__, 'detail': str(error)[:250]}) + else: results.append({'id': name, 'outcome': 'pass'}) + + +def archive_before_ledger(module,interruption,removed): + raw=Path(module.__file__).read_text();tree=ast.parse(raw) + insertion=next(node for node in ast.walk(tree) if isinstance(node,ast.Assign) + and any(isinstance(target,ast.Subscript) and isinstance(target.value,ast.Attribute) + and target.value.attr=='archive_nodes' for target in node.targets)) + with tempfile.TemporaryDirectory(prefix='sdk-python-184-archive-pre-ledger-') as temporary: + parent=Path(temporary);root=parent/'sdk-python-184-tools-guardian-pre-ledger' + held=None;opened=[];triggered=[] + def interrupt(frame,event,arg): + if event=='line' and frame.f_code.co_filename==module.__file__ and frame.f_lineno==insertion.lineno: + fd=frame.f_locals['fd'];opened.append(fd);triggered.append(True) + if removed: + (root/'wheelhouse'/'one.whl').rename(parent/'moved-one.whl') + (root/'wheelhouse').rmdir();root.rmdir() + raise interruption('synthetic immediate pre-ledger interruption') + return interrupt + try: + sys.settrace(interrupt) + try:module.ArchiveCustody(parent,root.name,['one.whl']) + except module.AllocationRefusal as error:held=error.custody;cleanup=error.cleanup + finally:sys.settrace(None) + assert triggered==[True] and len(opened)==1 + if module is ledger_predecessor: + assert os.fstat(opened[0]).st_nlink==1, 'predecessor live raw FD not reproduced' + held.close() + assert os.fstat(opened[0]).st_nlink==1, 'predecessor untracked FD unexpectedly closed' + if removed:assert cleanup['complete'] and (parent/'moved-one.whl').exists() + os.close(opened[0]);opened.clear() + else: + try:os.fstat(opened[0]) + except OSError as error:assert error.errno==9 + else:raise AssertionError('successor raw FD remains open') + opened.clear() + assert not cleanup['complete'] and cleanup['outer_owner_custody_required'] + assert 'archive:one.whl:allocation_unproven' in held.unlink_state()['retained_or_unproven_inodes'] + if removed:assert (parent/'moved-one.whl').exists() + else:assert (root/'wheelhouse'/'one.whl').exists() + held.close();held.close() + finally: + sys.settrace(None) + if held is not None:held.close() + for fd in opened: + try:os.close(fd) + except OSError:pass + + +with patch.object(socket, 'create_connection', side_effect=AssertionError('NO_NETWORK_IN_SOURCE_CONTROLS')): + for interruption in (MemoryError,KeyboardInterrupt): + for removed in (False,True): + for module,label in ((ledger_predecessor,'v6-leak'),(new,'v7-close-retain-unknown')): + check(label+'-archive-before-ledger-'+interruption.__name__+('-root-removed' if removed else '-root-present'), + lambda module=module,interruption=interruption,removed=removed:archive_before_ledger(module,interruption,removed)) + for interruption in (MemoryError,KeyboardInterrupt): + for module,label in ((callback_predecessor,'v5-raw-fd-leak'),(new,'v6-raw-fd-close-idempotent')): + check(label+'-'+interruption.__name__,lambda module=module,interruption=interruption:callback_registration_failure(module,interruption)) + for stage in ('parent','root','wheelhouse'): + check('v6-'+stage+'-callback-failure-closes-known-fd',lambda stage=stage:callback_registration_failure(new,MemoryError,stage)) + check('v4-false-complete-opened-archive-before-identity',lambda:archive_before_identity(archive_predecessor)) + check('v5-retains-opened-archive-before-identity',lambda:archive_before_identity(new)) + check('v5-fully-unlinked-partial-archive-root-absent-positive',lambda:archive_before_identity(new,True,True)) + check('v5-fully-unlinked-partial-archive-root-present-positive',lambda:archive_before_identity(new,True,False)) + check('predecessor-false-complete-allocation-before-child-identity',lambda:allocation_identity_gap(linked_predecessor)) + check('successor-retains-allocation-before-child-identity',lambda:allocation_identity_gap(new)) + for scenario in ('wheelhouse','archive','extra-directory'): + for module,label in ((linked_predecessor,'predecessor-false-complete'),(new,'successor-retains')): + check(label+'-removed-root-linked-'+scenario, + lambda module=module,scenario=scenario:removed_root_with_tracked_nodes(module,scenario)) + check('fully-unlinked-root-children-and-archives-completes',lambda:removed_root_with_tracked_nodes(new,'fully-unlinked')) + check('exact-schema-positive', lambda: bound(new, encoded(authority))) + for name, prefix in [('identical', '"decision":"' + REF + '",'), + ('conflicting', '"decision":"earlier-conflict",'), + ('escaped', '"decis\\u0069on":"earlier-conflict",')]: + check('old-accept-new-refuse-duplicate-' + name, + lambda prefix=prefix: parser_refuses(b'{' + prefix.encode() + encoded(authority)[1:])) + row = copy.deepcopy(authority); row['unreviewed_future_semantics'] = {} + check('old-accept-new-refuse-extra-top-field', lambda: parser_refuses(encoded(row))) + for key in ('survivor_policy', 'pool_parent', 'launcher_sha256', 'cpu_affinity', 'retention_deadline_utc'): + missing = copy.deepcopy(authority); del missing[key] + check('old-accept-new-refuse-missing-' + key, lambda missing=missing: parser_refuses(encoded(missing))) + for key, nested in [('inputs', inputs), ('environment', new.MINIMAL_ENV)]: + first = next(iter(nested)); duplicated = encoded(authority).replace( + ('"' + key + '":{').encode(), ('"' + key + '":{"' + first + '":"conflict",').encode(), 1) + check('old-accept-new-refuse-nested-duplicate-' + key, lambda raw=duplicated: parser_refuses(raw)) + for extra in (True, False): + value = dict(nested) + if extra: value['unknown'] = 'x' + else: del value[first] + check('refuse-nested-schema-' + key + ('-extra' if extra else '-missing'), + lambda key=key, value=value: parser_refuses(mutate(key, value), predecessor=False)) + for key in ('total_seconds', 'io_seconds', 'retries'): + check('old-accept-new-refuse-float-' + key, lambda key=key: parser_refuses(mutate(key, float(authority[key])))) + for kind, value in [('bool', False), ('string', str(authority[key])), ('nan', float('nan')), + ('positive-infinity', float('inf')), ('negative-infinity', -float('inf'))]: + check('refuse-' + key + '-' + kind, + lambda key=key, value=value: parser_refuses(mutate(key, value), predecessor=(key == 'retries' and value is False))) + for value in ([], [True], [0, 0], [0.0], ['0'], [0, 1, 2]): + check('refuse-cpu-schema-' + repr(value), lambda value=value: parser_refuses(mutate('cpu_affinity', value))) + check('nonfinite-at-unexpected-depth-refuses', lambda: parser_refuses( + encoded(authority).replace(b'"cpu_affinity":[0,1]', b'"cpu_affinity":[NaN]'))) + check('invalid-utf8-refuses', lambda: parser_refuses(b'\xff', predecessor=False)) + check('sha-check-preserved', sha_refuses) + for stage, unknown in [('normal', False), ('normal', True), ('after-root', False), + ('after-wheelhouse', False), ('after-root', True)]: + for module, label in ((old, 'predecessor'), (new, 'successor')): + check(label + '-tree-' + stage + ('-unknown' if unknown else ''), + lambda module=module, stage=stage, unknown=unknown: allocation(module, stage, unknown)) + +report = {'scope': 'guardian authority/root-tree predecessor countercontrols only; no admitted data API', + 'source_sha256': hashlib.sha256((BASE / '184-acquisition-custody-guardian-v7-proposal.py').read_bytes()).hexdigest(), + 'predecessor_sha256': 'afc5f198de12d4b26520957bc563ff0122f8535838a1cbc64c40272c5f3f7f70', + 'cases': len(results), 'passed': sum(row['outcome'] == 'pass' for row in results), + 'failed': sum(row['outcome'] == 'fail' for row in results), 'results': results, + 'real_network_requests': 0, 'selected_archives_acquired': 0, 'new_general_decision': None} +raw = (json.dumps(report, sort_keys=True, indent=2) + '\n').encode() +path = BASE / '184-acquisition-guardian-v7-controls-result.json' +if path.exists(): + if path.read_bytes() != raw: + print(json.dumps(report, indent=2)); raise SystemExit('CONTROL_RESULT_COLLISION; prior result retained') +else: + fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o400) + with os.fdopen(fd, 'wb') as stream: assert stream.write(raw) == len(raw) +print(json.dumps(report, indent=2)); raise SystemExit(bool(report['failed'])) diff --git a/qualification/issue-184/tool-acquisition/184-acquisition-interpreter-authority-proposal.json b/qualification/issue-184/tool-acquisition/184-acquisition-interpreter-authority-proposal.json new file mode 100644 index 0000000..eef4c5c --- /dev/null +++ b/qualification/issue-184/tool-acquisition/184-acquisition-interpreter-authority-proposal.json @@ -0,0 +1,32 @@ +{ + "architecture": "x86_64", + "binary_and_library": [ + { + "bytes": 7477160, + "gid": 0, + "mode": "0o755", + "path": "/usr/bin/python3.14", + "resolved": "/usr/bin/python3.14", + "sha256": "52e0a13e60a981d8c4b6478be2ba5176f69da07948a056bf49cf6f077e30cb41", + "uid": 0 + }, + { + "bytes": 8247200, + "gid": 0, + "mode": "0o644", + "path": "/usr/lib/x86_64-linux-gnu/libpython3.14.so", + "resolved": "/usr/lib/x86_64-linux-gnu/libpython3.14.so.1.0", + "sha256": "982cb3768d1d8b00126255ee2785bccf8360c68b197fb3dec95858ab56195f87", + "uid": 0 + } + ], + "implementation": "CPython", + "launch_invocation": null, + "new_general_decision": null, + "real_executable": "/usr/bin/python3.14", + "record": "read-only interpreter authority proposal; no data invocation", + "requested_executable": "/usr/bin/python3", + "stdlib": "/usr/lib/python3.14", + "symlink_target": "python3.14", + "version": "3.14.4 (main, Aug 20 2026, 10:41:58) [GCC 15.2.0]" +} diff --git a/qualification/issue-184/tool-acquisition/184-acquisition-launcher-bootstrap-controls-result.json b/qualification/issue-184/tool-acquisition/184-acquisition-launcher-bootstrap-controls-result.json new file mode 100644 index 0000000..e9dd499 --- /dev/null +++ b/qualification/issue-184/tool-acquisition/184-acquisition-launcher-bootstrap-controls-result.json @@ -0,0 +1,121 @@ +{ + "cases": 27, + "downloaded_wheel_imports_or_installs": 0, + "failed": 0, + "new_general_decision": null, + "passed": 27, + "real_network_requests": 0, + "results": [ + { + "id": "bootstrap-exact-authority-schema-positive", + "outcome": "pass" + }, + { + "id": "bootstrap-exact-authority-schema-extra", + "outcome": "pass" + }, + { + "id": "bootstrap-exact-authority-schema-missing", + "outcome": "pass" + }, + { + "id": "bootstrap-exact-authority-schema-nested-extra", + "outcome": "pass" + }, + { + "id": "bootstrap-exact-authority-schema-nested-missing", + "outcome": "pass" + }, + { + "id": "bootstrap-exact-authority-schema-environment-extra", + "outcome": "pass" + }, + { + "id": "bootstrap-exact-authority-schema-limit-float", + "outcome": "pass" + }, + { + "id": "bootstrap-exact-authority-schema-limit-bool", + "outcome": "pass" + }, + { + "id": "bootstrap-exact-authority-schema-limit-string", + "outcome": "pass" + }, + { + "id": "bootstrap-exact-authority-schema-limit-nonfinite", + "outcome": "pass" + }, + { + "id": "bootstrap-exact-authority-schema-duplicate", + "outcome": "pass" + }, + { + "id": "bootstrap-exact-authority-schema-escaped-duplicate", + "outcome": "pass" + }, + { + "id": "bootstrap-exact-authority-schema-nested-duplicate", + "outcome": "pass" + }, + { + "id": "verified-inline-source-positive", + "outcome": "pass" + }, + { + "id": "verified-inline-source-hash", + "outcome": "pass" + }, + { + "id": "verified-inline-source-bound", + "outcome": "pass" + }, + { + "id": "verified-inline-source-hardlink", + "outcome": "pass" + }, + { + "id": "verified-inline-source-symlink", + "outcome": "pass" + }, + { + "id": "ambient-credentials-or-wrong-startup-refuse-before-inputs", + "outcome": "pass" + }, + { + "id": "affinity-empty-refuses-before-mutation", + "outcome": "pass" + }, + { + "id": "affinity-too-many-refuses-before-mutation", + "outcome": "pass" + }, + { + "id": "affinity-bool-refuses-before-mutation", + "outcome": "pass" + }, + { + "id": "affinity-duplicate-refuses-before-mutation", + "outcome": "pass" + }, + { + "id": "affinity-unavailable-refuses-before-mutation", + "outcome": "pass" + }, + { + "id": "duplicate-authority-key-refused", + "outcome": "pass" + }, + { + "id": "nonterminal-session-held-until-quiescent-late-nonpass", + "outcome": "pass" + }, + { + "id": "unknown-custody-is-not-returned-or-discarded", + "outcome": "pass" + } + ], + "scope": "bootstrap file/refusal controls; no admitted start or selected archive access", + "selected_archives_acquired": 0, + "source_sha256": "0cd9d5309b1dd3314e5a7f288f7462c784e2bcf4cea9618eeff4aa401f516aaf" +} diff --git a/qualification/issue-184/tool-acquisition/184-acquisition-launcher-bootstrap-controls.py b/qualification/issue-184/tool-acquisition/184-acquisition-launcher-bootstrap-controls.py new file mode 100644 index 0000000..7b41aa8 --- /dev/null +++ b/qualification/issue-184/tool-acquisition/184-acquisition-launcher-bootstrap-controls.py @@ -0,0 +1,145 @@ +"""Local bootstrap refusal/resource controls. No acquisition or admitted start.""" +from __future__ import annotations + +import hashlib +import io +from contextlib import redirect_stdout +import importlib.util +import json +import os +from pathlib import Path +import socket +import sys +import tempfile +from unittest.mock import patch + +sys.dont_write_bytecode=True +BASE=Path('/tmp/sdk-python-handoff') +SOURCE=BASE/'184-acquisition-launcher-bootstrap-proposal.py' +spec=importlib.util.spec_from_file_location('trusted_launcher_bootstrap_control',SOURCE) +b=importlib.util.module_from_spec(spec);spec.loader.exec_module(b) +REF='https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-9999999999' +results=[] + + +def refuses(operation,code): + try:operation() + except ValueError as error:assert str(error)==code,(str(error),code) + else:raise AssertionError('required refusal absent: '+code) + + +def read_control(mode): + with tempfile.TemporaryDirectory(prefix='sdk-python-184-bootstrap-controls-') as temporary: + p=Path(temporary)/'source';raw=b'trusted synthetic source bytes';p.write_bytes(raw) + expected=hashlib.sha256(raw).hexdigest() + if mode=='positive':assert b.read_verified(p,expected,1024)==raw + elif mode=='hash':refuses(lambda:b.read_verified(p,'0'*64,1024),'BOOTSTRAP_INPUT_BYTES_CHANGED') + elif mode=='bound':refuses(lambda:b.read_verified(p,expected,1),'BOOTSTRAP_INPUT_CUSTODY_CHANGED') + elif mode=='hardlink': + os.link(p,Path(temporary)/'shared');refuses(lambda:b.read_verified(p,expected,1024),'BOOTSTRAP_INPUT_CUSTODY_CHANGED') + elif mode=='symlink': + link=Path(temporary)/'link';link.symlink_to(p) + try:b.read_verified(link,expected,1024) + except OSError:pass + else:raise AssertionError('symlink accepted') + + +def env_gate(): + with patch.dict(os.environ,{'GH_TOKEN':'synthetic'},clear=False), \ + patch.object(b,'read_verified',side_effect=AssertionError('must refuse before reading authority')): + refuses(lambda:b.launch('/synthetic','0'*64,REF,'0'*64), + 'ENV_I_AND_ISOLATED_NO_SITE_REQUIRED_BEFORE_STARTUP') + + +def affinity_refusal(value): + with patch.object(b.os,'sched_setaffinity',side_effect=AssertionError('must refuse before mutation')): + refuses(lambda:b.configure_resources(value),'EXACT_AVAILABLE_CPU_AFFINITY_REQUIRED') + + +def report_keeps_nonterminal_session(): + class Session: + finished=False;pid=123;owner_pid=456;expected_pool='/synthetic/owned-pool' + def reconcile_retained(self): + self.finished=True + return {'result':{'quiescent_after_failed_bound':True,'terminal_cleanup_complete':False,'acquisition_accepted':False}} + session=Session();output=io.StringIO() + with patch.object(b,'launch',return_value=(session,{'result':{'status':'SURVIVOR_CUSTODY_REQUIRED'}})),redirect_stdout(output): + result=b.run_and_report('/synthetic','0'*64,None,'0'*64) + phases=[json.loads(line)['phase'] for line in output.getvalue().splitlines()] + assert phases==['RESULT','CUSTODY_REQUIRED','POST_BOUND_QUIESCENT_NONPASS'] + assert session.finished and result['result']['acquisition_accepted'] is False + + +def report_never_returns_unresolved_unknown(): + class Session: + finished=False;pid=123;owner_pid=None;expected_pool='/synthetic/owned-pool' + def reconcile_retained(self):return None + class BoundedControlStop(Exception):pass + session=Session() + with patch.object(b,'launch',return_value=(session,{'result':{'status':'OUTER_CUSTODY_REQUIRED'}})), \ + patch.object(b.time,'sleep',side_effect=BoundedControlStop),redirect_stdout(io.StringIO()): + try:b.run_and_report('/synthetic','0'*64,None,'0'*64) + except BoundedControlStop:pass + else:raise AssertionError('unresolved custody returned to a disposable caller') + assert not session.finished + + +def authority_shape_control(mode): + row=json.loads((BASE/'authority-template.json').read_bytes()) + row['decision']=REF;row['cpu_affinity']=[0] + row['retention_deadline_utc']='2099-01-01T00:00:00Z' + raw=json.dumps(row,separators=(',',':')).encode() + if mode=='positive':assert b.parse_authority(raw)==row;return + if mode=='extra':row['unreviewed_future_semantics']={} + elif mode=='missing':del row['io_seconds'] + elif mode=='nested-extra':row['inputs']['unreviewed']='0'*64 + elif mode=='nested-missing':del row['inputs']['ca'] + elif mode=='environment-extra':row['environment']['GH_TOKEN']='synthetic' + elif mode.startswith('limit-'): + row['retries']={'float':0.0,'bool':False,'string':'0','nonfinite':float('nan')}[mode[6:]] + elif mode in {'duplicate','escaped-duplicate'}: + key='decision' if mode=='duplicate' else 'decis\\u0069on' + raw=('{"'+key+'":"earlier",').encode()+raw[1:] + elif mode=='nested-duplicate':raw=raw.replace(b'"inputs":{',b'"inputs":{"ca":"earlier",',1) + if mode not in {'duplicate','escaped-duplicate','nested-duplicate'}:raw=json.dumps(row).encode() + try:b.parse_authority(raw) + except ValueError:pass + else:raise AssertionError('bootstrap ambiguous authority accepted: '+mode) + + +def check(name,operation): + try:operation() + except BaseException as error:results.append({'id':name,'outcome':'fail','type':type(error).__name__,'detail':str(error)[:160]}) + else:results.append({'id':name,'outcome':'pass'}) + + +with patch.object(socket,'create_connection',side_effect=AssertionError('NO_NETWORK_IN_BOOTSTRAP_CONTROLS')): + for mode in ('positive','extra','missing','nested-extra','nested-missing','environment-extra', + 'limit-float','limit-bool','limit-string','limit-nonfinite', + 'duplicate','escaped-duplicate','nested-duplicate'): + check('bootstrap-exact-authority-schema-'+mode,lambda mode=mode:authority_shape_control(mode)) + for mode in ('positive','hash','bound','hardlink','symlink'): + check('verified-inline-source-'+mode,lambda mode=mode:read_control(mode)) + check('ambient-credentials-or-wrong-startup-refuse-before-inputs',env_gate) + for name,value in [('empty',[]),('too-many',[0,1,2]),('bool',[True]),('duplicate',[0,0]),('unavailable',[max(os.sched_getaffinity(0))+1])]: + check('affinity-'+name+'-refuses-before-mutation',lambda value=value:affinity_refusal(value)) + check('duplicate-authority-key-refused',lambda:refuses(lambda:b.pairs([('decision','one'),('decision','two')]),'DUPLICATE_AUTHORITY_KEY')) + check('nonterminal-session-held-until-quiescent-late-nonpass',report_keeps_nonterminal_session) + check('unknown-custody-is-not-returned-or-discarded',report_never_returns_unresolved_unknown) + +report={'scope':'bootstrap file/refusal controls; no admitted start or selected archive access', + 'source_sha256':hashlib.sha256(SOURCE.read_bytes()).hexdigest(),'cases':len(results), + 'passed':sum(row['outcome']=='pass' for row in results),'failed':sum(row['outcome']=='fail' for row in results), + 'results':results,'real_network_requests':0,'selected_archives_acquired':0, + 'downloaded_wheel_imports_or_installs':0,'new_general_decision':None} +report_path=BASE/'184-acquisition-launcher-bootstrap-controls-result.json' +report_bytes=(json.dumps(report,sort_keys=True,indent=2)+'\n').encode() +if report_path.exists(): + if report_path.read_bytes()!=report_bytes: + print(json.dumps(report,indent=2)) + raise SystemExit('CONTROL_RESULT_COLLISION; prior record preserved') +else: + descriptor=os.open(report_path,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o400) + with os.fdopen(descriptor,'wb') as stream: + assert stream.write(report_bytes)==len(report_bytes) +print(json.dumps(report,indent=2));raise SystemExit(bool(report['failed'])) diff --git a/qualification/issue-184/tool-acquisition/184-acquisition-launcher-bootstrap-proposal.py b/qualification/issue-184/tool-acquisition/184-acquisition-launcher-bootstrap-proposal.py new file mode 100644 index 0000000..c31c476 --- /dev/null +++ b/qualification/issue-184/tool-acquisition/184-acquisition-launcher-bootstrap-proposal.py @@ -0,0 +1,142 @@ +"""Unexecuted reviewed inline bootstrap; no decision is selected by this source. + +A future invocation must embed these exact reviewed bytes under env -i and the +absolute pinned interpreter with -I -S. Hash/decision arguments bind that separate +General decision; a syntactically valid comment URL does not grant permission. +""" +from __future__ import annotations + +import datetime as dt +import hashlib +import json +import os +from pathlib import Path +import re +import resource +import signal +import stat +import sys +import time +from types import ModuleType + +ENV={'PATH':'/usr/bin:/bin','LANG':'C.UTF-8','LC_ALL':'C.UTF-8','TZ':'UTC'} +SESSION=Path('/tmp/sdk-python-handoff/184-acquisition-session-proposal.py') +OLD='https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-6649046802' + + +def require(value,code): + if not value:raise ValueError(code) + + +def pairs(values): + result={} + for key,value in values: + require(key not in result,'DUPLICATE_AUTHORITY_KEY');result[key]=value + return result + + +def reject_constant(value): + raise ValueError('NONFINITE_AUTHORITY_NUMBER') + + +def parse_authority(raw): + authority=json.loads(raw.decode('utf-8'),object_pairs_hook=pairs,parse_constant=reject_constant) + require(type(authority) is dict and set(authority)=={ + 'decision','selected_source','inputs','environment','host','total_seconds','io_seconds', + 'retries','survivor_policy','pool_parent','launcher_sha256','cpu_affinity', + 'retention_deadline_utc'},'EXACT_AUTHORITY_SCHEMA_REQUIRED') + require(type(authority['inputs']) is dict and set(authority['inputs'])=={ + 'owner','bootstrap','guardian','terminal','selection','interpreter','library','ca'} + and all(type(value) is str and re.fullmatch(r'[0-9a-f]{64}',value) + for value in authority['inputs'].values()),'EXACT_INPUT_SCHEMA_REQUIRED') + require(type(authority['environment']) is dict and authority['environment']==ENV + and all(type(value) is str for value in authority['environment'].values()),'EXACT_ENVIRONMENT_SCHEMA_REQUIRED') + require(all(type(authority[key]) is int for key in ('total_seconds','io_seconds','retries')), + 'INTEGER_AUTHORITY_LIMITS_REQUIRED') + require(all(type(authority[key]) is str for key in ('decision','selected_source','host', + 'survivor_policy','pool_parent','launcher_sha256','retention_deadline_utc')),'EXACT_AUTHORITY_VALUE_TYPES_REQUIRED') + return authority + + +def read_verified(path,expected,cap): + fd=os.open(path,os.O_RDONLY|os.O_NOFOLLOW|os.O_NONBLOCK) + with os.fdopen(fd,'rb') as stream: + before=os.fstat(stream.fileno()) + require(stat.S_ISREG(before.st_mode) and before.st_uid==os.geteuid() and before.st_nlink==1 + and not before.st_mode&0o022 and before.st_size<=cap,'BOOTSTRAP_INPUT_CUSTODY_CHANGED') + raw=stream.read(cap+1);after=os.fstat(stream.fileno()) + stamp=lambda st:(st.st_dev,st.st_ino,st.st_size,st.st_mtime_ns,st.st_ctime_ns) + require(stamp(before)==stamp(after) and len(raw)==before.st_size + and hashlib.sha256(raw).hexdigest()==expected,'BOOTSTRAP_INPUT_BYTES_CHANGED') + require(stamp(os.stat(path,follow_symlinks=False))==stamp(before),'BOOTSTRAP_INPUT_PATH_CHANGED') + return raw + + +def configure_resources(affinity): + require(isinstance(affinity,list) and 1<=len(affinity)<=2 + and all(type(cpu) is int and cpu>=0 for cpu in affinity) + and len(set(affinity))==len(affinity) and set(affinity)<=os.sched_getaffinity(0), + 'EXACT_AVAILABLE_CPU_AFFINITY_REQUIRED') + os.sched_setaffinity(0,set(affinity)) + resource.setrlimit(resource.RLIMIT_AS,(128*1024**2,384*1024**2)) + resource.setrlimit(resource.RLIMIT_CPU,(30,60)) + resource.setrlimit(resource.RLIMIT_NOFILE,(128,128)) + resource.setrlimit(resource.RLIMIT_FSIZE,(4*1024**2,4*1024**2)) + signal.signal(signal.SIGCHLD,signal.SIG_DFL) + + +def launch(authority_path,expected_authority_sha,expected_decision,expected_session_sha): + started=time.monotonic();started_utc=dt.datetime.now(dt.timezone.utc).isoformat() + require(dict(os.environ)==ENV and sys.flags.isolated==1 and sys.flags.no_site==1, + 'ENV_I_AND_ISOLATED_NO_SITE_REQUIRED_BEFORE_STARTUP') + require(isinstance(expected_decision,str) and expected_decision!=OLD + and re.fullmatch(r'https://github\.com/durable-workflow/sdk-python/issues/184#issuecomment-[1-9][0-9]{5,17}',expected_decision), + 'EXACT_NEW_GENERAL_DECISION_REQUIRED') + raw=read_verified(Path(authority_path),expected_authority_sha,32768) + authority=parse_authority(raw) + require(authority.get('decision')==expected_decision + and authority.get('launcher_sha256')==expected_session_sha,'EXACT_AUTHORITY_BINDING_CHANGED') + code_raw=read_verified(SESSION,expected_session_sha,65536) + configure_resources(authority.get('cpu_affinity')) + module=ModuleType('reviewed_guarded_acquisition_session');module.__file__=str(SESSION) + exec(compile(code_raw,'','exec'),module.__dict__) + session=module.start(authority_path,expected_authority_sha,expected_decision,expected_session_sha, + started,started_utc) + anchor=dt.datetime.fromisoformat(started_utc) + print(json.dumps({'phase':'SESSION_CREATED','controller_pid':os.getpid(),'guardian_pid':session.pid, + 'pool':session.expected_pool,'started_at_utc':started_utc, + 'active_cutoff_utc':(anchor+dt.timedelta(seconds=270)).isoformat(), + 'cleanup_ceiling_utc':(anchor+dt.timedelta(seconds=300)).isoformat(), + 'authority_sha256':expected_authority_sha,'decision':expected_decision}),flush=True) + result=session.wait() + # A survivor/incomplete record retains the live Session object and all outer + # descriptor/process handles. The caller must escalate; it cannot finalize. + return session,result + + +def run_and_report(authority_path,expected_authority_sha,expected_decision,expected_session_sha): + """The only proposed invocation wrapper; retain custody until proved quiescent. + + CUSTODY_REQUIRED must be escalated immediately by the invoking agent/operator. + No automatic data retry, admission expansion or hidden work occurs here. + Unknown ownership can require explicit operator stop/manual reconciliation. + """ + session,result=launch(authority_path,expected_authority_sha,expected_decision,expected_session_sha) + print(json.dumps({'phase':'RESULT','record':result}),flush=True) + if not session.finished: + print(json.dumps({'phase':'CUSTODY_REQUIRED','controller_pid':os.getpid(),'guardian_pid':session.pid, + 'owner_pid':session.owner_pid,'pool':session.expected_pool, + 'terminal_cleanup_complete':False,'acquisition_accepted':False}),flush=True) + while not session.finished: + # This is the separately proposed failed-operation custody keeper, not + # acquisition work. Its policy must be explicitly admitted in authority. + later=session.reconcile_retained() + if later is not None: + result=later + if session.finished:print(json.dumps({'phase':'POST_BOUND_QUIESCENT_NONPASS','record':later}),flush=True) + if not session.finished:time.sleep(1) + return result + + +if __name__=='__main__': + raise SystemExit('UNEXECUTED_INLINE_SOURCE_PROPOSAL; no authority or new General decision selected') diff --git a/qualification/issue-184/tool-acquisition/184-acquisition-session-controls-result.json b/qualification/issue-184/tool-acquisition/184-acquisition-session-controls-result.json new file mode 100644 index 0000000..a023bf9 --- /dev/null +++ b/qualification/issue-184/tool-acquisition/184-acquisition-session-controls-result.json @@ -0,0 +1,246 @@ +{ + "acquisition_accepted": false, + "cases": 58, + "downloaded_wheel_imports_or_installs": 0, + "failed": 0, + "new_general_decision": null, + "passed": 58, + "real_network_requests": 0, + "results": [ + { + "id": "v6-guardian-pidfd-failure-loses-live-job-predecessor", + "outcome": "pass" + }, + { + "id": "guardian-pidfd-failure-before-allocation-stops-reaps-reserved-child", + "outcome": "pass" + }, + { + "id": "guardian-constructor-failure-before-allocation-stops-reaps-reserved-child", + "outcome": "pass" + }, + { + "id": "guardian-allocation-waits-for-returned-outer-session-start-gate", + "outcome": "pass" + }, + { + "id": "readonly-pre-ledger-MemoryError-closes-local-fd", + "outcome": "pass" + }, + { + "id": "v6-leak-metadata-immediate-pre-ledger-MemoryError", + "outcome": "pass" + }, + { + "id": "successor-closes-metadata-immediate-pre-ledger-MemoryError", + "outcome": "pass" + }, + { + "id": "v6-leak-events-immediate-pre-ledger-MemoryError", + "outcome": "pass" + }, + { + "id": "successor-closes-events-immediate-pre-ledger-MemoryError", + "outcome": "pass" + }, + { + "id": "readonly-pre-ledger-KeyboardInterrupt-closes-local-fd", + "outcome": "pass" + }, + { + "id": "v6-leak-metadata-immediate-pre-ledger-KeyboardInterrupt", + "outcome": "pass" + }, + { + "id": "successor-closes-metadata-immediate-pre-ledger-KeyboardInterrupt", + "outcome": "pass" + }, + { + "id": "v6-leak-events-immediate-pre-ledger-KeyboardInterrupt", + "outcome": "pass" + }, + { + "id": "successor-closes-events-immediate-pre-ledger-KeyboardInterrupt", + "outcome": "pass" + }, + { + "id": "v6-unowned-retention-pre-registration-open", + "outcome": "pass" + }, + { + "id": "successor-no-owner-tree-removed-pre-registration-open", + "outcome": "pass" + }, + { + "id": "v6-unowned-retention-pre-registration-fstat", + "outcome": "pass" + }, + { + "id": "successor-no-owner-tree-removed-pre-registration-fstat", + "outcome": "pass" + }, + { + "id": "v6-unowned-retention-pre-registration-send", + "outcome": "pass" + }, + { + "id": "successor-no-owner-tree-removed-pre-registration-send", + "outcome": "pass" + }, + { + "id": "v6-unowned-retention-pre-registration-send-after", + "outcome": "pass" + }, + { + "id": "successor-no-owner-tree-removed-pre-registration-send-after", + "outcome": "pass" + }, + { + "id": "unknown-pre-registration-tree-keeps-exact-guardian-custody", + "outcome": "pass" + }, + { + "id": "metadata-callback-failure-closes-recorded-directory-fd", + "outcome": "pass" + }, + { + "id": "events-callback-failure-closes-recorded-directory-fd", + "outcome": "pass" + }, + { + "id": "retention-deadline-boundary-0", + "outcome": "pass" + }, + { + "id": "retention-deadline-boundary-300", + "outcome": "pass" + }, + { + "id": "retention-deadline-boundary-301", + "outcome": "pass" + }, + { + "id": "retention-deadline-boundary-86400", + "outcome": "pass" + }, + { + "id": "retention-deadline-boundary-86401", + "outcome": "pass" + }, + { + "id": "alloc-after-root-root-custody-is-not-discarded", + "outcome": "pass" + }, + { + "id": "alloc-after-wheelhouse-root-custody-is-not-discarded", + "outcome": "pass" + }, + { + "id": "alloc-after-metadata-root-custody-is-not-discarded", + "outcome": "pass" + }, + { + "id": "alloc-unknown-root-custody-is-not-discarded", + "outcome": "pass" + }, + { + "id": "synthetic-owner-exit-independent-check-and-outer-readback", + "outcome": "pass" + }, + { + "id": "owner-failure-is-refusal-zero-archives", + "outcome": "pass" + }, + { + "id": "active-deadline-stops-and-reaps-exact-owner", + "outcome": "pass" + }, + { + "id": "candidate-marker-required-before-preserve", + "outcome": "pass" + }, + { + "id": "independent-readback-refusal-cleans-archives", + "outcome": "pass" + }, + { + "id": "explicit-outer-abort-stops-owner-cleans-archives", + "outcome": "pass" + }, + { + "id": "guardian-crash-outer-stops-exact-owner-cleans-archives", + "outcome": "pass" + }, + { + "id": "survivor-keeps-all-custody-until-confirmed-deferred-exit", + "outcome": "pass" + }, + { + "id": "actual-owner-and-terminal-compose-with-synthetic-six-tool-closure", + "outcome": "pass" + }, + { + "id": "actual-owner-transport-timeout-refuses-integrated-preserve", + "outcome": "pass" + }, + { + "id": "post-fork-pidfd-failure-exits-reserved-child-before-cleanup", + "outcome": "pass" + }, + { + "id": "unreported-live-owner-is-unknown-retains-custody", + "outcome": "pass" + }, + { + "id": "stop-permission-refusal-still-requires-confirmed-exit", + "outcome": "pass" + }, + { + "id": "late-quiescence-releases-custody-without-late-pass", + "outcome": "pass" + }, + { + "id": "outer-disappears-before-arming-no-owner-work", + "outcome": "pass" + }, + { + "id": "outer-disappears-with-blocked-owner-stops-before-work", + "outcome": "pass" + }, + { + "id": "descriptor-frame-roundtrip-preserves-scoped-handle", + "outcome": "pass" + }, + { + "id": "oversized-wire-frame-refused", + "outcome": "pass" + }, + { + "id": "duplicate-frame-key-refused", + "outcome": "pass" + }, + { + "id": "non-object-frame-refused", + "outcome": "pass" + }, + { + "id": "actual-root-owned-interpreter-clean-env-I-S-startup", + "outcome": "pass" + }, + { + "id": "missing-new-decision-refuses-before-authority-or-fork", + "outcome": "pass" + }, + { + "id": "all-control-children-reaped", + "outcome": "pass" + }, + { + "id": "all-control-descriptors-closed", + "outcome": "pass" + } + ], + "scope": "synthetic role-protocol controls; start/admitted-data API never called with a decision", + "selected_archives_acquired": 0, + "source_sha256": "608c968c38f95774f2cd68aa873b0a978ba48f363aebd86db14cfe61ce541a04" +} diff --git a/qualification/issue-184/tool-acquisition/184-acquisition-session-controls.py b/qualification/issue-184/tool-acquisition/184-acquisition-session-controls.py new file mode 100644 index 0000000..8704cd5 --- /dev/null +++ b/qualification/issue-184/tool-acquisition/184-acquisition-session-controls.py @@ -0,0 +1,646 @@ +"""Synthetic private role protocol; never calls the admitted-data start API.""" +from __future__ import annotations + +import datetime as dt +import ast +import hashlib +import importlib.util +import io +import json +import os +from pathlib import Path +import select +import signal +import socket +import subprocess +import sys +import tempfile +import time +import traceback +import urllib.request +import zipfile +from types import ModuleType,SimpleNamespace +from unittest.mock import patch + +sys.dont_write_bytecode=True +BASE=Path('/tmp/sdk-python-handoff') +REF='https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-9999999999' + + +def load(name,path): + spec=importlib.util.spec_from_file_location(name,path);module=importlib.util.module_from_spec(spec) + spec.loader.exec_module(module);return module + + +s=load('trusted_session_protocol_control',BASE/'184-acquisition-session-proposal.py') +g=load('trusted_guard_protocol_control',BASE/'184-acquisition-custody-guardian-v7-proposal.py') +historical=BASE/'control-inputs/historical-source/session-v6.source' +assert hashlib.sha256(historical.read_bytes()).hexdigest()=='e721bba90c8cf7fae9eede4b3a2df2b7abb25b59f84b62f8ee5be5b9ee381e89' +predecessor=ModuleType('trusted_session_v6_predecessor');predecessor.__file__=str(historical) +exec(compile(historical.read_bytes(),str(historical),'exec'),predecessor.__dict__) +t=load('trusted_terminal_protocol_control',BASE/'184-acquisition-terminal-proposal.py') +o=load('trusted_owner_protocol_control',BASE/'184-acquire-tools-v5.py') +selection=json.loads((BASE/'184-immutable-build-tool-input-proposal.json').read_text()) +filenames=[tool['filename'] for tool in selection['tools']] +metadata=[tool['name']+'.json' for tool in selection['tools']] +results=[] + + +class SyntheticBootstrap: + def __init__(self,mode):self.mode=mode + def launch(self,decision,proof): + assert dict(os.environ)==s.MINIMAL_ENV and decision==REF + signal.signal(signal.SIGALRM,signal.SIG_DFL);signal.alarm(5) + if self.mode=='fail':raise RuntimeError('synthetic failure only') + if self.mode in {'block','abort','guardian-crash','late-reconcile'}: + os.write(next(iter(proof['archive_fds'].values())),b'synthetic partial') + signal.pause() + if self.mode in {'survivor','stop-refusal'}: + os.write(next(iter(proof['archive_fds'].values())),b'synthetic retained partial') + time.sleep(0.8) + for fd in proof['archive_fds'].values(): + os.write(fd,b'synthetic protocol bytes');os.fchmod(fd,0o400);os.close(fd) + for name in ('metadata','wheelhouse','events'):(Path(proof['pool'])/name).chmod(0o500) + Path(proof['pool']).chmod(0o500) + return {'outcome':'pass' if self.mode=='bad-marker' else 'candidate','pool':proof['pool'], + 'source':s.SOURCE,'owner_environment_names':sorted(os.environ)} + + +def fake_terminal(mode): + def validate(*args): + if mode=='readback-refusal':raise t.Refusal('SYNTHETIC_READBACK_REFUSED') + assert args[-3] is True and args[-2]==0 + return {'status':'synthetic-protocol-only','acquisition_accepted':False} + return SimpleNamespace(Refusal=t.Refusal,validate_candidate=validate,snapshot=t.snapshot) + + +class Response: + def __init__(self,url,raw): + self.status=200;self.url=url;self.body=io.BytesIO(raw) + self.headers={'Content-Length':str(len(raw)),'Content-Encoding':'identity'} + def __enter__(self):return self + def __exit__(self,*args):self.body.close() + def geturl(self):return self.url + def read(self,count):return self.body.read(count) + + +def integrated_closure(parent,mode): + selected=json.loads((BASE/'184-immutable-build-tool-input-proposal.json').read_text()) + bodies={};archive_urls=set() + failed=BASE/'control-inputs' + for tool in selected['tools']: + authority=json.loads((failed/'metadata'/(tool['name']+'.json')).read_text()) + metadata=f"Metadata-Version: 2.4\nName: {tool['name']}\nVersion: {tool['version']}\nRequires-Python: {tool['requires_python']}\n" + metadata+=''.join('Requires-Dist: '+value+'\n' for value in authority['info'].get('requires_dist') or []) + buffer=io.BytesIO();root=tool['name'].replace('-','_')+'-'+tool['version']+'.dist-info/' + with zipfile.ZipFile(buffer,'w') as archive: + archive.writestr(root+'METADATA',metadata) + archive.writestr(root+'WHEEL','Wheel-Version: 1.0\nRoot-Is-Purelib: true\nTag: py3-none-any\n') + raw=buffer.getvalue();tool['bytes']=len(raw);tool['sha256']=hashlib.sha256(raw).hexdigest() + for row in authority['urls']: + if row['filename']==tool['filename']:row['size']=len(raw);row['digests']['sha256']=tool['sha256'] + bodies[tool['metadata_source']]=json.dumps(authority,sort_keys=True).encode() + bodies[tool['url']]=raw;archive_urls.add(tool['url']) + raw=json.dumps(selected,sort_keys=True).encode() + fixture=parent/'synthetic-selection.json';fixture.write_bytes(raw);fixture.chmod(0o400) + real_owner=load('trusted_owner_synthetic_data_closure',BASE/'184-acquire-tools-v5.py') + real_owner.SELECTION=fixture;real_owner.SELECTION_SHA=hashlib.sha256(raw).hexdigest() + real_owner.ARCHIVE_BYTES=sum(tool['bytes'] for tool in selected['tools']);real_owner.POOL_BASE=parent + real_owner.VERIFIED_SOURCE_SHA256=s.PINNED['owner'] + class Opener: + def open(self,request,timeout): + if mode=='integrated-timeout' and request.full_url in archive_urls:raise TimeoutError('synthetic transport only') + assert request.full_url in bodies + return Response(request.full_url,bodies[request.full_url]) + class Bootstrap: + def launch(self,decision,proof): + with patch.object(urllib.request,'build_opener',return_value=Opener()), \ + patch.object(real_owner.Acquisition,'cached',return_value=(None,None)): + return real_owner.run_guarded_once(decision,proof) + return real_owner,Bootstrap(),t + + +def cleanup_directory(path): + for directory in [path,*[p for p in path.rglob('*') if p.is_dir() and not p.is_symlink()]]: + directory.chmod(0o700) + + +def run_protocol(mode,disconnect=None,module=s): + with tempfile.TemporaryDirectory(prefix='sdk-python-184-session-controls-') as temporary: + parent=Path(temporary);outer,inner=socket.socketpair(socket.AF_UNIX,socket.SOCK_SEQPACKET) + started=time.monotonic();ceiling=started+3 + authority={'decision':REF};authority_raw=s.encoded({'scope':'synthetic protocol only','decision':REF}) + expected_pool=parent/'sdk-python-184-tools-guardian-14adc40b-9999999999' + modules=(g,o,SyntheticBootstrap(mode),fake_terminal(mode)) + if mode.startswith('integrated-'): + real_owner,bootstrap,terminal=integrated_closure(parent,mode);modules=(g,real_owner,bootstrap,terminal) + child=os.fork() + if child==0: + try: + outer.close();os.environ.clear();os.environ.update(s.MINIMAL_ENV) + module.POOL_PARENT=parent + if mode.startswith('pre-registration-'): + real_open=os.open;real_fstat=os.fstat;real_send=module.send_frame;opened=set() + def prereg_open(path,flags,*args,**kwargs): + if path==filenames[0] and flags==os.O_RDONLY|os.O_NOFOLLOW: + if mode=='pre-registration-open':raise OSError('synthetic readonly open failure') + fd=real_open(path,flags,*args,**kwargs);opened.add(fd);return fd + return real_open(path,flags,*args,**kwargs) + def prereg_fstat(fd): + if fd in opened and mode=='pre-registration-fstat':raise OSError('synthetic readonly identity failure') + return real_fstat(fd) + def prereg_send(channel,value,fds=()): + if value.get('type')=='REGISTERED': + if mode=='pre-registration-unknown':(expected_pool/'unknown-root-member').write_bytes(b'unknown must stay') + if mode=='pre-registration-send-after':real_send(channel,value,fds) + if mode in {'pre-registration-send','pre-registration-send-after','pre-registration-unknown'}: + raise OSError('synthetic registration send failure') + return real_send(channel,value,fds) + module.os.open=prereg_open;module.os.fstat=prereg_fstat;module.send_frame=prereg_send + if mode.startswith('alloc-'): + real_mkdir=os.mkdir;real_open=os.open + def allocated_mkdir(path,mode_bits=0o777,*,dir_fd=None): + if (path=='wheelhouse' and mode in {'alloc-after-root','alloc-unknown'}) or (path=='events' and mode=='alloc-after-metadata'): + if mode=='alloc-unknown':(expected_pool/'unknown-root-member').write_bytes(b'synthetic unknown stays') + raise OSError('synthetic allocation refusal') + return real_mkdir(path,mode_bits,dir_fd=dir_fd) + def allocated_open(path,flags,mode_bits=0o777,*,dir_fd=None): + if path==filenames[0] and mode=='alloc-after-wheelhouse':raise OSError('synthetic archive allocation refusal') + return real_open(path,flags,mode_bits,dir_fd=dir_fd) + s.os.mkdir=allocated_mkdir;s.os.open=allocated_open + if mode=='pidfd-fail': + def failed_pidfd(pid):raise OSError('synthetic pidfd prerequisite refusal') + s.os.pidfd_open=failed_pidfd + if mode=='stop-refusal': + def refused_stop(*args):raise PermissionError(1,'synthetic stop refusal') + s.signal.pidfd_send_signal=refused_stop + if mode=='survivor': + real_stop=s.stop_owner;calls=[0] + def stop_with_one_survivor(role,ceiling): + calls[0]+=1 + if calls[0]==1:return {'owner_exited':False,'owner_exit_code':None} + return real_stop(role,ceiling) + s.stop_owner=stop_with_one_survivor + code=module.guardian_job(inner,authority_raw,authority,started,dt.datetime.now(dt.timezone.utc).isoformat(), + modules,bounds=(0.05 if mode in {'survivor','stop-refusal'} else 1 if mode.startswith('integrated-') else 0.35,3)) + inner.close();os._exit(code) + except BaseException:os._exit(3) + inner.close();pidfd=os.pidfd_open(child);session=None;received=[];owner_pidfd=None;reaped=False + expected_pool=parent/'sdk-python-184-tools-guardian-14adc40b-9999999999' + try: + if disconnect: + outer.settimeout(2) + message,fds=s.recv_frame(outer);received+=fds + assert message['type']=='REGISTERED' and len(fds)==11 + if disconnect=='owner': + s.send_frame(outer,{'type':'ARM'}) + message,fds=s.recv_frame(outer);received+=fds + assert message['type']=='OWNER' and len(fds)==1;owner_pidfd=fds[0] + outer.close() + assert select.select([pidfd],[],[],max(0,ceiling-time.monotonic()))[0] + _,status=os.waitpid(child,0);reaped=True + assert os.waitstatus_to_exitcode(status)==1 + if owner_pidfd is not None:assert select.select([owner_pidfd],[],[],0)[0] + if disconnect=='register': + assert not expected_pool.exists() + assert all(os.fstat(fd).st_nlink==0 for fd in received[1:]) + else: + assert not list((expected_pool/'wheelhouse').iterdir()) + assert (expected_pool/'guardian-result.json').exists() + else: + session=s.Session(outer,child,pidfd,g,modules[3],ceiling,expected_pool,filenames,metadata,authority_raw) + if mode=='abort': + assert session.receive() is None and session.receive() is None + message=session.abort('SYNTHETIC_EXPLICIT_ABORT') + elif mode=='guardian-crash': + assert session.receive() is None and session.receive() is None + signal.pidfd_send_signal(session.pidfd,signal.SIGKILL) + message=session.wait() + elif mode=='late-reconcile': + assert session.receive() is None and session.receive() is None + session.ceiling=time.monotonic()+0.02 + first=session.wait() + assert not first['result']['terminal_cleanup_complete'] and not session.finished + assert select.select([session.pidfd],[],[],2)[0] + message=session.reconcile_retained() + else:message=session.wait() + reaped=session.guardian_reaped + result=message['result'];assert not result['acquisition_accepted'] + if mode=='pre-registration-unknown': + assert result['status']=='PRE_ADOPTION_CUSTODY_REQUIRED',result + assert not session.finished and not result['terminal_cleanup_complete'] + assert not select.select([pidfd],[],[],0)[0] + assert session.custody is None and session.owner_pidfd is None + assert (expected_pool/'unknown-root-member').read_bytes()==b'unknown must stay' + elif mode.startswith('pre-registration-'): + assert session.owner_pidfd is None and not result['acquisition_accepted'] + if module is predecessor: + assert not session.finished and result['outer_owner_custody_required'],result + assert result['status'] in {'REFUSED','OUTER_CUSTODY_REQUIRED'} + assert session.custody is None and expected_pool.exists() + assert select.select([pidfd],[],[],max(0,ceiling-time.monotonic()))[0] + assert not list((expected_pool/'wheelhouse').iterdir()) + else: + assert result['status']=='PRE_ADOPTION_REMOVED' and result['terminal_cleanup_complete'],result + assert result['owner_was_allocated'] is False and not expected_pool.exists() and session.finished + elif mode=='alloc-unknown': + assert not session.finished and result['outer_owner_custody_required'] + assert not result['terminal_cleanup_complete'] and not result.get('retained_forensic_custody_ready') + assert not select.select([pidfd],[],[],0)[0], 'partial allocation guardian lost custody' + assert (expected_pool/'unknown-root-member').read_bytes()==b'synthetic unknown stays' + elif mode.startswith('alloc-'): + assert result['status']=='PRE_ADOPTION_REMOVED' and result['terminal_cleanup_complete'] + assert result['allocation_cleanup']['complete'] and session.finished + assert not expected_pool.exists(), 'outer independently verifies allocation root absence' + elif mode=='late-reconcile': + assert result['quiescent_after_failed_bound'] and not result['terminal_cleanup_complete'] + assert not result['acquisition_accepted'] and session.finished + assert not list((expected_pool/'wheelhouse').iterdir()) + elif mode=='survivor': + assert result['status']=='SURVIVOR_CUSTODY_REQUIRED' and result['outer_owner_custody_required'] + assert not result['terminal_cleanup_complete'] and not session.finished + assert not select.select([session.owner_pidfd],[],[],0)[0] + assert all(os.fstat(row['fd']).st_nlink==1 for row in session.custody.archive_nodes.values()) + message=session.wait();result=message['result'];reaped=session.guardian_reaped + assert message['type']=='DEFERRED_CLEANUP' and result['retained_forensic_custody_ready'] and not result['terminal_cleanup_complete'] + assert session.finished and not list((expected_pool/'wheelhouse').iterdir()) + elif mode in {'positive','integrated-positive'}: + assert result['status']=='COLLECTED_FOR_OUTER_READBACK' and session.finished + assert len(list((expected_pool/'wheelhouse').iterdir()))==6 + assert (expected_pool/'operation-authority.json').read_bytes()==authority_raw + else: + assert result['status']=='REFUSED',result + assert not result['terminal_cleanup_complete'] and result['retained_custody_sealed'] + assert result['outer_owner_custody_required'] and result['retention_owner']=='sdk-python' + assert result['retained_tree_inventory_verified'] and result['root_tree_retained'] + assert not list((expected_pool/'wheelhouse').iterdir()) + assert session.finished + if mode=='pidfd-fail': + assert result['stop_reason']=='OWNER_PIDFD_PREREQUISITE_FAILED' + assert result['owner_process_exited'] and not result['owner_pidfd_available'] + if mode=='stop-refusal':assert result['stop_refusal_code']=='PIDFD_STOP_ERRNO_1' + if not session.finished and mode not in {'alloc-unknown','pre-registration-unknown'}: + assert select.select([pidfd],[],[],0)[0] + finally: + if not reaped: + try:os.kill(child,signal.SIGKILL) + except ProcessLookupError:pass + try:os.waitpid(child,0) + except ChildProcessError:pass + if session is not None:session.stack.close() + else: + outer.close() + for fd in received:os.close(fd) + os.close(pidfd) + cleanup_directory(parent) + + +def frame_control(raw,code): + one,two=socket.socketpair(socket.AF_UNIX,socket.SOCK_SEQPACKET) + try: + one.send(raw) + try:s.recv_frame(two) + except (ValueError,s.Refusal) as error: + if code is not None:assert str(error)==code,(str(error),code) + else:raise AssertionError('invalid frame accepted') + finally:one.close();two.close() + + +def unreported_live_owner_keeps_custody(): + """A separate private pipe gives only this test the missing actor's ground truth.""" + with tempfile.TemporaryDirectory(prefix='sdk-python-184-unreported-owner-control-') as temporary: + parent=Path(temporary);pool=parent/'sdk-python-184-tools-guardian-14adc40b-9999999999' + outer,inner=socket.socketpair(socket.AF_UNIX,socket.SOCK_SEQPACKET) + side_read,side_write=os.pipe();child=os.fork() + if child==0: + try: + outer.close();os.close(side_read);os.environ.clear();os.environ.update(s.MINIMAL_ENV) + custody=s.prepare_tree(g,parent,pool.name,filenames);custody.metadata_names=set(metadata) + proof={'pool':str(pool),'ready':True,'started_monotonic':time.monotonic(), + 'started_utc':dt.datetime.now(dt.timezone.utc).isoformat(),'dir_ids':custody.dir_ids} + fds=[custody.parent_fd,*[custody.directory_fds[d] for d in ('.','wheelhouse','metadata','events')], + *[custody.archive_nodes[name]['fd'] for name in filenames]] + s.send_frame(inner,{'type':'REGISTERED','proof':proof,'filenames':filenames,'metadata_names':sorted(metadata)},fds) + message,received=s.recv_frame(inner);assert message=={'type':'ARM'} and not received + hidden=os.fork() + if hidden==0: + inner.close();os.close(side_write) + signal.signal(signal.SIGALRM,signal.SIG_DFL);signal.alarm(5) + os.write(next(iter(custody.archive_nodes.values()))['fd'],b'synthetic live holder') + signal.pause();os._exit(2) + os.write(side_write,str(hidden).encode());os.close(side_write) + # Fault boundary: guardian exits without publishing OWNER/pidfd. + custody.close();inner.close();os._exit(0) + except BaseException:os._exit(3) + inner.close();os.close(side_write) + guardian_pidfd=os.pidfd_open(child) + session=s.Session(outer,child,guardian_pidfd,g,t,time.monotonic()+5,pool,filenames,metadata,b'{}') + unknown_pidfd=None + try: + assert session.receive() is None + hidden=int(os.read(side_read,64));unknown_pidfd=os.pidfd_open(hidden) + message=session.wait();result=message['result'] + assert result['status']=='OUTER_CUSTODY_REQUIRED' and result['guardian_process_exited'] + assert not result['owner_process_exited'] and not result['terminal_cleanup_complete'] + assert session.owner_pidfd is None and not session.finished + assert not select.select([unknown_pidfd],[],[],0)[0] + assert len(list((pool/'wheelhouse').iterdir()))==6 + assert all(os.fstat(row['fd']).st_nlink==1 for row in session.custody.archive_nodes.values()) + # Only the independent test knows/reconciles the missing exact handle. + signal.pidfd_send_signal(unknown_pidfd,signal.SIGKILL) + assert select.select([unknown_pidfd],[],[],2)[0] + session.owner_pidfd=os.dup(unknown_pidfd);session.owner_pid=hidden + session.stack.callback(os.close,session.owner_pidfd) + final=session.abort('SYNTHETIC_INDEPENDENT_HANDLE_RECONCILIATION') + assert final['result']['retained_tree_inventory_verified'] and session.finished + assert not final['result']['terminal_cleanup_complete'] and final['result']['outer_owner_custody_required'] + assert not list((pool/'wheelhouse').iterdir()) + finally: + if unknown_pidfd is not None: + if not select.select([unknown_pidfd],[],[],0)[0]:signal.pidfd_send_signal(unknown_pidfd,signal.SIGKILL) + assert select.select([unknown_pidfd],[],[],2)[0];os.close(unknown_pidfd) + if not session.guardian_reaped: + signal.pidfd_send_signal(guardian_pidfd,signal.SIGKILL);os.waitpid(child,0) + session.stack.close();os.close(side_read);cleanup_directory(parent) + + +def fd_frame_roundtrip(): + one,two=socket.socketpair(socket.AF_UNIX,socket.SOCK_SEQPACKET) + read,write=os.pipe();received=[] + try: + s.send_frame(one,{'type':'synthetic'},[read]) + value,received=s.recv_frame(two);assert value=={'type':'synthetic'} and len(received)==1 + os.write(write,b'G');assert os.read(received[0],1)==b'G' + finally: + one.close();two.close();os.close(read);os.close(write) + for fd in received:os.close(fd) + + +def actual_closed_startup(): + # Only trusted local stdlib/controller source, not a data operation or tool wheel. + cpus=sorted(os.sched_getaffinity(0))[:2] + code="import pathlib,types,signal; b=pathlib.Path('/tmp/sdk-python-handoff/184-acquisition-launcher-bootstrap-proposal.py'); bootstrap=types.ModuleType('resource_source_control'); exec(compile(b.read_bytes(),str(b),'exec'),bootstrap.__dict__); signal.signal(signal.SIGCHLD,signal.SIG_IGN); bootstrap.configure_resources("+repr(cpus)+"); p=pathlib.Path('/tmp/sdk-python-handoff/184-acquisition-session-proposal.py'); m=types.ModuleType('source_startup_control'); m.__file__=str(p); exec(compile(p.read_bytes(),str(p),'exec'),m.__dict__); m.verify_startup(); print('CLOSED_STARTUP_OK')" + run=subprocess.run([s.INTERPRETER,'-I','-S','-c',code],env=s.MINIMAL_ENV,close_fds=True, + capture_output=True,timeout=5,check=True) + assert run.stdout==b'CLOSED_STARTUP_OK\n' + + +def live_start_refuses_no_decision(): + with patch.object(s,'verify_startup'),patch.object(s,'checked_bytes',side_effect=AssertionError('must not read authority')): + try:s.start('/synthetic/absent','0'*64,None,'0'*64,time.monotonic(),dt.datetime.now(dt.timezone.utc).isoformat()) + except s.Refusal as error:assert str(error)=='UNFROZEN_OR_UNADMITTED_PACKET' + else:raise AssertionError('missing admission accepted') + + +def retention_boundary(seconds,accepted): + now=1800000000 + value=dt.datetime.fromtimestamp(now+seconds,dt.timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ') + try:s.verify_retention_deadline(value,now) + except s.Refusal as error: + assert not accepted and str(error)=='BOUNDED_FUTURE_RETENTION_REQUIRED' + else:assert accepted + + +def preparation_callback_failure_closes(directory): + with tempfile.TemporaryDirectory(prefix='sdk-python-184-extra-dir-close-control-') as temporary: + parent=Path(temporary);opened=[];held=None;real_open=os.open;real_callback=g.ExitStack.callback + def allocated_open(path,*args,**kwargs): + fd=real_open(path,*args,**kwargs) + if path==directory:opened.append(fd) + return fd + def callback(stack,function,*args,**kwargs): + if args and args[0] in opened:raise MemoryError('synthetic extra-directory callback failure') + return real_callback(stack,function,*args,**kwargs) + try: + with patch.object(s.os,'open',side_effect=allocated_open),patch.object(g.ExitStack,'callback',new=callback): + try:s.prepare_tree(g,parent,'sdk-python-184-tools-guardian-extra-dir-close',filenames) + except g.AllocationRefusal as error:held=error.custody;cleanup=error.cleanup + else:raise AssertionError('directory callback failure not exercised') + assert len(opened)==1 and held.directory_fds[directory]==opened[0] + assert cleanup['complete'] and not held.path.exists() + held.close();held.close() + try:os.fstat(opened[0]) + except OSError as error:assert error.errno==9 + else:raise AssertionError('extra directory descriptor leaked') + finally: + if held is not None:held.close() + + +def directory_before_ledger(module,directory,interruption): + tree=ast.parse(Path(module.__file__).read_text()) + insertion=next(node for node in ast.walk(tree) if isinstance(node,ast.Assign) + and any(isinstance(target,ast.Subscript) and isinstance(target.value,ast.Attribute) + and target.value.attr=='directory_fds' for target in node.targets)) + with tempfile.TemporaryDirectory(prefix='sdk-python-184-directory-pre-ledger-') as temporary: + parent=Path(temporary);held=None;opened=[] + def interrupt(frame,event,arg): + if event=='line' and frame.f_code.co_filename==module.__file__ and frame.f_lineno==insertion.lineno and frame.f_locals['name']==directory: + opened.append(frame.f_locals['fd']);raise interruption('synthetic pre-ledger directory interruption') + return interrupt + try: + sys.settrace(interrupt) + try:module.prepare_tree(g,parent,'sdk-python-184-tools-guardian-directory-pre-ledger',filenames) + except g.AllocationRefusal as error:held=error.custody + finally:sys.settrace(None) + assert len(opened)==1 + held.close();held.close() + if module is predecessor: + assert os.fstat(opened[0]).st_nlink==0, 'predecessor untracked directory FD not reproduced' + os.close(opened.pop()) + else: + try:os.fstat(opened[0]) + except OSError as error:assert error.errno==9 + else:raise AssertionError('pre-ledger directory descriptor leaked') + opened.clear() + assert not held.path.exists() + finally: + sys.settrace(None) + if held is not None:held.close() + for fd in opened: + try:os.close(fd) + except OSError:pass + + +def readonly_before_ledger(interruption): + with tempfile.TemporaryDirectory(prefix='sdk-python-184-readonly-pre-ledger-') as temporary: + custody=s.prepare_tree(g,Path(temporary),'sdk-python-184-tools-guardian-readonly-pre-ledger',filenames) + opened=[];real_open=os.open + class RefusedList: + def append(self,fd):raise interruption('synthetic read-only list insertion interruption') + def opened_fd(*args,**kwargs): + fd=real_open(*args,**kwargs);opened.append(fd);return fd + try: + with patch.object(s.os,'open',side_effect=opened_fd): + try:s.append_readonly_archive(custody,filenames[0],RefusedList()) + except interruption:pass + else:raise AssertionError('read-only insertion failure not exercised') + assert len(opened)==1 + try:os.fstat(opened[0]) + except OSError as error:assert error.errno==9 + else:raise AssertionError('pre-ledger read-only FD leaked') + assert s.remove_unadopted_tree(custody)['complete'] and not custody.path.exists() + finally:custody.close() + + +def guardian_startup_failure(mode): + with tempfile.TemporaryDirectory(prefix='sdk-python-184-guardian-setup-') as temporary: + parent=Path(temporary);pool=parent/'not-allocated';marker=parent/'job-started';session=None + def job(channel): + marker.write_bytes(b'synthetic source fixture only') + signal.signal(signal.SIGALRM,signal.SIG_DFL);signal.alarm(3) + signal.pause();return 1 + try: + target='pidfd_open' if mode=='pidfd' else 'Session' + owner=s.os if mode=='pidfd' else s + with patch.object(owner,target,side_effect=OSError('synthetic guardian startup prerequisite failure')): + session=s.spawn_guardian_session(job,g,t,time.monotonic()+2,pool,filenames,metadata,b'{}') + result=session.wait()['result'] + assert session.finished and result['status']=='GUARDIAN_SETUP_REFUSED' + assert result['guardian_process_exited'] and result['handles_closed'] and result['terminal_cleanup_complete'] + assert not result['allocation_gate_sent'] and not result['pool_was_allocated'] and not result['owner_was_allocated'] + assert not marker.exists() and not pool.exists() and not result['acquisition_accepted'] + try:os.waitpid(session.pid,os.WNOHANG) + except ChildProcessError:pass + else:raise AssertionError('exact setup guardian not reaped') + finally: + if session is not None and not session.stop['guardian_reaped']: + os.kill(session.pid,signal.SIGKILL);os.waitpid(session.pid,0) + + +def predecessor_guardian_pidfd_failure(): + # Compile only the exact frozen start() fork suffix, retaining its error path; + # substitute guardian_job with an inert marker fixture, never call start(). + tree=ast.parse(historical.read_text());start=next(node for node in tree.body if isinstance(node,ast.FunctionDef) and node.name=='start') + index=next(i for i,node in enumerate(start.body) if isinstance(node,ast.Assign) + and any(isinstance(target,ast.Tuple) for target in node.targets)) + function=ast.FunctionDef(name='frozen_guardian_fork_suffix',args=ast.arguments(posonlyargs=[],args=[],kwonlyargs=[],kw_defaults=[],defaults=[]), + body=start.body[index:],decorator_list=[]) + wrapper=ast.fix_missing_locations(ast.Module(body=[function],type_ignores=[])) + with tempfile.TemporaryDirectory(prefix='sdk-python-184-v6-guardian-pidfd-') as temporary: + parent=Path(temporary);marker=parent/'unowned-job-started';created=[];channels=[] + real_fork=os.fork;real_pair=socket.socketpair + def fork(): + pid=real_fork() + if pid:created.append(pid) + return pid + def pair(*args): + pair=real_pair(*args);channels.extend(pair);return pair + def job(*args): + marker.write_bytes(b'predecessor allocation could run without outer pidfd') + signal.signal(signal.SIGALRM,signal.SIG_DFL);signal.alarm(3);signal.pause();return 1 + env=dict(predecessor.__dict__,POOL_PARENT=parent,raw=b'{}',authority={'decision':REF}, + started=time.monotonic(),started_utc='synthetic',guard=g,owner=o,bootstrap=None,terminal=t, + selection=selection,guardian_job=job) + exec(compile(wrapper,str(historical)+'#exact-start-fork-suffix','exec'),env) + try: + with patch.object(predecessor.os,'fork',side_effect=fork),patch.object(predecessor.socket,'socketpair',side_effect=pair), \ + patch.object(predecessor.os,'pidfd_open',side_effect=OSError('synthetic outer pidfd failure')): + try:env['frozen_guardian_fork_suffix']() + except OSError:pass + else:raise AssertionError('predecessor pidfd refusal missing') + assert len(created)==1 + deadline=time.monotonic()+1 + while not marker.exists() and time.monotonic()','exec'),result.__dict__) + return result + + +def verify_startup(): + require(dict(os.environ)==MINIMAL_ENV and sys.flags.isolated==1 and sys.flags.no_site==1, + 'CLOSED_ISOLATED_NO_SITE_STARTUP_REQUIRED') + require(Path(sys.executable).resolve()==Path(INTERPRETER),'EXACT_INTERPRETER_REQUIRED') + require(1<=len(os.sched_getaffinity(0))<=2,'BOUNDED_CPU_AFFINITY_REQUIRED') + require(signal.getsignal(signal.SIGCHLD)==signal.SIG_DFL,'WAITABLE_CHILD_DISPOSITION_REQUIRED') + require(resource.getrlimit(resource.RLIMIT_AS)==(128*1024**2,384*1024**2) + and resource.getrlimit(resource.RLIMIT_CPU)==(30,60) + and resource.getrlimit(resource.RLIMIT_NOFILE)==(128,128) + and resource.getrlimit(resource.RLIMIT_FSIZE)==(POOL_CAP,POOL_CAP),'EXACT_STARTUP_LIMITS_REQUIRED') + checked_bytes(Path(INTERPRETER),INTERPRETER_SHA,8*1024**2,0) + checked_bytes(Path(LIBRARY),LIBRARY_SHA,9*1024**2,0) + + +def send_frame(channel,value,fds=()): + raw=encoded(value);require(len(raw)<=FRAME_CAP and len(fds)<=16,'FRAME_BOUND') + ancillary=[] if not fds else [(socket.SOL_SOCKET,socket.SCM_RIGHTS,array('i',fds))] + require(channel.sendmsg([raw],ancillary)==len(raw),'SHORT_FRAME_WRITE') + + +def recv_frame(channel): + raw,ancillary,flags,_=channel.recvmsg(FRAME_CAP+1,socket.CMSG_SPACE(16*array('i').itemsize)) + received=[] + try: + for level,kind,data in ancillary: + require(level==socket.SOL_SOCKET and kind==socket.SCM_RIGHTS,'UNKNOWN_FRAME_ANCILLARY') + values=array('i');values.frombytes(data[:len(data)-len(data)%values.itemsize]);received.extend(values) + require(raw and len(raw)<=FRAME_CAP and not flags&(socket.MSG_TRUNC|socket.MSG_CTRUNC), + 'FRAME_EOF_OR_TRUNCATION') + value=json.loads(raw,object_pairs_hook=pairs) + require(isinstance(value,dict),'FRAME_OBJECT_REQUIRED') + return value,received + except BaseException: + for fd in received:os.close(fd) + raise + + +def prepare_tree(guard,parent,leaf,filenames): + custody=guard.ArchiveCustody(parent,leaf,filenames) + custody.dir_ids={'.':list(custody.root_id),'wheelhouse':list(custody.wheelhouse_id)} + custody.directory_fds={'.':custody.root_fd,'wheelhouse':custody.wheelhouse_fd} + try: + for name in ('metadata','events'): + os.mkdir(name,0o700,dir_fd=custody.root_fd) + custody.allocated_directory_names.add(name) + st=os.stat(name,dir_fd=custody.root_fd,follow_symlinks=False) + custody.dir_ids[name]=[st.st_dev,st.st_ino] + fd=None;adopted=False + try: + fd=os.open(name,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW,dir_fd=custody.root_fd) + custody.directory_fds[name]=fd + adopted=True + custody.stack.callback(custody.close_owned_fd,fd) + st=os.fstat(fd) + require([st.st_dev,st.st_ino]==custody.dir_ids[name],'DIRECTORY_ALLOCATION_COLLISION') + finally: + if fd is not None and not adopted:custody.close_owned_fd(fd) + return custody + except BaseException as error: + # No owner exists. Roll back only identity-verified, empty extra dirs. + # On any collision the partial object/handles must escape to the keeper. + try: + for name in ('events','metadata'): + if name not in custody.dir_ids:continue + st=os.stat(name,dir_fd=custody.root_fd,follow_symlinks=False) + require([st.st_dev,st.st_ino]==custody.dir_ids[name],'ALLOCATION_ROLLBACK_COLLISION') + os.rmdir(name,dir_fd=custody.root_fd) + cleanup=custody.cleanup() + except BaseException as rollback_error: + cleanup={'complete':False,'outer_owner_custody_required':True, + 'refusal_code':type(rollback_error).__name__} + raise guard.AllocationRefusal(custody,cleanup,error) from error + + +def progress_bound(custody): + custody.scope();total=0;count=0 + for directory in ('.','metadata','wheelhouse','events'): + parent=custody.directory_fds[directory] + st=os.fstat(parent) + require([st.st_dev,st.st_ino]==custody.dir_ids[directory],'PROGRESS_DIRECTORY_CHANGED') + if directory!='.': + named=os.stat(directory,dir_fd=custody.root_fd,follow_symlinks=False) + require([named.st_dev,named.st_ino]==custody.dir_ids[directory],'PROGRESS_DIRECTORY_CHANGED') + names=set(os.listdir(parent)) + if directory=='.':names-={'metadata','wheelhouse','events'} + require(len(names)<=128,'PROGRESS_FILE_COUNT_BOUND') + for name in names: + st=os.stat(name,dir_fd=parent,follow_symlinks=False) + require(stat.S_ISREG(st.st_mode) and st.st_uid==os.geteuid() and st.st_nlink==1, + 'PROGRESS_FILE_CUSTODY_CHANGED') + total+=st.st_size;count+=1 + if directory=='wheelhouse': + require(name in custody.archive_nodes and st.st_size<=2*1024**2 + and (st.st_dev,st.st_ino)==custody.archive_nodes[name]['identity'], + 'PROGRESS_ARCHIVE_BOUND_OR_IDENTITY_CHANGED') + require(count<=128 and total<=POOL_CAP,'PROGRESS_POOL_BOUND') + total+=sum(os.fstat(row['fd']).st_size for row in custody.archive_nodes.values() + if os.fstat(row['fd']).st_nlink==0) + require(total<=POOL_CAP,'PROGRESS_POOL_BOUND') + require(sum(os.fstat(row['fd']).st_size for row in custody.archive_nodes.values())<=3*1024**2, + 'PROGRESS_ARCHIVE_TOTAL_BOUND') + return total + + +def spawn_owner(bootstrap,decision,proof,custody,cleanup_ceiling): + status_read,status_write=os.pipe2(os.O_CLOEXEC) + gate_read,gate_write=os.pipe2(os.O_CLOEXEC) + borrowed={name:custody.duplicate_for_owner(name) for name in custody.archive_nodes} + proof=dict(proof,archive_fds=borrowed) + pid=os.fork() + if pid==0: + try: + keep={status_write,gate_read,*borrowed.values()} + for item in Path('/proc/self/fd').iterdir(): + fd=int(item.name) + if fd>=3 and fd not in keep: + try:os.close(fd) + except OSError:pass + devnull=os.open('/dev/null',os.O_RDWR) + for fd in (0,1,2):os.dup2(devnull,fd) + if devnull not in keep and devnull>2:os.close(devnull) + os.environ.clear();os.environ.update(MINIMAL_ENV);os.chdir('/') + resource.setrlimit(resource.RLIMIT_AS,(384*1024**2,384*1024**2)) + resource.setrlimit(resource.RLIMIT_CPU,(60,60)) + resource.setrlimit(resource.RLIMIT_NOFILE,(64,64)) + resource.setrlimit(resource.RLIMIT_FSIZE,(POOL_CAP,POOL_CAP)) + require(os.read(gate_read,1)==b'G','OWNER_NOT_ARMED') + os.close(gate_read) + result=bootstrap.launch(decision,proof) + except BaseException as error: + result={'outcome':'fail','refusal_code':'OWNER_'+type(error).__name__.upper()} + try: + raw=encoded(result) + require(len(raw)<=32768,'OWNER_RESULT_BOUND') + while raw: + written=os.write(status_write,raw);require(written>0,'SHORT_OWNER_RESULT_WRITE');raw=raw[written:] + finally:os._exit(0 if result.get('outcome')=='candidate' else 1) + os.close(status_write);os.close(gate_read) + for fd in borrowed.values():os.close(fd) + os.set_blocking(status_read,False) + role={'pid':pid,'pidfd':None,'status':status_read,'gate':gate_write} + try: + role['pidfd']=os.pidfd_open(pid) + except BaseException: + # No RUN/G byte has been sent. This exact, unreaped child PID is reserved + # by this single-threaded parent, so cleanup cannot target a reused PID. + os.close(gate_write);role['gate']=-1 + stop_refusal=None + try:os.kill(pid,signal.SIGKILL) + except ProcessLookupError:pass + except OSError as error:stop_refusal='RESERVED_CHILD_STOP_ERRNO_'+str(error.errno) + limit=min(time.monotonic()+5,cleanup_ceiling) + while time.monotonic()=limit:return False + time.sleep(0.01) + + +def record_terminal(custody,result,authority_raw,label='guardian-result.json'): + require(label in {'guardian-result.json','outer-result.json'},'INVALID_TERMINAL_RECORD_NAME') + custody.scope();used=progress_bound(custody) + additions={'operation-authority.json':authority_raw,label:encoded(result)} + try: + fd=os.open('operation-authority.json',os.O_RDONLY|os.O_NOFOLLOW|os.O_NONBLOCK,dir_fd=custody.root_fd) + except FileNotFoundError:pass + else: + with os.fdopen(fd,'rb') as stream: + st=os.fstat(stream.fileno()) + require(stat.S_ISREG(st.st_mode) and st.st_uid==os.geteuid() and st.st_nlink==1 + and st.st_size==len(authority_raw) and stream.read(32769)==authority_raw, + 'EXISTING_OPERATION_AUTHORITY_COLLISION') + del additions['operation-authority.json'] + require(used+sum(len(raw) for raw in additions.values())<=POOL_CAP,'TERMINAL_RECORD_POOL_BOUND') + os.fchmod(custody.root_fd,0o700) + try: + for name,raw in additions.items(): + fd=os.open(name,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o400,dir_fd=custody.root_fd) + with os.fdopen(fd,'wb') as stream: + require(stream.write(raw)==len(raw),'SHORT_TERMINAL_RECORD_WRITE');stream.flush();os.fsync(stream.fileno()) + finally:os.fchmod(custody.root_fd,0o500) + + +def seal_retained(custody): + """Seal known new forensic nodes after exit; unknown replacements stay untouched.""" + custody.scope();complete=True + fixed={'selection.json','ca.pem','launch.json','wheel-metadata.json','acquisition-observation.json', + 'inventory.json','failure.json','operation-authority.json','guardian-result.json','outer-result.json'} + for directory,parent in custody.directory_fds.items(): + st=os.fstat(parent) + require([st.st_dev,st.st_ino]==custody.dir_ids[directory],'SEAL_DIRECTORY_CHANGED') + names=set(os.listdir(parent)) + if directory=='.':names-={'metadata','wheelhouse','events'} + for name in names: + known=(name in fixed if directory=='.' else name in custody.metadata_names if directory=='metadata' + else name in custody.archive_nodes if directory=='wheelhouse' + else bool(re.fullmatch(r'(?:0[1-9]|1[0-2])-(?:request|headers|complete|refusal)\.json',name))) + if not known:complete=False;continue + try: + if directory=='wheelhouse':custody.node(name) + fd=os.open(name,os.O_RDONLY|os.O_NOFOLLOW|os.O_NONBLOCK,dir_fd=parent) + try: + node=os.fstat(fd) + require(stat.S_ISREG(node.st_mode) and node.st_uid==os.geteuid() and node.st_nlink==1, + 'SEAL_FILE_CUSTODY_CHANGED') + named=os.stat(name,dir_fd=parent,follow_symlinks=False) + require((named.st_dev,named.st_ino)==(node.st_dev,node.st_ino),'SEAL_FILE_IDENTITY_CHANGED') + os.fchmod(fd,0o400) + finally:os.close(fd) + except (OSError,ValueError):complete=False + os.fchmod(parent,0o500) + custody.scope();return complete + + +def cleanup_after_exit(custody,stop,reason,ceiling): + if not stop['owner_exited']: + return {'status':'SURVIVOR_CUSTODY_REQUIRED','stop_reason':reason,'owner_process_exited':False, + 'archive_cleanup':{'complete':False,'refusal_code':'OWNER_EXIT_UNCONFIRMED'}, + 'terminal_cleanup_complete':False,'outer_owner_custody_required':True, + 'retained_owned_names':sorted(custody.archive_nodes),'acquisition_accepted':False} + try:cleanup=custody.cleanup_archives() + except BaseException as error: + cleanup={'complete':False,'refusal_code':str(error) if isinstance(error,ValueError) else type(error).__name__} + try:sealed=seal_retained(custody) + except BaseException:sealed=False + within=time.monotonic()=0:os.close(role[name]) + if custody is not None:custody.close() + + +class Session: + """Outer owner retains handles until process exit AND scoped terminal proof.""" + def __init__(self,channel,pid,pidfd,guard,terminal,ceiling,expected_pool,expected_names,expected_metadata,authority_raw=None,start_guardian=False): + self.channel,self.pid,self.pidfd=channel,pid,pidfd + self.guard,self.terminal,self.ceiling=guard,terminal,ceiling + self.expected_pool,self.expected_names=str(expected_pool),list(expected_names) + self.expected_metadata=set(expected_metadata) + self.authority_raw=authority_raw + self.retention_deadline_utc=(None if authority_raw is None else + json.loads(authority_raw,object_pairs_hook=pairs).get('retention_deadline_utc')) + self.custody=None;self.owner_pidfd=None;self.owner_pid=None + self.stack=ExitStack();self.stack.callback(channel.close);self.stack.callback(os.close,pidfd) + self.finished=False;self.guardian_reaped=False + self.needs_guardian_start=start_guardian + self.arm_sent=False;self.pre_adoption_result=None + + def receive(self): + if self.needs_guardian_start: + self.needs_guardian_start=False + send_frame(self.channel,{'type':'START'}) + self.channel.settimeout(max(0.001,self.ceiling-time.monotonic())) + message,fds=recv_frame(self.channel) + adopted=False + try: + if message.get('type')=='REGISTERED': + require(self.custody is None,'DUPLICATE_POOL_REGISTRATION') + names=message['filenames'];require(len(names)==6 and len(fds)==11,'POOL_DESCRIPTOR_HANDOFF_CHANGED') + proof=message['proof'] + require(proof.get('pool')==self.expected_pool and names==self.expected_names + and set(message.get('metadata_names',[]))==self.expected_metadata + and set(proof.get('dir_ids',{}))=={'.','wheelhouse','metadata','events'}, + 'EXPECTED_POOL_HANDOFF_CHANGED') + actual_parent=os.stat(Path(self.expected_pool).parent,follow_symlinks=False) + handed_parent=os.fstat(fds[0]) + require((actual_parent.st_dev,actual_parent.st_ino)==(handed_parent.st_dev,handed_parent.st_ino), + 'EXPECTED_PARENT_DESCRIPTOR_CHANGED') + lease=self.guard.ArchiveCustody.__new__(self.guard.ArchiveCustody) + lease.path=Path(proof['pool']);lease.parent_fd=fds[0];lease.root_fd=fds[1];lease.wheelhouse_fd=fds[2] + lease.dir_ids=proof['dir_ids'];lease.root_id=tuple(lease.dir_ids['.']);lease.wheelhouse_id=tuple(lease.dir_ids['wheelhouse']) + lease.directory_fds=dict(zip(['.','wheelhouse','metadata','events'],fds[1:5])) + lease.archive_nodes={name:{'fd':fd,'identity':(os.fstat(fd).st_dev,os.fstat(fd).st_ino)} for name,fd in zip(names,fds[5:])} + lease.metadata_names=self.expected_metadata + lease.closed_owned_fds=set();lease.allocated_directory_names={'wheelhouse','metadata','events'} + lease.pending_archive_names=set() + lease.scope();progress_bound(lease) + for name in names:require(lease.node(name).st_size==0,'ARCHIVE_NOT_EMPTY_AT_HANDOFF') + self.custody=lease + for fd in fds:self.stack.callback(os.close,fd) + adopted=True;self.arm_sent=True;send_frame(self.channel,{'type':'ARM'}) + elif message.get('type')=='OWNER': + require(self.custody is not None and self.owner_pidfd is None and len(fds)==1,'OWNER_DESCRIPTOR_HANDOFF_CHANGED') + require(os.readlink(f'/proc/self/fd/{fds[0]}')=='anon_inode:[pidfd]','EXACT_OWNER_PIDFD_REQUIRED') + info=Path(f'/proc/self/fdinfo/{fds[0]}').read_text() + require(('Pid:\t'+str(message['pid'])+'\n') in info,'EXACT_OWNER_PIDFD_REQUIRED') + self.owner_pidfd=fds[0];self.owner_pid=message['pid'];self.stack.callback(os.close,fds[0]);adopted=True + send_frame(self.channel,{'type':'RUN'}) + print(json.dumps({'phase':'OWNER_ARMED','owner_pid':self.owner_pid,'guardian_pid':self.pid, + 'pool':self.expected_pool}),flush=True) + elif message.get('type') in {'TERMINAL','DEFERRED_CLEANUP'}: + require(not fds,'UNEXPECTED_TERMINAL_DESCRIPTORS') + result=message['result'] + if result.get('status') in {'SURVIVOR_CUSTODY_REQUIRED','PRE_ADOPTION_CUSTODY_REQUIRED'}:return message + if result.get('status')=='PRE_ADOPTION_REMOVED': + self.verify_pre_adoption_removal(result) + self.pre_adoption_result=result + try:send_frame(self.channel,{'type':'REMOVAL_ACK'}) + except OSError:pass + require(select.select([self.pidfd],[],[],max(0,self.ceiling-time.monotonic()))[0], + 'GUARDIAN_EXIT_UNCONFIRMED') + _,status=os.waitpid(self.pid,0) + self.guardian_reaped=True + if result.get('status')=='COLLECTED_FOR_OUTER_READBACK': + require(os.waitstatus_to_exitcode(status)==0 and self.owner_pidfd is not None + and select.select([self.owner_pidfd],[],[],0)[0],'TERMINAL_PROCESS_EXIT_MISMATCH') + current=self.terminal.snapshot(self.custody) + public={name:{key:value for key,value in row.items() if key!='raw'} for name,row in current.items()} + require(public==message.get('inventory'),'OUTER_INVENTORY_READBACK_CHANGED') + elif result.get('status')=='ALLOCATION_REFUSED' and result.get('terminal_cleanup_complete'): + require(result.get('owner_was_allocated') is False,'ALLOCATION_OWNER_STATE_CHANGED') + if result['allocation_cleanup'].get('exclusive_root_created') is not False: + require(not os.path.lexists(self.expected_pool),'ALLOCATION_ROOT_RETAINED') + elif result.get('status')=='PRE_ADOPTION_REMOVED': + self.verify_pre_adoption_removal(result) + elif result.get('retained_forensic_custody_ready'): + require(result.get('owner_process_exited') is True or result.get('owner_was_allocated') is False, + 'RETENTION_OWNER_EXIT_UNPROVEN') + if self.owner_pidfd is not None: + require(select.select([self.owner_pidfd],[],[],0)[0],'RETAINED_OWNER_STILL_LIVE') + elif result.get('owner_was_allocated') is not False: + require(result.get('owner_exit_confirmation')=='reserved-child-waitpid' + and result.get('owner_pidfd_available') is False,'RETENTION_OWNER_HANDLE_UNPROVEN') + require(retained_inventory(self.custody,self.terminal)==message.get('inventory'), + 'OUTER_RETAINED_INVENTORY_CHANGED') + result['retained_tree_inventory_verified']=True + result['outer_owner_custody_required']=True + elif not result.get('terminal_cleanup_complete',False): + result['outer_owner_custody_required']=True;return message + self.finished=True;self.stack.close();return message + else:raise Refusal('UNEXPECTED_SESSION_MESSAGE') + return None + finally: + if not adopted: + for fd in fds:os.close(fd) + + def wait(self): + try: + while not self.finished: + result=self.receive() + if result is not None:return result + except BaseException as error: + code=str(error) if isinstance(error,Refusal) else '' + return self.abort('OUTER_'+(code if re.fullmatch(r'[A-Z0-9_]{1,128}',code) else type(error).__name__.upper())) + + def reconcile_retained(self): + """Custody-only reconciliation, never a retry or late operation acceptance.""" + if self.finished:return None + if self.owner_pidfd is None:return None + if not select.select([self.pidfd],[],[],0)[0] or not select.select([self.owner_pidfd],[],[],0)[0]:return None + result=self.abort('POST_BOUND_CUSTODY_RECONCILIATION') + record=result['result'] + if record.get('retained_tree_inventory_verified'): + self.finished=True;self.stack.close() + record['quiescent_after_failed_bound']=True + record['terminal_cleanup_complete']=False;record['acquisition_accepted']=False + return result + + def verify_pre_adoption_removal(self,result): + require(self.owner_pidfd is None and result.get('owner_was_allocated') is False + and result.get('outer_adoption_acknowledged') is False + and result.get('allocation_cleanup',{}).get('complete') is True + and result.get('terminal_cleanup_complete') is True + and time.monotonic()6 and kind=='archive'),'REQUEST_ORDER_CHANGED') + if kind=='metadata': + require(url in selected_metadata and url not in seen_metadata,'METADATA_REQUEST_CHANGED_OR_RETRIED') + seen_metadata.add(url);tool=selected_metadata[url];body=rows['metadata/'+tool['name']+'.json']['raw'] + else: + require(kind=='archive' and url in selected_archives and url not in seen_archives, + 'ARCHIVE_REQUEST_CHANGED_OR_RETRIED') + seen_archives.add(url);tool=selected_archives[url];body=rows['wheelhouse/'+tool['filename']]['raw'] + require(request.get('bytes_received')==len(body) and request.get('body_sha256')==sha(body), + 'REQUEST_BODY_CUSTODY_CHANGED') + prefix=f'events/{number:02d}-' + expected_names|={prefix+'request.json',prefix+'headers.json',prefix+'complete.json'} + require(all(prefix+suffix in rows for suffix in ('request.json','headers.json','complete.json')), + 'REQUEST_EVENT_RECORD_MISSING') + before=parsed(rows[prefix+'request.json']['raw']) + headers=parsed(rows[prefix+'headers.json']['raw']) + complete=parsed(rows[prefix+'complete.json']['raw']) + require(before=={'number':number,'kind':kind,'url':url,'attempt':1,'stage':'before-open'} + and complete==request,'REQUEST_EVENT_PHASE_LINK_CHANGED') + require(headers.get('number')==number and headers.get('status')==200 + and headers.get('same_selected_url') is True and headers.get('content_encoding')=='identity', + 'HTTP_RESPONSE_SHAPE_CHANGED') + if kind=='archive':require(headers.get('content_length')==tool['bytes'],'ARCHIVE_RESPONSE_SIZE_CHANGED') + require(seen_metadata==set(selected_metadata),'SIX_FRESH_METADATA_REQUESTS_REQUIRED') + require(set(rows)==expected_names,'EXTRA_OR_MISSING_TERMINAL_OUTPUT') + semantic=[] + inspector=owner_module.Acquisition.__new__(owner_module.Acquisition) + for tool in tools: + authority=parsed(rows['metadata/'+tool['name']+'.json']['raw']) + owner_module.validate_authority(authority,tool) + semantic.append(inspector.inspect_wheel(rows['wheelhouse/'+tool['filename']]['raw'],tool,authority)) + metadata=parsed(rows['wheel-metadata.json']['raw']) + require(metadata.get('environment')=='CPython3.13 Linux amd64; no extras' + and metadata.get('actual_requires_dist_matches_versioned_authority') is True + and metadata.get('active_dependency_edges_match_approved_metadata_selection') is True + and isinstance(metadata.get('tools'),list) and len(metadata['tools'])==6,'METADATA_RESULT_BOUNDARY_CHANGED') + for actual,record in zip(semantic,metadata['tools']): + require({key:value for key,value in record.items() if key!='cache_source'}==actual, + 'METADATA_SEMANTIC_READBACK_CHANGED') + second=snapshot(custody) + require({name:{key:value for key,value in row.items() if key!='raw'} for name,row in rows.items()} + =={name:{key:value for key,value in row.items() if key!='raw'} for name,row in second.items()}, + 'POOL_CHANGED_DURING_TERMINAL_READBACK') + require(time.monotonic()=24.0", + "pyproject_hooks" + ], + "bytes": 31179, + "cached_bytes_verified": false, + "filename": "build-1.6.1-py3-none-any.whl", + "metadata_source": "https://pypi.org/pypi/build/1.6.1/json", + "name": "build", + "requires_python": ">=3.10", + "sha256": "ecd351a4be9d35a9eaaba244a7687143c9c7d4aea6ac964e7e7ddab20cbcf4e7", + "url": "https://files.pythonhosted.org/packages/ad/9b/9fb3585dabcd73a1b2a6267f63f62649347c9e6d072c9fde365b105abb2c/build-1.6.1-py3-none-any.whl", + "version": "1.6.1" + }, + { + "active_requirements": [], + "bytes": 129956, + "cached_bytes_verified": false, + "filename": "packaging-26.3-py3-none-any.whl", + "metadata_source": "https://pypi.org/pypi/packaging/26.3/json", + "name": "packaging", + "requires_python": ">=3.9", + "sha256": "d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", + "url": "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", + "version": "26.3" + }, + { + "active_requirements": [], + "bytes": 1819850, + "cached_bytes_verified": false, + "filename": "pip-26.2.2-py3-none-any.whl", + "metadata_source": "https://pypi.org/pypi/pip/26.2.2/json", + "name": "pip", + "requires_python": ">=3.10", + "sha256": "fd7f4dfa8190e014013b9f08f3e973f4e4c26ffd179a204895533a7254813a70", + "url": "https://files.pythonhosted.org/packages/3f/06/3e4420302363fbb71be3c7f35ce042f54bfca330e3e66b3a2e3c4cca579f/pip-26.2.2-py3-none-any.whl", + "version": "26.2.2" + }, + { + "active_requirements": [], + "bytes": 10724, + "cached_bytes_verified": false, + "filename": "pyproject_hooks-1.3.3-py3-none-any.whl", + "metadata_source": "https://pypi.org/pypi/pyproject-hooks/1.3.3/json", + "name": "pyproject-hooks", + "requires_python": ">=3.8", + "sha256": "5fc53fdac9f7bd63fbcdc868fb5f90b4784d78a53a3d3388cd738b807441a20b", + "url": "https://files.pythonhosted.org/packages/85/11/044d1ae1b4ec0d7af88ee5bc91e081be1022533032b906a7bdabdbb60977/pyproject_hooks-1.3.3-py3-none-any.whl", + "version": "1.3.3" + }, + { + "active_requirements": [], + "bytes": 876263, + "cached_bytes_verified": false, + "filename": "setuptools-84.2.1-py3-none-any.whl", + "metadata_source": "https://pypi.org/pypi/setuptools/84.2.1/json", + "name": "setuptools", + "requires_python": ">=3.10", + "sha256": "ca140b45d5325a9478f880e351484f62fef15f336462e449ca0c03c873d589c2", + "url": "https://files.pythonhosted.org/packages/ef/55/6dc6d6bb953bdfc52519a0059592c52b6e3b8ad51cf8256dffceb55b0610/setuptools-84.2.1-py3-none-any.whl", + "version": "84.2.1" + }, + { + "active_requirements": [ + "packaging>=24.0" + ], + "bytes": 33320, + "cached_bytes_verified": false, + "filename": "wheel-0.48.0-py3-none-any.whl", + "metadata_source": "https://pypi.org/pypi/wheel/0.48.0/json", + "name": "wheel", + "requires_python": ">=3.9", + "sha256": "3217dcc807155e45db462d7ef2431f5ddda0d7273b700d05a67b271ceb1287ab", + "url": "https://files.pythonhosted.org/packages/2e/29/69cfbb602cd91690c55d38ba9fe53e6a7e76a6fa647bf38f19c138d25449/wheel-0.48.0-py3-none-any.whl", + "version": "0.48.0" + } + ], + "wheel_bytes_retrieved_or_installed": false +} diff --git a/qualification/issue-184/tool-acquisition/README.md b/qualification/issue-184/tool-acquisition/README.md new file mode 100644 index 0000000..1e3d48a --- /dev/null +++ b/qualification/issue-184/tool-acquisition/README.md @@ -0,0 +1,153 @@ +# Issue 184 guarded public-tool acquisition proposal + +**Unexecuted composed source packet for independent review. No acquisition is +admitted.** The authority template deliberately leaves the new General decision +and CPU assignment null. No real network, selected archive retrieval/import/install, +Docker, SDK build, installed qualification or connected retry ran here. + +This draft is stacked on selected SDK source14adc40b/tree9bdf4d38 for review only. +Do not merge it into either selected/frozen release preparation ref. SDK source, +pyproject and workflows are unchanged. Original frozen H/wheel/offline-v5 PASS, +connected PRODUCT-FAIL, failed data acquisition and its unproved exact cause retain +their original identities. Whole-source PASS does not qualify or replace them. + +## Source layers + +- `184-acquisition-launcher-bootstrap-proposal.py`: proposed reviewed inline + entry, absolute pinned host interpreter with-I-S and env-i, exact authority + hash/decision/source binding, CPU/AS/CPU-time/FD/file limits, startup anchor and + structured handles/cutoffs. `run_and_report` retains nonterminal Session custody. +- `184-acquisition-session-proposal.py`: independent guardian, four pinned + directories, six preallocated archive inodes, read-only outer duplicates, + bounded descriptor handoff, owner pidfd before RUN, progress bounds, exact exit, + independent preserve/readback, and explicit refusal/deferred custody states. +- Guardian-v7/session close the three composed-source findings6665713217: + allocation waits for START from a returned outer Session with a guardian pidfd. + A failed pidfd/Session setup closes known handles and stops/reaps the still + reserved child; an unconfirmed reap remains an explicit custody session. No + START, allocation, owner, retry or data fallback occurs on that path. + Before ARM acknowledgement, no owner exists: exact empty auxiliary directories, + archives, wheelhouse and root are removed, or the guardian stays alive holding + the partial tree. No pre-adoption forensic tree is sealed and abandoned. + The outer acknowledges verified removal, including a queued REGISTERED failure, + before exact guardian reap. Once ARM may have been sent, an unreported owner + remains unknown; the no-owner drain cannot discard a possible OWNER handoff. + Archive allocation intent precedes open. Raw archive/extra-directory/read-only + descriptors are protected through ledger insertion and close locally if that + insertion is interrupted. An archive with unproved ledger insertion remains + explicit unknown custody even if its root was removed or the archive moved. +- `184-acquire-tools-v5.py` and its bootstrap: adopt the attested pool before + network; fresh six metadata checks then exact cache/read-or-one-request inputs; + response/semantic diagnostics and candidate-only result. Exact reviewed CA + SHA2ef52ad5/118certificates is checked in-process; subject facts are attributed + to that reviewed byte identity. The owner has no child-launch surface. +- `184-acquisition-terminal-proposal.py`: independently verifies successful owner + exit, all six raw/ZIP/METADATA/WHEEL/dependency tuples, exact request/event + linkage, clean trust, sealed inode/byte inventory and stable second readback. +- Guardian-v6 additionally proposes the close-path correction6663692366. + Public close explicitly closes tracked archive/directory descriptors even if + callback registration failed. Callbacks use the same idempotent closer, so a + second close cannot close an unrelated descriptor reusing the number. Closed + descriptor numbers cannot re-enter unlink proof. Closing handles never deletes + or accepts a linked uncertain node. Extra directories are recorded before + callback registration too. +- Guardian-v5's archive-allocation correction6663491930 remains preserved. + Every exclusively opened archive name/raw descriptor enters custody before + callback/identity reads. A pending identity forbids handoff or named-node + removal. Completion requires type/UID/zero-link proof for that held raw fd, + with exact identity compared wherever already recorded. A pending but linked + archive remains explicit retained custody. +- Missing-root completion still requires every tracked directory/archive to + be unlinked. Moved children and directories created before their identity can + be read remain unproved custody. V4's pre-registration gap is retained as an + actual predecessor countercontrol; fully-unlinked partial archives are positives. +- Frozen guardian-v2/v3/v4/v5/v6 remain byte-identical predecessor countercontrols. + V3 closed findings6660476483/6662253657 (partial allocation and authority + parsing), but its later moved-child HOLD supersedes whole-packet PASS6662795537. + The exact frozen session-v6 is also retained as inert historical source. Its + guardian-pidfd loss, pre-registration unowned retention, and raw extra-directory + leaks are reproduced by controls. Exact successor review is pending; the narrow + v6 PASS does not close the later composed-source HOLD or carry to this packet. + +Normal preservation requires owner exit0, independent full input readback, +guardian exit and a second outer inventory comparison. Result is collected for +review with acquisition_accepted=false. Actual tool-pool acceptance and any later +SDK artifact production remain separate review/decision steps. + +The guardian authority parser rejects duplicate keys at every depth (including +escaped-equivalent names), non-finite constants, unknown/missing top-level or +nested input/environment fields, and float/bool/string limits. Exact raw SHA +binding remains required. The inline bootstrap applies the same lexical/schema +refusals before resource setup; the session binds the exact selected values. + +## Bounds and failure custody + +Proposed operation: assigned lab4, exact root-owned CPython3.14.4/libpython pins, +closed four-variable environment, one/two admitted CPUs,128MiB outer/guardian +soft AS and384MiB owner AS. The target metadata environment is CPython3.13/Linux +amd64/no extras; host parsing imports no selected tool code. Six exact public +archives total2,901,292B, capped at2MiB/file,3MiB archives and4MiB whole pool. +Setup counts inside270s active/I-O and300s total. No solve, substitute, automatic +retry, image pull or extra runtime/service phase exists. + +Confirmed exact exit precedes external archive cleanup. Failure to allocate a +pidfd consumes setup: the still-reserved child is stopped/reaped before cleanup, +with no RUN/data fallback. Missing/unreported handles mean unknown ownership. +Unknown roots/inodes stay untouched; incomplete custody cannot report completion. +Allocation failures surface a custody-bearing exception. Exact empty root and +wheelhouse removal is independently checked; unknown allocation siblings retain +the guardian and outer process handle instead of escaping constructor unwinding. + +The primitive's full cleanup removes identity-verified empty owned directories. +After acknowledged outer adoption, the composed session intentionally retains +sealed diagnostic trees after an exited owner. Those results explicitly keep root_tree_retained=true, +outer_owner_custody_required=true, retention_owner=sdk-python, and +terminal_cleanup_complete=false. Both roles verify the complete retained tree +inventory before process/fd custody can close. Unknown members cannot take that +retention path. Closing a quiescent role transfers the recorded forensic-tree +retention obligation to sdk-python; it does not assert removal or acquisition +acceptance. A separately admitted operation must bind its retention deadline (after the +300s operation bound and at most24h ahead). The template leaves it null. + +The authority proposes `retain-custody-and-escalate`. If exact exit/ownership +cannot be confirmed at the failed bound, the caller prints CUSTODY_REQUIRED and +holds handles/names. A custody-only keeper can outlive that failed300s bound; +no acquisition work or late pass is permitted. Known late quiescence releases +custody as an explicit nonpass. Unknown ownership requires immediate escalation +and possible explicit operator stop/manual reconciliation. General must separately +admit this residual policy; it is not permission supplied by these sources. + +## Source validation and inputs + +| Component | Passing local cases | Scope | +| --- | ---: | --- | +| Owner |86| parser, transport, trust, custody and no-child boundary | +| Preallocated adapter |16| readiness, descriptors, inode collisions and partial writes | +| Independent readback |16| synthetic six-tool tuples, metadata, ledger and inventory | +| Guardian successor |81| retained73 plus actual v6 immediate pre-ledger archive leak/false completion, successor close/unknown retention, root present/removed and MemoryError/KeyboardInterrupt | +| Role protocol |58| retained33 plus frozen guardian-pidfd loss, gated startup/constructor failure, pre-registration open/identity/send failures, unknown-tree keeper, immediate directory/read-only insertion failures, and exact child/FD absence | +| Launcher bootstrap |27| exact reads, authority schemas, startup refusals, resource affinity and custody wrapper | + +These are source controls, not acquired-package or SDK/runtime qualification. +The data start API is never called with a decision by the controls. All control +processes were independently absent and all ten original failed-pool hashes were +reverified unchanged. Ruff F checks and credential-pattern/diff checks pass. + +`control-inputs/` holds labelled derived semantic metadata fields and frozen +historical source countercontrols. Their hashes/origins are recorded; they are +not reconstructed original HTTP evidence or job entrypoints. Historical `.source` +files are hash-checked and compiled only under non-main control module names. +No control silently downloads missing input. Report reruns verify identical saved +bytes; a different result preserves the old record and refuses overwrite. + +Runtime/control paths remain explicit lab4 review paths under +`/tmp/sdk-python-handoff`. Materialization must skip identical immutable inputs +and refuse collisions. No generic installer/automatic dispatcher is provided. +`source-manifest.json` binds every packet file except itself; `review-packet.json` +records exact source/input scope and pending operational fields. + +Next owners: Security reviews the exact whole source/input packet; General owns +source sequencing and any new exact data decision. A filled authority/hash/CPU +assignment, fresh host/input/root/cache checks and one separately admitted call +must precede actual data collection. No publication/tag/guide/adoption or new +wheel/sdist/installed result is implied by this source return. diff --git a/qualification/issue-184/tool-acquisition/authority-template.json b/qualification/issue-184/tool-acquisition/authority-template.json new file mode 100644 index 0000000..1d0bef7 --- /dev/null +++ b/qualification/issue-184/tool-acquisition/authority-template.json @@ -0,0 +1,29 @@ +{ + "cpu_affinity": null, + "decision": null, + "environment": { + "LANG": "C.UTF-8", + "LC_ALL": "C.UTF-8", + "PATH": "/usr/bin:/bin", + "TZ": "UTC" + }, + "host": "lab4", + "inputs": { + "bootstrap": "6ca5da88054b4c3ea27b2614dfd987f88ecb3db981271a995bb9941d06398fed", + "ca": "2ef52ad56b4803e264045c915e55c236cbfa045b84b81efefc218a19eb44bd2b", + "guardian": "032a2da74a26ea8ed036428d5470420dd49f099b7fe61cfcf4d7ab216d720066", + "interpreter": "52e0a13e60a981d8c4b6478be2ba5176f69da07948a056bf49cf6f077e30cb41", + "library": "982cb3768d1d8b00126255ee2785bccf8360c68b197fb3dec95858ab56195f87", + "owner": "234e099b5208aaafca379d3e5535cf44b3158a327764eec0ef4e12afcfc45ffb", + "selection": "e9a7d639325b866569cdab83b81a2949437209a6bca91c881c2e365fdc573a1c", + "terminal": "7e75b0929e612ec8605d095956fd446c2ad779a8f8b7e8403563f2764b1e70bf" + }, + "io_seconds": 270, + "launcher_sha256": "608c968c38f95774f2cd68aa873b0a978ba48f363aebd86db14cfe61ce541a04", + "pool_parent": "/home/lab4/fleet/tmp", + "retention_deadline_utc": null, + "retries": 0, + "selected_source": "14adc40b00b68cc8a3e1ae5647a885a42f273650", + "survivor_policy": "retain-custody-and-escalate", + "total_seconds": 300 +} diff --git a/qualification/issue-184/tool-acquisition/control-inputs/historical-source/manifest.json b/qualification/issue-184/tool-acquisition/control-inputs/historical-source/manifest.json new file mode 100644 index 0000000..529f434 --- /dev/null +++ b/qualification/issue-184/tool-acquisition/control-inputs/historical-source/manifest.json @@ -0,0 +1,35 @@ +{ + "records": [ + { + "bytes": 17804, + "durable_source": "https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-6649346694", + "file": "original-owner.source", + "sha256": "e0b8955c8c26c68333e1439c4eb195ef803d710d68f4a4b297b3dd17fcfd7c54" + }, + { + "bytes": 34138, + "durable_source": "https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-6649812366", + "file": "v2-owner.source", + "sha256": "16200705aee523e1171ad9c2c1f8522fa089c3479c18d474fdd5ff6ed7b29ff7" + }, + { + "bytes": 39357, + "durable_source": "https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-6655818641", + "file": "v3-owner.source", + "sha256": "0d25554f8f15409141708073060798478185a1d9990ac0383cf1cf5661f05997" + }, + { + "bytes": 2769, + "durable_source": "https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-6655819896", + "file": "v3-bootstrap.source", + "sha256": "8797366c75b3f55d61c21c19ac3e98ba0bed5b65bbf0a04e4bf3e2b73d326fcc" + }, + { + "bytes": 41341, + "durable_source": "https://github.com/durable-workflow/sdk-python/tree/d42e510952f945e9de68cdd8a18b4f5cf8969987/qualification/issue-184/tool-acquisition/184-acquisition-session-proposal.py", + "file": "session-v6.source", + "sha256": "e721bba90c8cf7fae9eede4b3a2df2b7abb25b59f84b62f8ee5be5b9ee381e89" + } + ], + "scope": "historical inert source countercontrols; never job entrypoints or current admissions" +} diff --git a/qualification/issue-184/tool-acquisition/control-inputs/historical-source/original-owner.source b/qualification/issue-184/tool-acquisition/control-inputs/historical-source/original-owner.source new file mode 100644 index 0000000..558094f --- /dev/null +++ b/qualification/issue-184/tool-acquisition/control-inputs/historical-source/original-owner.source @@ -0,0 +1,313 @@ +"""One admitted data-only acquisition; never import/install any acquired wheel.""" +from __future__ import annotations + +import argparse +import datetime as dt +import email.parser +import hashlib +import io +import json +import os +from pathlib import Path, PurePosixPath +import re +import secrets +import signal +import ssl +import stat +import subprocess +import sys +import time +import urllib.parse +import urllib.request +import zipfile + +SELECTION = Path('/tmp/sdk-python-handoff/184-immutable-build-tool-input-proposal.json') +SELECTION_SHA = 'e9a7d639325b866569cdab83b81a2949437209a6bca91c881c2e365fdc573a1c' +CA_SOURCE = Path('/home/lab4/fleet/sessions/sdk-python/20261105-010242-eef95b/.venv/lib/python3.14/site-packages/certifi/cacert.pem') +CA_SHA = '2ef52ad56b4803e264045c915e55c236cbfa045b84b81efefc218a19eb44bd2b' +CA_WHEEL_SHA = 'e9b54de76e530bb6cf0ccd036787103acb58fd42ba930ea8bf948ddbaa2f61d1' +SOURCE = '14adc40b00b68cc8a3e1ae5647a885a42f273650' +ADMISSION = 'https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-6649046802' +ARCHIVE_BYTES = 2901292 +FILE_CAP, ARCHIVE_CAP, POOL_CAP = 2 * 1024**2, 3 * 1024**2, 4 * 1024**2 +JSON_CAP, ZIP_MEMBER_CAP = 65536, 65536 +BLOCKED = ('AAA Certificate Services', 'emSign ECC Root CA - G3') + + +def require(value: bool, message: str) -> None: + if not value: + raise ValueError(message) + + +def sha(raw: bytes) -> str: + return hashlib.sha256(raw).hexdigest() + + +def utc() -> str: + return dt.datetime.now(dt.timezone.utc).isoformat() + + +def encoded(value: object) -> bytes: + return (json.dumps(value, sort_keys=True, indent=2) + '\n').encode() + + +def pairs(values: list[tuple[str, object]]) -> dict: + result = {} + for key, value in values: + require(key not in result, 'duplicate JSON key') + result[key] = value + return result + + +def read_regular(path: Path, cap: int) -> bytes: + fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK) + with os.fdopen(fd, 'rb') as stream: + before = os.fstat(stream.fileno()) + require(stat.S_ISREG(before.st_mode) and before.st_uid == os.geteuid() + and before.st_size <= cap, 'invalid existing data file') + raw = stream.read(cap + 1) + after = os.fstat(stream.fileno()) + require((before.st_dev, before.st_ino, before.st_size, before.st_mtime_ns, before.st_ctime_ns) + == (after.st_dev, after.st_ino, after.st_size, after.st_mtime_ns, after.st_ctime_ns), + 'existing data changed while reading') + require(len(raw) == before.st_size, 'existing data size mismatch') + return raw + + +class NoRedirect(urllib.request.HTTPRedirectHandler): + def redirect_request(self, req, fp, code, msg, headers, newurl): + raise ValueError('redirect refused; no second request') + + +class Acquisition: + def __init__(self) -> None: + self.started = time.monotonic() + self.started_utc = utc() + self.io_deadline = self.started + 270 + self.ceiling = self.started + 300 + self.pool = Path('/tmp') / ('sdk-python-184-tools-14adc40b-' + + dt.datetime.now(dt.timezone.utc).strftime('%Y%m%dT%H%M%SZ') + '-' + secrets.token_hex(5)) + self.used = 0 + self.archive_used = 0 + self.requests: list[dict] = [] + self.files: set[str] = set() + self.phase = 'selection-and-existing-trust' + + def write(self, name: str, raw: bytes) -> None: + require(name not in self.files and len(raw) + self.used <= POOL_CAP, 'pool output bound or collision') + parts = PurePosixPath(name) + require(not parts.is_absolute() and '..' not in parts.parts and '\\' not in name, + 'invalid output path') + target = self.pool / name + fd = os.open(target, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600) + with os.fdopen(fd, 'wb') as stream: + require(stream.write(raw) == len(raw), 'short output write') + stream.flush() + os.fsync(stream.fileno()) + target.chmod(0o400) + self.used += len(raw) + self.files.add(name) + + def get(self, url: str, cap: int, kind: str, size: int | None = None) -> bytes: + parsed = urllib.parse.urlsplit(url) + allowed = 'pypi.org' if kind == 'metadata' else 'files.pythonhosted.org' + require(parsed.scheme == 'https' and parsed.hostname == allowed and not parsed.username + and not parsed.password and parsed.port is None and not parsed.query and not parsed.fragment, + 'unexpected public authority') + remaining = self.io_deadline - time.monotonic() + require(remaining > 0, 'acquisition I/O deadline') + self.requests.append({'kind': kind, 'url': url, 'attempt': 1}) + signal.setitimer(signal.ITIMER_REAL, min(20, remaining)) + try: + request = urllib.request.Request(url, headers={ + 'User-Agent': 'sdk-python-source-input/1', 'Accept-Encoding': 'identity', + 'Accept': 'application/json' if kind == 'metadata' else 'application/octet-stream', + }) + with self.opener.open(request, timeout=min(15, remaining)) as response: + require(response.status == 200 and response.geturl() == url, 'unexpected HTTP result') + require(response.headers.get('Content-Encoding', 'identity') == 'identity', 'encoded body refused') + length = response.headers.get('Content-Length') + if length is not None: + require(length.isdecimal() and int(length) <= cap, 'HTTP body exceeds bound') + if size is not None: + require(length is not None and int(length) == size, 'archive HTTP size changed') + raw = bytearray() + while True: + require(time.monotonic() < self.io_deadline, 'acquisition I/O deadline') + chunk = response.read(min(65536, cap + 1 - len(raw))) + if not chunk: + break + raw.extend(chunk) + require(len(raw) <= cap, 'HTTP body exceeds bound') + return bytes(raw) + finally: + signal.setitimer(signal.ITIMER_REAL, 0) + + def inspect_wheel(self, raw: bytes, tool: dict, authority: dict) -> dict: + with zipfile.ZipFile(io.BytesIO(raw)) as archive: + entries = archive.infolist() + require(0 < len(entries) <= 4096 and sum(row.file_size for row in entries) <= 64 * 1024**2, + 'ZIP member/count size bound') + names: set[str] = set() + for row in entries: + path = PurePosixPath(row.filename) + require(row.filename not in names and not path.is_absolute() and '..' not in path.parts + and str(path) == row.filename.rstrip('/') and not row.filename.endswith('//') + and len(row.filename.encode('utf-8')) <= 512 + and '\\' not in row.filename and ':' not in row.filename + and not any(ord(c) < 32 for c in row.filename) + and not row.flag_bits & 1, 'unsafe ZIP member') + mode = stat.S_IFMT(row.external_attr >> 16) + require(mode in (0, stat.S_IFDIR) if row.is_dir() else mode in (0, stat.S_IFREG), + 'ZIP special/link member') + names.add(row.filename) + metadata = [name for name in names if name.count('/') == 1 and name.endswith('.dist-info/METADATA')] + wheel = [name for name in names if name.count('/') == 1 and name.endswith('.dist-info/WHEEL')] + require(len(metadata) == len(wheel) == 1, 'ambiguous wheel metadata') + require(metadata[0].rsplit('/', 1)[0] == wheel[0].rsplit('/', 1)[0], 'wheel metadata root mismatch') + for name in metadata + wheel: + require(archive.getinfo(name).file_size <= ZIP_MEMBER_CAP, 'wheel metadata size bound') + msg = email.parser.BytesParser().parsebytes(archive.read(metadata[0])) + whl = email.parser.BytesParser().parsebytes(archive.read(wheel[0])) + normalize = lambda value: re.sub(r'[-_.]+', '-', value).lower() + require(normalize(msg['Name']) == normalize(tool['name']) and msg['Version'] == tool['version'], + 'wheel name/version mismatch') + require(msg['Requires-Python'] == tool['requires_python'], 'wheel Python bound mismatch') + actual = sorted(' '.join(value.split()) for value in msg.get_all('Requires-Dist', [])) + published = sorted(' '.join(value.split()) for value in authority['info'].get('requires_dist') or []) + require(actual == published, 'wheel dependency metadata mismatch') + require(whl.get_all('Tag', []) == ['py3-none-any'] and whl['Root-Is-Purelib'] == 'true', + 'unexpected tool platform tag') + return {'name': tool['name'], 'version': tool['version'], 'metadata_member': metadata[0], + 'metadata_sha256': sha(archive.read(metadata[0])), 'requires_python': msg['Requires-Python'], + 'requires_dist': msg.get_all('Requires-Dist', []), + 'approved_active_requirements': tool['active_requirements'], + 'wheel_tags': whl.get_all('Tag', []), 'members': len(entries), + 'uncompressed_header_bytes': sum(row.file_size for row in entries), + 'extracted_or_imported': False} + + def run(self) -> dict: + selection_raw = read_regular(SELECTION, 32768) + require(sha(selection_raw) == SELECTION_SHA, 'selected tuple source changed') + selection = json.loads(selection_raw, object_pairs_hook=pairs) + require(selection['source'] == SOURCE and len(selection['tools']) == 6, 'wrong selected source/tool set') + tools = selection['tools'] + require(sum(tool['bytes'] for tool in tools) == ARCHIVE_BYTES <= ARCHIVE_CAP, + 'selected total archive size changed') + require(all(0 < tool['bytes'] <= FILE_CAP and Path(tool['filename']).name == tool['filename'] + for tool in tools), 'selected archive bounds') + ca = read_regular(CA_SOURCE, 300000) + require(sha(ca) == CA_SHA, 'existing clean CA provenance changed') + certs = re.findall(rb'-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----', ca, re.S) + require(len(certs) == 118, 'existing clean CA count changed') + for cert in certs: + check = subprocess.run(['/usr/bin/openssl', 'x509', '-noout', '-subject', '-nameopt', 'RFC2253'], + input=cert, capture_output=True, timeout=2, check=True) + subject = check.stdout.decode().removeprefix('subject=').strip() + require(not any(re.search(r'(?:^|[,+])CN=' + re.escape(name) + r'(?:$|[,+])', subject) + for name in BLOCKED), 'blocked CA subject') + context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + context.minimum_version = ssl.TLSVersion.TLSv1_2 + context.load_verify_locations(cadata=ca.decode('ascii')) + self.opener = urllib.request.build_opener(urllib.request.ProxyHandler({}), + urllib.request.HTTPSHandler(context=context), NoRedirect()) + os.mkdir(self.pool, 0o700) + os.mkdir(self.pool / 'metadata', 0o700) + os.mkdir(self.pool / 'wheelhouse', 0o700) + self.write('selection.json', selection_raw) + self.write('ca.pem', ca) + self.write('launch.json', encoded({'started_at_utc': self.started_utc, 'owner_pid': os.getpid(), + 'pool': str(self.pool), 'admission': ADMISSION, 'source': SOURCE, + 'io_seconds': 270, 'total_seconds': 300, + 'hard_cutoff_utc': (dt.datetime.fromisoformat(self.started_utc) + dt.timedelta(seconds=300)).isoformat(), + 'ca_sha256': CA_SHA, 'ca_certificates': 118, 'blocked_CA_subjects': 0, + 'CA_provenance': {'existing_pinned_wheel_sha256': CA_WHEEL_SHA, + 'sealed_original_input': '/Users/lab3/fleet/tmp/sdk-python-2150-connected-v1-once-01a1a5ef/inputs/wheelhouse/certifi-2026.10.23-py3-none-any.whl', + 'member': 'certifi/cacert.pem', 'member_sha256': CA_SHA}, + 'local_named_archive_cache_scan': {'roots': ['/home/lab4/.cache', '/tmp'], 'matching_selected_archives': 0}, + 'credentials_proxy_default_trust': False})) + print(json.dumps({'phase': 'launched', 'pool': str(self.pool), 'started_at_utc': self.started_utc}), flush=True) + authorities = {} + self.phase = 'official-versioned-metadata-recheck' + for tool in tools: + raw = self.get(tool['metadata_source'], JSON_CAP, 'metadata') + authority = json.loads(raw, object_pairs_hook=pairs) + matches = [row for row in authority['urls'] if row['filename'] == tool['filename']] + require(authority['info']['version'] == tool['version'] and len(matches) == 1, + 'versioned metadata identity changed') + row = matches[0] + require(row['url'] == tool['url'] and type(row['size']) is int and row['size'] == tool['bytes'] + and row['digests']['sha256'] == tool['sha256'] + and type(row['yanked']) is bool and row['yanked'] is False, + 'versioned archive tuple/yank state changed') + authorities[tool['name']] = authority + self.write('metadata/' + tool['name'] + '.json', raw) + actual_metadata = [] + self.phase = 'six-exact-archive-requests' + for tool in tools: + raw = self.get(tool['url'], FILE_CAP, 'archive', tool['bytes']) + require(len(raw) == tool['bytes'] and sha(raw) == tool['sha256'], 'archive bytes/hash mismatch') + require(self.archive_used + len(raw) <= ARCHIVE_CAP, 'combined archive size bound') + actual_metadata.append(self.inspect_wheel(raw, tool, authorities[tool['name']])) + self.write('wheelhouse/' + tool['filename'], raw) + self.archive_used += len(raw) + require(self.archive_used == ARCHIVE_BYTES, 'archive total mismatch') + self.write('wheel-metadata.json', encoded({'environment': 'CPython3.13 Linux amd64; no extras', + 'tools': actual_metadata, 'actual_requires_dist_matches_versioned_authority': True, + 'active_dependency_edges_match_approved_metadata_selection': True})) + require(time.monotonic() < self.ceiling, 'total acquisition deadline') + self.phase = 'sealing-custody' + self.write('acquisition-observation.json', encoded({'status': 'collected-before-final-custody-check', 'finished_at_utc': utc(), + 'elapsed_seconds': time.monotonic() - self.started, 'archive_bytes': self.archive_used, + 'http_requests': self.requests, 'archive_requests': sum(row['kind'] == 'archive' for row in self.requests), + 'retries': 0, 'import_install_container_SDK_build': False})) + entries = [] + for name in sorted(self.files): + p = self.pool / name + st = p.lstat() + require(stat.S_ISREG(st.st_mode) and st.st_nlink == 1 and st.st_uid == os.geteuid() + and stat.S_IMODE(st.st_mode) == 0o400, 'custody type/mode mismatch') + raw = read_regular(p, POOL_CAP) + entries.append({'path': name, 'bytes': len(raw), 'sha256': sha(raw), + 'device': st.st_dev, 'inode': st.st_ino, 'mode': '0400', 'links': st.st_nlink}) + inventory_raw = encoded({'source': SOURCE, 'entries': entries, 'inventory_excludes_self': True}) + self.write('inventory.json', inventory_raw) + for p in (self.pool / 'metadata', self.pool / 'wheelhouse', self.pool): + p.chmod(0o500) + actual_names = {str(p.relative_to(self.pool)) for p in self.pool.rglob('*') if p.is_file()} + require(actual_names == self.files and self.used == sum((self.pool / name).stat().st_size for name in self.files), + 'extra output or pool accounting mismatch') + require(self.used <= POOL_CAP and time.monotonic() < self.ceiling, 'final pool/time bound') + return {'outcome': 'pass', 'pool': str(self.pool), 'source': SOURCE, 'files': len(self.files), + 'whole_pool_bytes': self.used, 'archive_bytes': self.archive_used, + 'inventory_sha256': sha(inventory_raw), 'finished_at_utc': utc(), + 'elapsed_seconds': time.monotonic() - self.started, + 'import_install_container_SDK_build': False} + + +def alarm(signum, frame): + raise TimeoutError('bounded request deadline') + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--admitted-data-only', action='store_true', required=True) + parser.parse_args() + signal.signal(signal.SIGALRM, alarm) + owner = Acquisition() + try: + result = owner.run() + except BaseException as error: + signal.setitimer(signal.ITIMER_REAL, 0) + result = {'outcome': 'fail', 'pool': str(owner.pool), 'phase': owner.phase, + 'failure_type': type(error).__name__, 'finished_at_utc': utc(), + 'elapsed_seconds': time.monotonic() - owner.started, 'retries': 0, + 'import_install_container_SDK_build': False} + if owner.pool.is_dir() and time.monotonic() < owner.ceiling: + try: + owner.write('failure.json', encoded(result)) + except BaseException: + pass + print(json.dumps(result), flush=True) + sys.exit(1) + print(json.dumps(result), flush=True) diff --git a/qualification/issue-184/tool-acquisition/control-inputs/historical-source/session-v6.source b/qualification/issue-184/tool-acquisition/control-inputs/historical-source/session-v6.source new file mode 100644 index 0000000..001381d --- /dev/null +++ b/qualification/issue-184/tool-acquisition/control-inputs/historical-source/session-v6.source @@ -0,0 +1,699 @@ +"""Unexecuted isolated guardian/owner session. No CLI invocation is admitted. + +The public start function requires exact reviewed authority bytes/decision and +closed startup. A valid-looking URL alone is not authorization. In a survivor +state the session retains descriptor/process custody and cannot finalize. +""" +from __future__ import annotations + +from array import array +from contextlib import ExitStack +import datetime as dt +import hashlib +import json +import os +from pathlib import Path +import resource +import re +import select +import signal +import socket +import stat +import sys +import time +from types import ModuleType + +BASE=Path('/tmp/sdk-python-handoff') +POOL_PARENT=Path('/home/lab4/fleet/tmp') +MINIMAL_ENV={'PATH':'/usr/bin:/bin','LANG':'C.UTF-8','LC_ALL':'C.UTF-8','TZ':'UTC'} +SOURCE='14adc40b00b68cc8a3e1ae5647a885a42f273650' +INTERPRETER='/usr/bin/python3.14' +INTERPRETER_SHA='52e0a13e60a981d8c4b6478be2ba5176f69da07948a056bf49cf6f077e30cb41' +LIBRARY='/usr/lib/x86_64-linux-gnu/libpython3.14.so.1.0' +LIBRARY_SHA='982cb3768d1d8b00126255ee2785bccf8360c68b197fb3dec95858ab56195f87' +PINNED={'owner': '234e099b5208aaafca379d3e5535cf44b3158a327764eec0ef4e12afcfc45ffb', 'bootstrap': '6ca5da88054b4c3ea27b2614dfd987f88ecb3db981271a995bb9941d06398fed', 'guardian': 'cbb1672855a0dec48c7d952f02be49ab3c0935d9bd7b1cf467638d73fc543e4c', 'terminal': '7e75b0929e612ec8605d095956fd446c2ad779a8f8b7e8403563f2764b1e70bf', 'selection': 'e9a7d639325b866569cdab83b81a2949437209a6bca91c881c2e365fdc573a1c', 'interpreter': '52e0a13e60a981d8c4b6478be2ba5176f69da07948a056bf49cf6f077e30cb41', 'library': '982cb3768d1d8b00126255ee2785bccf8360c68b197fb3dec95858ab56195f87', 'ca': '2ef52ad56b4803e264045c915e55c236cbfa045b84b81efefc218a19eb44bd2b'} +FRAME_CAP=65536 +POOL_CAP=4*1024**2 + + +class Refusal(ValueError):pass + + +class OwnerSetupRefusal(Refusal): + def __init__(self,role,stop): + super().__init__('OWNER_PIDFD_PREREQUISITE_FAILED') + self.role,self.stop=role,stop + + +def require(value,code): + if not value:raise Refusal(code) + + +def sha(raw):return hashlib.sha256(raw).hexdigest() +def encoded(value):return (json.dumps(value,sort_keys=True,separators=(',',':'))+'\n').encode() + + +def pairs(values): + result={} + for key,value in values: + require(key not in result,'DUPLICATE_FRAME_KEY');result[key]=value + return result + + +def checked_bytes(path,expected,cap,uid): + descriptor=os.open(path,os.O_RDONLY|os.O_NOFOLLOW|os.O_NONBLOCK) + with os.fdopen(descriptor,'rb') as stream: + before=os.fstat(stream.fileno()) + require(stat.S_ISREG(before.st_mode) and before.st_uid==uid and before.st_nlink==1 + and not before.st_mode&0o022 and before.st_size<=cap,'INPUT_CUSTODY_CHANGED') + raw=stream.read(cap+1);after=os.fstat(stream.fileno()) + require((before.st_dev,before.st_ino,before.st_size,before.st_mtime_ns,before.st_ctime_ns) + ==(after.st_dev,after.st_ino,after.st_size,after.st_mtime_ns,after.st_ctime_ns) + and len(raw)==before.st_size and sha(raw)==expected,'INPUT_BYTES_CHANGED') + named=os.stat(path,follow_symlinks=False) + require((named.st_dev,named.st_ino,named.st_mtime_ns,named.st_ctime_ns) + ==(before.st_dev,before.st_ino,before.st_mtime_ns,before.st_ctime_ns),'INPUT_PATH_CHANGED') + return raw + + +def module(name,filename,expected): + raw=checked_bytes(BASE/filename,expected,65536,os.geteuid()) + result=ModuleType(name);result.__file__=str(BASE/filename) + result.VERIFIED_SOURCE_SHA256=expected + exec(compile(raw,'','exec'),result.__dict__) + return result + + +def verify_startup(): + require(dict(os.environ)==MINIMAL_ENV and sys.flags.isolated==1 and sys.flags.no_site==1, + 'CLOSED_ISOLATED_NO_SITE_STARTUP_REQUIRED') + require(Path(sys.executable).resolve()==Path(INTERPRETER),'EXACT_INTERPRETER_REQUIRED') + require(1<=len(os.sched_getaffinity(0))<=2,'BOUNDED_CPU_AFFINITY_REQUIRED') + require(signal.getsignal(signal.SIGCHLD)==signal.SIG_DFL,'WAITABLE_CHILD_DISPOSITION_REQUIRED') + require(resource.getrlimit(resource.RLIMIT_AS)==(128*1024**2,384*1024**2) + and resource.getrlimit(resource.RLIMIT_CPU)==(30,60) + and resource.getrlimit(resource.RLIMIT_NOFILE)==(128,128) + and resource.getrlimit(resource.RLIMIT_FSIZE)==(POOL_CAP,POOL_CAP),'EXACT_STARTUP_LIMITS_REQUIRED') + checked_bytes(Path(INTERPRETER),INTERPRETER_SHA,8*1024**2,0) + checked_bytes(Path(LIBRARY),LIBRARY_SHA,9*1024**2,0) + + +def send_frame(channel,value,fds=()): + raw=encoded(value);require(len(raw)<=FRAME_CAP and len(fds)<=16,'FRAME_BOUND') + ancillary=[] if not fds else [(socket.SOL_SOCKET,socket.SCM_RIGHTS,array('i',fds))] + require(channel.sendmsg([raw],ancillary)==len(raw),'SHORT_FRAME_WRITE') + + +def recv_frame(channel): + raw,ancillary,flags,_=channel.recvmsg(FRAME_CAP+1,socket.CMSG_SPACE(16*array('i').itemsize)) + received=[] + try: + for level,kind,data in ancillary: + require(level==socket.SOL_SOCKET and kind==socket.SCM_RIGHTS,'UNKNOWN_FRAME_ANCILLARY') + values=array('i');values.frombytes(data[:len(data)-len(data)%values.itemsize]);received.extend(values) + require(raw and len(raw)<=FRAME_CAP and not flags&(socket.MSG_TRUNC|socket.MSG_CTRUNC), + 'FRAME_EOF_OR_TRUNCATION') + value=json.loads(raw,object_pairs_hook=pairs) + require(isinstance(value,dict),'FRAME_OBJECT_REQUIRED') + return value,received + except BaseException: + for fd in received:os.close(fd) + raise + + +def prepare_tree(guard,parent,leaf,filenames): + custody=guard.ArchiveCustody(parent,leaf,filenames) + custody.dir_ids={'.':list(custody.root_id),'wheelhouse':list(custody.wheelhouse_id)} + custody.directory_fds={'.':custody.root_fd,'wheelhouse':custody.wheelhouse_fd} + try: + for name in ('metadata','events'): + os.mkdir(name,0o700,dir_fd=custody.root_fd) + custody.allocated_directory_names.add(name) + st=os.stat(name,dir_fd=custody.root_fd,follow_symlinks=False) + custody.dir_ids[name]=[st.st_dev,st.st_ino] + fd=os.open(name,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW,dir_fd=custody.root_fd) + custody.directory_fds[name]=fd + custody.stack.callback(custody.close_owned_fd,fd) + st=os.fstat(fd) + require([st.st_dev,st.st_ino]==custody.dir_ids[name],'DIRECTORY_ALLOCATION_COLLISION') + return custody + except BaseException as error: + # No owner exists. Roll back only identity-verified, empty extra dirs. + # On any collision the partial object/handles must escape to the keeper. + try: + for name in ('events','metadata'): + if name not in custody.dir_ids:continue + st=os.stat(name,dir_fd=custody.root_fd,follow_symlinks=False) + require([st.st_dev,st.st_ino]==custody.dir_ids[name],'ALLOCATION_ROLLBACK_COLLISION') + os.rmdir(name,dir_fd=custody.root_fd) + cleanup=custody.cleanup() + except BaseException as rollback_error: + cleanup={'complete':False,'outer_owner_custody_required':True, + 'refusal_code':type(rollback_error).__name__} + raise guard.AllocationRefusal(custody,cleanup,error) from error + + +def progress_bound(custody): + custody.scope();total=0;count=0 + for directory in ('.','metadata','wheelhouse','events'): + parent=custody.directory_fds[directory] + st=os.fstat(parent) + require([st.st_dev,st.st_ino]==custody.dir_ids[directory],'PROGRESS_DIRECTORY_CHANGED') + if directory!='.': + named=os.stat(directory,dir_fd=custody.root_fd,follow_symlinks=False) + require([named.st_dev,named.st_ino]==custody.dir_ids[directory],'PROGRESS_DIRECTORY_CHANGED') + names=set(os.listdir(parent)) + if directory=='.':names-={'metadata','wheelhouse','events'} + require(len(names)<=128,'PROGRESS_FILE_COUNT_BOUND') + for name in names: + st=os.stat(name,dir_fd=parent,follow_symlinks=False) + require(stat.S_ISREG(st.st_mode) and st.st_uid==os.geteuid() and st.st_nlink==1, + 'PROGRESS_FILE_CUSTODY_CHANGED') + total+=st.st_size;count+=1 + if directory=='wheelhouse': + require(name in custody.archive_nodes and st.st_size<=2*1024**2 + and (st.st_dev,st.st_ino)==custody.archive_nodes[name]['identity'], + 'PROGRESS_ARCHIVE_BOUND_OR_IDENTITY_CHANGED') + require(count<=128 and total<=POOL_CAP,'PROGRESS_POOL_BOUND') + total+=sum(os.fstat(row['fd']).st_size for row in custody.archive_nodes.values() + if os.fstat(row['fd']).st_nlink==0) + require(total<=POOL_CAP,'PROGRESS_POOL_BOUND') + require(sum(os.fstat(row['fd']).st_size for row in custody.archive_nodes.values())<=3*1024**2, + 'PROGRESS_ARCHIVE_TOTAL_BOUND') + return total + + +def spawn_owner(bootstrap,decision,proof,custody,cleanup_ceiling): + status_read,status_write=os.pipe2(os.O_CLOEXEC) + gate_read,gate_write=os.pipe2(os.O_CLOEXEC) + borrowed={name:custody.duplicate_for_owner(name) for name in custody.archive_nodes} + proof=dict(proof,archive_fds=borrowed) + pid=os.fork() + if pid==0: + try: + keep={status_write,gate_read,*borrowed.values()} + for item in Path('/proc/self/fd').iterdir(): + fd=int(item.name) + if fd>=3 and fd not in keep: + try:os.close(fd) + except OSError:pass + devnull=os.open('/dev/null',os.O_RDWR) + for fd in (0,1,2):os.dup2(devnull,fd) + if devnull not in keep and devnull>2:os.close(devnull) + os.environ.clear();os.environ.update(MINIMAL_ENV);os.chdir('/') + resource.setrlimit(resource.RLIMIT_AS,(384*1024**2,384*1024**2)) + resource.setrlimit(resource.RLIMIT_CPU,(60,60)) + resource.setrlimit(resource.RLIMIT_NOFILE,(64,64)) + resource.setrlimit(resource.RLIMIT_FSIZE,(POOL_CAP,POOL_CAP)) + require(os.read(gate_read,1)==b'G','OWNER_NOT_ARMED') + os.close(gate_read) + result=bootstrap.launch(decision,proof) + except BaseException as error: + result={'outcome':'fail','refusal_code':'OWNER_'+type(error).__name__.upper()} + try: + raw=encoded(result) + require(len(raw)<=32768,'OWNER_RESULT_BOUND') + while raw: + written=os.write(status_write,raw);require(written>0,'SHORT_OWNER_RESULT_WRITE');raw=raw[written:] + finally:os._exit(0 if result.get('outcome')=='candidate' else 1) + os.close(status_write);os.close(gate_read) + for fd in borrowed.values():os.close(fd) + os.set_blocking(status_read,False) + role={'pid':pid,'pidfd':None,'status':status_read,'gate':gate_write} + try: + role['pidfd']=os.pidfd_open(pid) + except BaseException: + # No RUN/G byte has been sent. This exact, unreaped child PID is reserved + # by this single-threaded parent, so cleanup cannot target a reused PID. + os.close(gate_write);role['gate']=-1 + stop_refusal=None + try:os.kill(pid,signal.SIGKILL) + except ProcessLookupError:pass + except OSError as error:stop_refusal='RESERVED_CHILD_STOP_ERRNO_'+str(error.errno) + limit=min(time.monotonic()+5,cleanup_ceiling) + while time.monotonic()=limit:return False + time.sleep(0.01) + + +def record_terminal(custody,result,authority_raw,label='guardian-result.json'): + require(label in {'guardian-result.json','outer-result.json'},'INVALID_TERMINAL_RECORD_NAME') + custody.scope();used=progress_bound(custody) + additions={'operation-authority.json':authority_raw,label:encoded(result)} + try: + fd=os.open('operation-authority.json',os.O_RDONLY|os.O_NOFOLLOW|os.O_NONBLOCK,dir_fd=custody.root_fd) + except FileNotFoundError:pass + else: + with os.fdopen(fd,'rb') as stream: + st=os.fstat(stream.fileno()) + require(stat.S_ISREG(st.st_mode) and st.st_uid==os.geteuid() and st.st_nlink==1 + and st.st_size==len(authority_raw) and stream.read(32769)==authority_raw, + 'EXISTING_OPERATION_AUTHORITY_COLLISION') + del additions['operation-authority.json'] + require(used+sum(len(raw) for raw in additions.values())<=POOL_CAP,'TERMINAL_RECORD_POOL_BOUND') + os.fchmod(custody.root_fd,0o700) + try: + for name,raw in additions.items(): + fd=os.open(name,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o400,dir_fd=custody.root_fd) + with os.fdopen(fd,'wb') as stream: + require(stream.write(raw)==len(raw),'SHORT_TERMINAL_RECORD_WRITE');stream.flush();os.fsync(stream.fileno()) + finally:os.fchmod(custody.root_fd,0o500) + + +def seal_retained(custody): + """Seal known new forensic nodes after exit; unknown replacements stay untouched.""" + custody.scope();complete=True + fixed={'selection.json','ca.pem','launch.json','wheel-metadata.json','acquisition-observation.json', + 'inventory.json','failure.json','operation-authority.json','guardian-result.json','outer-result.json'} + for directory,parent in custody.directory_fds.items(): + st=os.fstat(parent) + require([st.st_dev,st.st_ino]==custody.dir_ids[directory],'SEAL_DIRECTORY_CHANGED') + names=set(os.listdir(parent)) + if directory=='.':names-={'metadata','wheelhouse','events'} + for name in names: + known=(name in fixed if directory=='.' else name in custody.metadata_names if directory=='metadata' + else name in custody.archive_nodes if directory=='wheelhouse' + else bool(re.fullmatch(r'(?:0[1-9]|1[0-2])-(?:request|headers|complete|refusal)\.json',name))) + if not known:complete=False;continue + try: + if directory=='wheelhouse':custody.node(name) + fd=os.open(name,os.O_RDONLY|os.O_NOFOLLOW|os.O_NONBLOCK,dir_fd=parent) + try: + node=os.fstat(fd) + require(stat.S_ISREG(node.st_mode) and node.st_uid==os.geteuid() and node.st_nlink==1, + 'SEAL_FILE_CUSTODY_CHANGED') + named=os.stat(name,dir_fd=parent,follow_symlinks=False) + require((named.st_dev,named.st_ino)==(node.st_dev,node.st_ino),'SEAL_FILE_IDENTITY_CHANGED') + os.fchmod(fd,0o400) + finally:os.close(fd) + except (OSError,ValueError):complete=False + os.fchmod(parent,0o500) + custody.scope();return complete + + +def cleanup_after_exit(custody,stop,reason,ceiling): + if not stop['owner_exited']: + return {'status':'SURVIVOR_CUSTODY_REQUIRED','stop_reason':reason,'owner_process_exited':False, + 'archive_cleanup':{'complete':False,'refusal_code':'OWNER_EXIT_UNCONFIRMED'}, + 'terminal_cleanup_complete':False,'outer_owner_custody_required':True, + 'retained_owned_names':sorted(custody.archive_nodes),'acquisition_accepted':False} + try:cleanup=custody.cleanup_archives() + except BaseException as error: + cleanup={'complete':False,'refusal_code':str(error) if isinstance(error,ValueError) else type(error).__name__} + try:sealed=seal_retained(custody) + except BaseException:sealed=False + within=time.monotonic()=0:os.close(role[name]) + if custody is not None:custody.close() + + +class Session: + """Outer owner retains handles until process exit AND scoped terminal proof.""" + def __init__(self,channel,pid,pidfd,guard,terminal,ceiling,expected_pool,expected_names,expected_metadata,authority_raw=None): + self.channel,self.pid,self.pidfd=channel,pid,pidfd + self.guard,self.terminal,self.ceiling=guard,terminal,ceiling + self.expected_pool,self.expected_names=str(expected_pool),list(expected_names) + self.expected_metadata=set(expected_metadata) + self.authority_raw=authority_raw + self.retention_deadline_utc=(None if authority_raw is None else + json.loads(authority_raw,object_pairs_hook=pairs).get('retention_deadline_utc')) + self.custody=None;self.owner_pidfd=None;self.owner_pid=None + self.stack=ExitStack();self.stack.callback(channel.close);self.stack.callback(os.close,pidfd) + self.finished=False;self.guardian_reaped=False + + def receive(self): + self.channel.settimeout(max(0.001,self.ceiling-time.monotonic())) + message,fds=recv_frame(self.channel) + adopted=False + try: + if message.get('type')=='REGISTERED': + require(self.custody is None,'DUPLICATE_POOL_REGISTRATION') + names=message['filenames'];require(len(names)==6 and len(fds)==11,'POOL_DESCRIPTOR_HANDOFF_CHANGED') + proof=message['proof'] + require(proof.get('pool')==self.expected_pool and names==self.expected_names + and set(message.get('metadata_names',[]))==self.expected_metadata + and set(proof.get('dir_ids',{}))=={'.','wheelhouse','metadata','events'}, + 'EXPECTED_POOL_HANDOFF_CHANGED') + actual_parent=os.stat(Path(self.expected_pool).parent,follow_symlinks=False) + handed_parent=os.fstat(fds[0]) + require((actual_parent.st_dev,actual_parent.st_ino)==(handed_parent.st_dev,handed_parent.st_ino), + 'EXPECTED_PARENT_DESCRIPTOR_CHANGED') + lease=self.guard.ArchiveCustody.__new__(self.guard.ArchiveCustody) + lease.path=Path(proof['pool']);lease.parent_fd=fds[0];lease.root_fd=fds[1];lease.wheelhouse_fd=fds[2] + lease.dir_ids=proof['dir_ids'];lease.root_id=tuple(lease.dir_ids['.']);lease.wheelhouse_id=tuple(lease.dir_ids['wheelhouse']) + lease.directory_fds=dict(zip(['.','wheelhouse','metadata','events'],fds[1:5])) + lease.archive_nodes={name:{'fd':fd,'identity':(os.fstat(fd).st_dev,os.fstat(fd).st_ino)} for name,fd in zip(names,fds[5:])} + lease.metadata_names=self.expected_metadata + lease.scope();progress_bound(lease) + for name in names:require(lease.node(name).st_size==0,'ARCHIVE_NOT_EMPTY_AT_HANDOFF') + self.custody=lease + for fd in fds:self.stack.callback(os.close,fd) + adopted=True;send_frame(self.channel,{'type':'ARM'}) + elif message.get('type')=='OWNER': + require(self.custody is not None and self.owner_pidfd is None and len(fds)==1,'OWNER_DESCRIPTOR_HANDOFF_CHANGED') + require(os.readlink(f'/proc/self/fd/{fds[0]}')=='anon_inode:[pidfd]','EXACT_OWNER_PIDFD_REQUIRED') + info=Path(f'/proc/self/fdinfo/{fds[0]}').read_text() + require(('Pid:\t'+str(message['pid'])+'\n') in info,'EXACT_OWNER_PIDFD_REQUIRED') + self.owner_pidfd=fds[0];self.owner_pid=message['pid'];self.stack.callback(os.close,fds[0]);adopted=True + send_frame(self.channel,{'type':'RUN'}) + print(json.dumps({'phase':'OWNER_ARMED','owner_pid':self.owner_pid,'guardian_pid':self.pid, + 'pool':self.expected_pool}),flush=True) + elif message.get('type') in {'TERMINAL','DEFERRED_CLEANUP'}: + require(not fds,'UNEXPECTED_TERMINAL_DESCRIPTORS') + result=message['result'] + if result.get('status')=='SURVIVOR_CUSTODY_REQUIRED':return message + require(select.select([self.pidfd],[],[],max(0,self.ceiling-time.monotonic()))[0], + 'GUARDIAN_EXIT_UNCONFIRMED') + _,status=os.waitpid(self.pid,0) + self.guardian_reaped=True + if result.get('status')=='COLLECTED_FOR_OUTER_READBACK': + require(os.waitstatus_to_exitcode(status)==0 and self.owner_pidfd is not None + and select.select([self.owner_pidfd],[],[],0)[0],'TERMINAL_PROCESS_EXIT_MISMATCH') + current=self.terminal.snapshot(self.custody) + public={name:{key:value for key,value in row.items() if key!='raw'} for name,row in current.items()} + require(public==message.get('inventory'),'OUTER_INVENTORY_READBACK_CHANGED') + elif result.get('status')=='ALLOCATION_REFUSED' and result.get('terminal_cleanup_complete'): + require(result.get('owner_was_allocated') is False,'ALLOCATION_OWNER_STATE_CHANGED') + if result['allocation_cleanup'].get('exclusive_root_created') is not False: + require(not os.path.lexists(self.expected_pool),'ALLOCATION_ROOT_RETAINED') + elif result.get('retained_forensic_custody_ready'): + require(result.get('owner_process_exited') is True or result.get('owner_was_allocated') is False, + 'RETENTION_OWNER_EXIT_UNPROVEN') + if self.owner_pidfd is not None: + require(select.select([self.owner_pidfd],[],[],0)[0],'RETAINED_OWNER_STILL_LIVE') + elif result.get('owner_was_allocated') is not False: + require(result.get('owner_exit_confirmation')=='reserved-child-waitpid' + and result.get('owner_pidfd_available') is False,'RETENTION_OWNER_HANDLE_UNPROVEN') + require(retained_inventory(self.custody,self.terminal)==message.get('inventory'), + 'OUTER_RETAINED_INVENTORY_CHANGED') + result['retained_tree_inventory_verified']=True + result['outer_owner_custody_required']=True + elif not result.get('terminal_cleanup_complete',False): + result['outer_owner_custody_required']=True;return message + self.finished=True;self.stack.close();return message + else:raise Refusal('UNEXPECTED_SESSION_MESSAGE') + return None + finally: + if not adopted: + for fd in fds:os.close(fd) + + def wait(self): + try: + while not self.finished: + result=self.receive() + if result is not None:return result + except BaseException as error: + code=str(error) if isinstance(error,Refusal) else '' + return self.abort('OUTER_'+(code if re.fullmatch(r'[A-Z0-9_]{1,128}',code) else type(error).__name__.upper())) + + def reconcile_retained(self): + """Custody-only reconciliation, never a retry or late operation acceptance.""" + if self.finished:return None + if self.owner_pidfd is None:return None + if not select.select([self.pidfd],[],[],0)[0] or not select.select([self.owner_pidfd],[],[],0)[0]:return None + result=self.abort('POST_BOUND_CUSTODY_RECONCILIATION') + record=result['result'] + if record.get('retained_tree_inventory_verified'): + self.finished=True;self.stack.close() + record['quiescent_after_failed_bound']=True + record['terminal_cleanup_complete']=False;record['acquisition_accepted']=False + return result + + def abort(self,reason): + """Keep custody if either writer/guardian exit or exact cleanup is unproved.""" + stop_refusal=None + try:send_frame(self.channel,{'type':'ABORT'}) + except BaseException:pass + guardian_gone=bool(select.select([self.pidfd],[],[],max(0,self.ceiling-time.monotonic()))[0]) + owner_gone=self.owner_pidfd is not None and bool(select.select([self.owner_pidfd],[],[],0)[0]) + if guardian_gone and not owner_gone and self.owner_pidfd is not None: + try:signal.pidfd_send_signal(self.owner_pidfd,signal.SIGKILL) + except ProcessLookupError:pass + except OSError as error:stop_refusal='OUTER_PIDFD_STOP_ERRNO_'+str(error.errno) + owner_gone=bool(select.select([self.owner_pidfd],[],[],max(0,min(5,self.ceiling-time.monotonic())))[0]) + if guardian_gone and not self.guardian_reaped: + os.waitpid(self.pid,0);self.guardian_reaped=True + if guardian_gone and owner_gone and self.owner_pidfd is not None: + owner_gone=confirm_reaped_pidfd(self.owner_pidfd,self.ceiling) + if guardian_gone and owner_gone and self.custody is not None: + result=cleanup_after_exit(self.custody,{'owner_exited':True,'owner_exit_code':None},reason,self.ceiling) + if self.authority_raw is not None: + try:record_terminal(self.custody,result,self.authority_raw,'outer-result.json') + except BaseException:result['retained_forensic_custody_ready']=False + if result.get('retained_forensic_custody_ready'): + try: + first=retained_inventory(self.custody,self.terminal) + require(first==retained_inventory(self.custody,self.terminal),'OUTER_RETAINED_INVENTORY_CHANGED') + result['retained_tree_inventory_verified']=True + result['retained_inventory']=first + except BaseException:result['retained_forensic_custody_ready']=False + if result.get('retained_tree_inventory_verified'): + self.finished=True;self.stack.close() + else:result['outer_owner_custody_required']=True + else: + result={'status':'OUTER_CUSTODY_REQUIRED','stop_reason':reason,'terminal_cleanup_complete':False, + 'owner_process_exited':owner_gone if self.owner_pidfd is not None else None, + 'owner_handle_reported':self.owner_pidfd is not None,'guardian_process_exited':guardian_gone, + 'outer_owner_custody_required':True,'acquisition_accepted':False} + if stop_refusal is not None:result['stop_refusal_code']=stop_refusal + result['retention_deadline_utc']=self.retention_deadline_utc + return {'type':'TERMINAL','result':result} + + +def start(authority_path,expected_sha,expected_decision,expected_session_sha,started,started_utc): + """Proposal API only; caller must have the separate exact General admission.""" + verify_startup() + require(PINNED and expected_decision is not None,'UNFROZEN_OR_UNADMITTED_PACKET') + checked_bytes(Path(__file__),expected_session_sha,65536,os.geteuid()) + raw=checked_bytes(Path(authority_path),expected_sha,32768,os.geteuid()) + guard=module('reviewed_guardian','184-acquisition-custody-guardian-v6-proposal.py',PINNED['guardian']) + authority=guard.bind_authority(raw,expected_sha,expected_decision,PINNED) + require(authority.get('survivor_policy')=='retain-custody-and-escalate' + and authority.get('pool_parent')==str(POOL_PARENT) + and authority.get('launcher_sha256')==expected_session_sha + and isinstance(authority.get('cpu_affinity'),list) + and all(type(cpu) is int for cpu in authority['cpu_affinity']) + and set(authority['cpu_affinity'])==os.sched_getaffinity(0),'OUTER_CUSTODY_POLICY_REQUIRED') + require(set(authority)=={'decision','selected_source','inputs','environment','host','total_seconds','io_seconds', + 'retries','survivor_policy','pool_parent','launcher_sha256','cpu_affinity', + 'retention_deadline_utc'},'UNKNOWN_AUTHORITY_FIELD') + verify_retention_deadline(authority['retention_deadline_utc']) + owner=module('reviewed_guarded_owner','184-acquire-tools-v5.py',PINNED['owner']) + bootstrap=module('reviewed_owner_bootstrap','184-acquire-tools-v5-bootstrap.py',PINNED['bootstrap']) + terminal=module('reviewed_terminal','184-acquisition-terminal-proposal.py',PINNED['terminal']) + require(bootstrap.OWNER_SHA==PINNED['owner'],'BOOTSTRAP_OWNER_AUTHORITY_CHANGED') + require(owner.CA_SHA==PINNED['ca'] and owner.SELECTION_SHA==PINNED['selection'] + and owner.SOURCE==SOURCE,'OWNER_DECLARED_INPUT_AUTHORITY_CHANGED') + selection_raw=checked_bytes(owner.SELECTION,owner.SELECTION_SHA,32768,os.geteuid()) + selection=json.loads(selection_raw,object_pairs_hook=pairs) + checked_bytes(owner.CA_SOURCE,owner.CA_SHA,300000,os.geteuid()) + require(type(started) in (int,float) and 0 None: + self.code = re.sub(r'[^A-Za-z0-9]+', '_', message).strip('_').upper() + if not self.code or len(self.code) > 128: + raise ValueError('invalid authored refusal code') + super().__init__(self.code) + + +def require(value: bool, message: str) -> None: + if not value: + raise Refusal(message) + + +MARKER_ENV = { + 'python_version': '3.13', 'python_full_version': '3.13.0', 'implementation_version': '3.13.0', + 'implementation_name': 'cpython', 'platform_python_implementation': 'CPython', + 'os_name': 'posix', 'sys_platform': 'linux', 'platform_system': 'Linux', + 'platform_machine': 'x86_64', 'extra': '', +} +VERSION_MARKERS = {'python_version', 'python_full_version', 'implementation_version'} + + +def marker_tree(text: str): + tree = ast.parse(text.strip(), mode='eval').body + allowed = (ast.BoolOp, ast.And, ast.Or, ast.Compare, ast.Name, ast.Load, ast.Constant, + ast.Eq, ast.NotEq, ast.Lt, ast.LtE, ast.Gt, ast.GtE, ast.In, ast.NotIn) + require(all(isinstance(node, allowed) for node in ast.walk(tree)), 'unsupported marker syntax') + require(all(node.id in MARKER_ENV for node in ast.walk(tree) if isinstance(node, ast.Name)), + 'unsupported marker variable') + require(all(isinstance(node.value, str) for node in ast.walk(tree) if isinstance(node, ast.Constant)), + 'unsupported marker literal') + return tree + + +def requirement_parts(text: str) -> tuple: + """Bounded syntax normalization, not a resolver or execution of marker code.""" + require(isinstance(text, str) and len(text) <= 2048, 'requirement text bound') + body, separator, marker = text.partition(';') + match = re.fullmatch(r'\s*([A-Za-z0-9_.-]+)(?:\[([^\]]+)\])?\s*(.*?)\s*', body) + require(match is not None and '@' not in body, 'unsupported requirement form') + name = re.sub(r'[-_.]+', '-', match[1]).lower() + extras = tuple(sorted(re.sub(r'[-_.]+', '-', value.strip()).lower() + for value in (match[2] or '').split(',') if value.strip())) + spec = match[3].strip() + if spec.startswith('(') and spec.endswith(')'): + spec = spec[1:-1] + specs = tuple(sorted(value.strip().replace(' ', '') for value in spec.split(',') if value.strip())) + parsed_marker = marker_tree(marker) if separator else None + normalized_marker = ast.dump(parsed_marker, include_attributes=False) if parsed_marker is not None else '' + return name, extras, specs, normalized_marker + + +def marker_active(text: str) -> bool: + _, separator, marker = text.partition(';') + if not separator: + return True + tree = marker_tree(marker) + def value(node): + if isinstance(node, ast.Name): + require(node.id in MARKER_ENV, 'unsupported marker variable') + return MARKER_ENV[node.id] + require(isinstance(node, ast.Constant) and isinstance(node.value, str), 'unsupported marker operand') + return node.value + def evaluate(node): + if isinstance(node, ast.BoolOp): + require(isinstance(node.op, (ast.And, ast.Or)), 'unsupported marker boolean') + return all(evaluate(child) for child in node.values) if isinstance(node.op, ast.And) \ + else any(evaluate(child) for child in node.values) + require(isinstance(node, ast.Compare) and len(node.ops) == len(node.comparators) == 1, + 'unsupported marker comparison') + left, right = value(node.left), value(node.comparators[0]) + op = node.ops[0] + version = any(isinstance(part, ast.Name) and part.id in VERSION_MARKERS + for part in (node.left, node.comparators[0])) + if version and not isinstance(op, (ast.In, ast.NotIn)): + require(bool(re.fullmatch(r'\d+(?:\.\d+)*', left)) + and bool(re.fullmatch(r'\d+(?:\.\d+)*', right)), 'unsupported marker version') + first, second = tuple(map(int, left.split('.'))), tuple(map(int, right.split('.'))) + full_version = any(isinstance(part, ast.Name) and part.id in { + 'python_full_version', 'implementation_version', + } for part in (node.left, node.comparators[0])) + if full_version: + literal = node.comparators[0] if isinstance(node.left, ast.Name) else node.left + require(not isinstance(literal, ast.Constant) or not literal.value.startswith('3.13'), + 'patch sensitive marker requires exact runtime version') + length = max(len(first), len(second)) + left, right = first + (0,) * (length - len(first)), second + (0,) * (length - len(second)) + if isinstance(op, ast.Eq): return left == right + if isinstance(op, ast.NotEq): return left != right + if isinstance(op, ast.Lt): return left < right + if isinstance(op, ast.LtE): return left <= right + if isinstance(op, ast.Gt): return left > right + if isinstance(op, ast.GtE): return left >= right + if isinstance(op, ast.In): return left in right + if isinstance(op, ast.NotIn): return left not in right + raise Refusal('unsupported marker operator') + return bool(evaluate(tree)) + + +def sha(raw: bytes) -> str: + return hashlib.sha256(raw).hexdigest() + + +def utc() -> str: + return dt.datetime.now(dt.timezone.utc).isoformat() + + +def encoded(value: object) -> bytes: + return (json.dumps(value, sort_keys=True, indent=2) + '\n').encode() + + +def pairs(values: list[tuple[str, object]]) -> dict: + result = {} + for key, value in values: + require(key not in result, 'duplicate JSON key') + result[key] = value + return result + + +def validate_authority(authority: dict, tool: dict) -> None: + require(isinstance(authority, dict) and isinstance(authority.get('info'), dict) + and isinstance(authority.get('urls'), list) and len(authority['urls']) <= 16, + 'versioned metadata structure changed') + info = authority['info'] + normalize = lambda text: re.sub(r'[-_.]+', '-', text).lower() + require(isinstance(info.get('name'), str) and normalize(info['name']) == normalize(tool['name']) + and info.get('version') == tool['version'] and info.get('requires_python') == tool['requires_python'], + 'versioned metadata identity changed') + require(all(isinstance(row, dict) for row in authority['urls']), 'versioned metadata file shape') + matches = [row for row in authority['urls'] if row.get('filename') == tool['filename']] + require(len(matches) == 1, 'versioned metadata archive cardinality') + row = matches[0] + require(row.get('url') == tool['url'] and type(row.get('size')) is int and row['size'] == tool['bytes'] + and isinstance(row.get('digests'), dict) and row['digests'].get('sha256') == tool['sha256'] + and type(row.get('yanked')) is bool and row['yanked'] is False, + 'versioned archive tuple yank state changed') + + +def read_regular(path: Path, cap: int) -> bytes: + fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK) + with os.fdopen(fd, 'rb') as stream: + before = os.fstat(stream.fileno()) + require(stat.S_ISREG(before.st_mode) and before.st_uid == os.geteuid() + and before.st_size <= cap, 'invalid existing data file') + raw = stream.read(cap + 1) + after = os.fstat(stream.fileno()) + require((before.st_dev, before.st_ino, before.st_size, before.st_mtime_ns, before.st_ctime_ns) + == (after.st_dev, after.st_ino, after.st_size, after.st_mtime_ns, after.st_ctime_ns), + 'existing data changed while reading') + require(len(raw) == before.st_size, 'existing data size mismatch') + return raw + + +class NoRedirect(urllib.request.HTTPRedirectHandler): + def redirect_request(self, req, fp, code, msg, headers, newurl): + refusal = Refusal('redirect refused no second request') + refusal.http_status = code + raise refusal + + +class Acquisition: + def __init__(self, admission: str) -> None: + require(bool(re.fullmatch(r'https://github\.com/durable-workflow/sdk-python/issues/184#issuecomment-[1-9][0-9]{5,17}', admission)) + and admission != OLD_ADMISSION, 'new admission required old attempt cannot retry') + self.admission = admission + self.started = time.monotonic() + self.started_utc = utc() + self.io_deadline = self.started + 270 + self.ceiling = self.started + 300 + self.pool = POOL_BASE / ('sdk-python-184-tools-v2-14adc40b-admission-' + admission.rsplit('-', 1)[-1]) + self.created = False + self.used = 0 + self.archive_used = 0 + self.requests: list[dict] = [] + self.files: set[str] = set() + self.written_hashes: dict[str, str] = {} + self.dir_ids: dict[str, tuple[int, int]] = {} + self.phase = 'selection-and-existing-trust' + + def write(self, name: str, raw: bytes) -> None: + require(name not in self.files and len(raw) + self.used <= POOL_CAP - (0 if name == 'failure.json' else 4096), + 'pool output bound or collision') + parts = PurePosixPath(name) + require(not parts.is_absolute() and '..' not in parts.parts and '\\' not in name + and str(parts) == name and len(parts.parts) in (1, 2) + and (len(parts.parts) == 1 or parts.parts[0] in {'metadata', 'wheelhouse', 'events'}), + 'invalid output path') + with ExitStack() as stack: + root_fd = os.open(self.pool, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) + stack.callback(os.close, root_fd) + current = os.fstat(root_fd) + require((current.st_dev, current.st_ino) == self.dir_ids.get('.'), 'pool directory changed') + parent_fd = root_fd + if len(parts.parts) == 2: + parent_fd = os.open(parts.parts[0], os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=root_fd) + stack.callback(os.close, parent_fd) + current = os.fstat(parent_fd) + require((current.st_dev, current.st_ino) == self.dir_ids.get(parts.parts[0]), + 'pool subdirectory changed') + fd = os.open(parts.parts[-1], os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, + 0o600, dir_fd=parent_fd) + stream = stack.enter_context(os.fdopen(fd, 'wb')) + require(stream.write(raw) == len(raw), 'short output write') + stream.flush() + os.fchmod(stream.fileno(), 0o400) + os.fsync(stream.fileno()) + self.used += len(raw) + self.files.add(name) + self.written_hashes[name] = sha(raw) + + def check_pool(self) -> list[dict]: + entries = [] + with ExitStack() as stack: + root = os.open(self.pool, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) + stack.callback(os.close, root) + st = os.fstat(root) + require((st.st_dev, st.st_ino) == self.dir_ids.get('.'), 'pool directory changed') + directories = {'metadata', 'wheelhouse', 'events'} + expected_root = directories | {name for name in self.files if '/' not in name} + require(set(os.listdir(root)) == expected_root, 'extra or missing pool output') + for directory in ('.', 'metadata', 'wheelhouse', 'events'): + parent = root + if directory != '.': + parent = os.open(directory, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=root) + stack.callback(os.close, parent) + st = os.fstat(parent) + require((st.st_dev, st.st_ino) == self.dir_ids[directory], 'pool subdirectory changed') + expected = {name for name in self.files if '/' not in name} if directory == '.' else { + name.split('/', 1)[1] for name in self.files if name.startswith(directory + '/')} + if directory != '.': + require(set(os.listdir(parent)) == expected, 'extra or missing pool output') + require(len(expected) <= 128, 'pool file count bound') + for leaf in sorted(expected): + name = leaf if directory == '.' else directory + '/' + leaf + fd = os.open(leaf, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=parent) + with os.fdopen(fd, 'rb') as stream: + st = os.fstat(stream.fileno()) + require(stat.S_ISREG(st.st_mode) and st.st_nlink == 1 and st.st_uid == os.geteuid() + and stat.S_IMODE(st.st_mode) == 0o400 and st.st_size <= POOL_CAP, + 'custody type mode or size mismatch') + raw = stream.read(POOL_CAP + 1) + after = os.fstat(stream.fileno()) + require((st.st_dev, st.st_ino, st.st_size, st.st_mtime_ns, st.st_ctime_ns) + == (after.st_dev, after.st_ino, after.st_size, after.st_mtime_ns, after.st_ctime_ns) + and len(raw) == st.st_size and sha(raw) == self.written_hashes[name], + 'written pool bytes changed') + entries.append({'path': name, 'bytes': len(raw), 'sha256': sha(raw), + 'device': st.st_dev, 'inode': st.st_ino, 'mode': '0400', 'links': st.st_nlink}) + require(sum(row['bytes'] for row in entries) == self.used <= POOL_CAP, 'pool accounting mismatch') + return sorted(entries, key=lambda row: row['path']) + + def seal(self) -> None: + with ExitStack() as stack: + root = os.open(self.pool, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) + stack.callback(os.close, root) + st = os.fstat(root) + require((st.st_dev, st.st_ino) == self.dir_ids.get('.'), 'pool directory changed') + for name in ('metadata', 'wheelhouse', 'events'): + fd = os.open(name, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=root) + stack.callback(os.close, fd) + st = os.fstat(fd) + require((st.st_dev, st.st_ino) == self.dir_ids[name], 'pool subdirectory changed') + os.fchmod(fd, 0o500) + os.fchmod(root, 0o500) + + def get(self, url: str, cap: int, kind: str, size: int | None = None) -> bytes: + require(kind in {'metadata', 'archive'} and 0 < cap <= (JSON_CAP if kind == 'metadata' else FILE_CAP), + 'invalid request kind or bound') + require(kind != 'archive' or type(size) is int and 0 < size <= cap, 'invalid archive request size') + parsed = urllib.parse.urlsplit(url) + allowed = 'pypi.org' if kind == 'metadata' else 'files.pythonhosted.org' + require(parsed.scheme == 'https' and parsed.hostname == allowed and not parsed.username + and not parsed.password and parsed.port is None and not parsed.query and not parsed.fragment, + 'unexpected public authority') + remaining = self.io_deadline - time.monotonic() + require(remaining > 0, 'acquisition I/O deadline') + number = len(self.requests) + 1 + entry = {'number': number, 'kind': kind, 'url': url, 'attempt': 1, 'stage': 'before-open'} + self.requests.append(entry) + self.write(f'events/{number:02d}-request.json', encoded(entry)) + signal.setitimer(signal.ITIMER_REAL, min(20, remaining)) + try: + request = urllib.request.Request(url, headers={ + 'User-Agent': 'sdk-python-source-input/1', 'Accept-Encoding': 'identity', + 'Accept': 'application/json' if kind == 'metadata' else 'application/octet-stream', + }) + with self.opener.open(request, timeout=min(15, remaining)) as response: + entry['stage'] = 'headers-received' + header_length = response.headers.get('Content-Length') + header_encoding = response.headers.get('Content-Encoding', 'identity') + self.write(f'events/{number:02d}-headers.json', encoded({ + 'number': number, 'status': response.status, + 'same_selected_url': response.geturl() == url, + 'content_length': int(header_length) if header_length and header_length.isdecimal() + and len(header_length) < 12 else 'missing-or-invalid', + 'content_encoding': header_encoding if re.fullmatch(r'[A-Za-z0-9_.-]{0,32}', header_encoding) + else 'invalid', + })) + require(response.status == 200 and response.geturl() == url, 'unexpected HTTP result') + require(response.headers.get('Content-Encoding', 'identity') == 'identity', 'encoded body refused') + length = response.headers.get('Content-Length') + if length is not None: + require(length.isdecimal() and int(length) <= cap, 'HTTP body exceeds bound') + if size is not None: + require(length is not None and int(length) == size, 'archive HTTP size changed') + raw = bytearray() + digest = hashlib.sha256() + entry['stage'] = 'body-reading' + while True: + require(time.monotonic() < self.io_deadline, 'acquisition I/O deadline') + chunk = response.read(min(65536, cap + 1 - len(raw))) + if not chunk: + break + raw.extend(chunk) + digest.update(chunk) + entry['bytes_received'] = len(raw) + entry['partial_body_sha256'] = digest.hexdigest() + require(len(raw) <= cap, 'HTTP body exceeds bound') + require(length is None or len(raw) == int(length), 'HTTP body length mismatch') + entry.update(stage='body-complete', bytes_received=len(raw), body_sha256=sha(raw)) + self.write(f'events/{number:02d}-complete.json', encoded(entry)) + return bytes(raw) + except BaseException as error: + code = error.code if isinstance(error, Refusal) else 'EXCEPTION_' + type(error).__name__.upper() + try: + self.write(f'events/{number:02d}-refusal.json', encoded({ + 'number': number, 'stage': entry['stage'], 'bytes_received': entry.get('bytes_received', 0), + 'refusal_code': code, + 'http_status': getattr(error, 'http_status', None) + if isinstance(getattr(error, 'http_status', None), int) + else getattr(error, 'code', None) if type(getattr(error, 'code', None)) is int else None, + })) + except BaseException: + entry['diagnostic_write_failed'] = True + raise + finally: + signal.setitimer(signal.ITIMER_REAL, 0) + + def cached(self, tool: dict) -> tuple[bytes | None, str | None]: + roots = sorted([*Path('/tmp').glob('sdk-python-184-tools*14adc40b-*'), + *POOL_BASE.glob('sdk-python-184-tools*14adc40b-*')]) + require(len(roots) <= 32, 'named cache search bound') + for root in roots: + if root == self.pool: + continue + require(not root.is_symlink() and root.is_dir() and root.stat().st_uid == os.geteuid(), + 'named cache ownership collision') + directory = root / 'wheelhouse' + require(not directory.is_symlink(), 'named cache directory symlink') + path = directory / tool['filename'] + if not path.exists() and not path.is_symlink(): + continue + raw = read_regular(path, FILE_CAP) + require(len(raw) == tool['bytes'] and sha(raw) == tool['sha256'], 'named cache tuple mismatch') + return raw, str(path) + return None, None + + def inspect_wheel(self, raw: bytes, tool: dict, authority: dict) -> dict: + with zipfile.ZipFile(io.BytesIO(raw)) as archive: + entries = archive.infolist() + require(0 < len(entries) <= 4096 and sum(row.file_size for row in entries) <= 64 * 1024**2, + 'ZIP member/count size bound') + names: set[str] = set() + for row in entries: + path = PurePosixPath(row.filename) + require(row.filename not in names and not path.is_absolute() and '..' not in path.parts + and str(path) == row.filename.rstrip('/') and not row.filename.endswith('//') + and len(row.filename.encode('utf-8')) <= 512 + and '\\' not in row.filename and ':' not in row.filename + and not any(ord(c) < 32 for c in row.filename) + and not row.flag_bits & 1, 'unsafe ZIP member') + mode = stat.S_IFMT(row.external_attr >> 16) + require(mode in (0, stat.S_IFDIR) if row.is_dir() else mode in (0, stat.S_IFREG), + 'ZIP special/link member') + names.add(row.filename) + metadata = [name for name in names if name.count('/') == 1 and name.endswith('.dist-info/METADATA')] + wheel = [name for name in names if name.count('/') == 1 and name.endswith('.dist-info/WHEEL')] + require(len(metadata) == len(wheel) == 1, 'ambiguous wheel metadata') + require(metadata[0].rsplit('/', 1)[0] == wheel[0].rsplit('/', 1)[0], 'wheel metadata root mismatch') + for name in metadata + wheel: + require(archive.getinfo(name).file_size <= ZIP_MEMBER_CAP, 'wheel metadata size bound') + msg = email.parser.BytesParser().parsebytes(archive.read(metadata[0])) + whl = email.parser.BytesParser().parsebytes(archive.read(wheel[0])) + require(all(len(msg.get_all(key, [])) == 1 for key in ('Name', 'Version', 'Requires-Python')), + 'wheel critical metadata cardinality') + normalize = lambda value: re.sub(r'[-_.]+', '-', value).lower() + require(normalize(msg['Name']) == normalize(tool['name']) and msg['Version'] == tool['version'], + 'wheel name/version mismatch') + require(msg['Requires-Python'] == tool['requires_python'], 'wheel Python bound mismatch') + actual = sorted(requirement_parts(value) for value in msg.get_all('Requires-Dist', [])) + published = sorted(requirement_parts(value) for value in authority['info'].get('requires_dist') or []) + require(actual == published, 'wheel dependency metadata mismatch') + active = sorted(requirement_parts(value)[:3] for value in msg.get_all('Requires-Dist', []) + if marker_active(value)) + expected_active = sorted(requirement_parts(value)[:3] for value in tool['active_requirements']) + require(active == expected_active, 'approved active dependency graph changed') + require(whl.get_all('Tag', []) == ['py3-none-any'] and whl['Root-Is-Purelib'] == 'true', + 'unexpected tool platform tag') + return {'name': tool['name'], 'version': tool['version'], 'metadata_member': metadata[0], + 'metadata_sha256': sha(archive.read(metadata[0])), 'requires_python': msg['Requires-Python'], + 'requires_dist': msg.get_all('Requires-Dist', []), + 'approved_active_requirements': tool['active_requirements'], + 'wheel_tags': whl.get_all('Tag', []), 'members': len(entries), + 'uncompressed_header_bytes': sum(row.file_size for row in entries), + 'extracted_or_imported': False} + + def retain_and_inspect(self, raw: bytes, tool: dict, authority: dict, cache_source=None) -> dict: + require(0 < len(raw) <= FILE_CAP, 'archive per file bound') + require(len(raw) == tool['bytes'] and sha(raw) == tool['sha256'], 'archive bytes/hash mismatch') + require(self.archive_used + len(raw) <= ARCHIVE_CAP, 'combined archive size bound') + self.write('wheelhouse/' + tool['filename'], raw) + self.archive_used += len(raw) + self.phase = 'archive/' + tool['name'] + '/retained-before-semantic-validation' + inspected = self.inspect_wheel(raw, tool, authority) + inspected['cache_source'] = cache_source + return inspected + + def run(self) -> dict: + require(isinstance(VERIFIED_SOURCE_SHA256, str) + and bool(re.fullmatch(r'[0-9a-f]{64}', VERIFIED_SOURCE_SHA256)), 'verified source bootstrap required') + selection_raw = read_regular(SELECTION, 32768) + require(sha(selection_raw) == SELECTION_SHA, 'selected tuple source changed') + selection = json.loads(selection_raw, object_pairs_hook=pairs) + require(selection['source'] == SOURCE and len(selection['tools']) == 6, 'wrong selected source/tool set') + tools = selection['tools'] + require(sum(tool['bytes'] for tool in tools) == ARCHIVE_BYTES <= ARCHIVE_CAP, + 'selected total archive size changed') + require(all(0 < tool['bytes'] <= FILE_CAP and Path(tool['filename']).name == tool['filename'] + for tool in tools), 'selected archive bounds') + ca = read_regular(CA_SOURCE, 300000) + require(sha(ca) == CA_SHA, 'existing clean CA provenance changed') + certs = re.findall(rb'-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----', ca, re.S) + require(len(certs) == 118, 'existing clean CA count changed') + for cert in certs: + check = subprocess.run(['/usr/bin/openssl', 'x509', '-noout', '-subject', '-nameopt', 'RFC2253'], + input=cert, capture_output=True, timeout=2, check=True) + subject = check.stdout.decode().removeprefix('subject=').strip() + require(not any(re.search(r'(?:^|[,+])CN=' + re.escape(name) + r'(?:$|[,+])', subject) + for name in BLOCKED), 'blocked CA subject') + context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + context.minimum_version = ssl.TLSVersion.TLSv1_2 + context.load_verify_locations(cadata=ca.decode('ascii')) + self.opener = urllib.request.build_opener(urllib.request.ProxyHandler({}), + urllib.request.HTTPSHandler(context=context), NoRedirect()) + require(POOL_BASE.is_dir() and not POOL_BASE.is_symlink() and POOL_BASE.stat().st_uid == os.geteuid(), + 'persistent pool parent unavailable') + os.mkdir(self.pool, 0o700) + self.created = True + os.mkdir(self.pool / 'metadata', 0o700) + os.mkdir(self.pool / 'wheelhouse', 0o700) + os.mkdir(self.pool / 'events', 0o700) + for name in ('.', 'metadata', 'wheelhouse', 'events'): + st = (self.pool if name == '.' else self.pool / name).lstat() + require(stat.S_ISDIR(st.st_mode) and st.st_uid == os.geteuid(), 'pool directory ownership') + self.dir_ids[name] = (st.st_dev, st.st_ino) + self.write('selection.json', selection_raw) + self.write('ca.pem', ca) + self.write('launch.json', encoded({'started_at_utc': self.started_utc, 'owner_pid': os.getpid(), + 'pool': str(self.pool), 'admission': self.admission, 'source': SOURCE, + 'verified_acquisition_source_sha256': VERIFIED_SOURCE_SHA256, + 'io_seconds': 270, 'total_seconds': 300, + 'hard_cutoff_utc': (dt.datetime.fromisoformat(self.started_utc) + dt.timedelta(seconds=300)).isoformat(), + 'ca_sha256': CA_SHA, 'ca_certificates': 118, 'blocked_CA_subjects': 0, + 'CA_provenance': {'existing_pinned_wheel_sha256': CA_WHEEL_SHA, + 'sealed_original_input': '/Users/lab3/fleet/tmp/sdk-python-2150-connected-v1-once-01a1a5ef/inputs/wheelhouse/certifi-2026.10.23-py3-none-any.whl', + 'member': 'certifi/cacert.pem', 'member_sha256': CA_SHA}, + 'cache_policy': 'byte-verify existing regular named SDK archive or one request per missing archive', + 'credentials_proxy_default_trust': False})) + print(json.dumps({'phase': 'launched', 'pool': str(self.pool), 'started_at_utc': self.started_utc}), flush=True) + authorities = {} + self.phase = 'official-versioned-metadata-recheck' + for tool in tools: + self.phase = 'metadata/' + tool['name'] + '/request' + raw = self.get(tool['metadata_source'], JSON_CAP, 'metadata') + self.write('metadata/' + tool['name'] + '.json', raw) + self.phase = 'metadata/' + tool['name'] + '/retained-before-parse' + authority = json.loads(raw, object_pairs_hook=pairs) + validate_authority(authority, tool) + authorities[tool['name']] = authority + actual_metadata = [] + self.phase = 'six-exact-archive-requests' + for tool in tools: + self.phase = 'archive/' + tool['name'] + '/cache-or-request' + raw, cache_source = self.cached(tool) + if raw is None: + raw = self.get(tool['url'], FILE_CAP, 'archive', tool['bytes']) + actual_metadata.append(self.retain_and_inspect(raw, tool, authorities[tool['name']], cache_source)) + require(self.archive_used == ARCHIVE_BYTES, 'archive total mismatch') + self.write('wheel-metadata.json', encoded({'environment': 'CPython3.13 Linux amd64; no extras', + 'tools': actual_metadata, 'actual_requires_dist_matches_versioned_authority': True, + 'active_dependency_edges_match_approved_metadata_selection': True})) + require(time.monotonic() < self.ceiling, 'total acquisition deadline') + self.phase = 'sealing-custody' + self.write('acquisition-observation.json', encoded({'status': 'collected-before-final-custody-check', 'finished_at_utc': utc(), + 'elapsed_seconds': time.monotonic() - self.started, 'archive_bytes': self.archive_used, + 'http_requests': self.requests, 'archive_requests': sum(row['kind'] == 'archive' for row in self.requests), + 'retries': 0, 'import_install_container_SDK_build': False})) + entries = self.check_pool() + inventory_raw = encoded({'source': SOURCE, 'entries': entries, 'inventory_excludes_self': True}) + self.write('inventory.json', inventory_raw) + self.seal() + self.check_pool() + require(self.used <= POOL_CAP and time.monotonic() < self.ceiling, 'final pool/time bound') + return {'outcome': 'pass', 'pool': str(self.pool), 'source': SOURCE, 'files': len(self.files), + 'whole_pool_bytes': self.used, 'archive_bytes': self.archive_used, + 'inventory_sha256': sha(inventory_raw), 'finished_at_utc': utc(), + 'elapsed_seconds': time.monotonic() - self.started, + 'import_install_container_SDK_build': False} + + +def alarm(signum, frame): + raise TimeoutError('bounded request deadline') + + +def run_once(admission: str) -> None: + signal.signal(signal.SIGALRM, alarm) + owner = Acquisition(admission) + try: + result = owner.run() + except BaseException as error: + signal.setitimer(signal.ITIMER_REAL, 0) + result = {'outcome': 'fail', 'pool': str(owner.pool), 'phase': owner.phase, + 'failure_type': type(error).__name__, 'finished_at_utc': utc(), + 'refusal_code': error.code if isinstance(error, Refusal) + else 'EXCEPTION_' + type(error).__name__.upper(), + 'last_request': owner.requests[-1] if owner.requests else None, + 'elapsed_seconds': time.monotonic() - owner.started, 'retries': 0, + 'import_install_container_SDK_build': False} + if owner.created and owner.pool.is_dir() and time.monotonic() < owner.ceiling: + try: + owner.write('failure.json', encoded(result)) + except BaseException: + pass + try: + owner.seal() + result['custody_seal_complete'] = True + except BaseException: + result['custody_seal_complete'] = False + print(json.dumps(result), flush=True) + sys.exit(1) + print(json.dumps(result), flush=True) + + +if __name__ == '__main__': + raise SystemExit('VERIFIED_SOURCE_BOOTSTRAP_AND_NEW_ADMISSION_REQUIRED; proposal not launched') diff --git a/qualification/issue-184/tool-acquisition/control-inputs/historical-source/v3-bootstrap.source b/qualification/issue-184/tool-acquisition/control-inputs/historical-source/v3-bootstrap.source new file mode 100644 index 0000000..1b65c6c --- /dev/null +++ b/qualification/issue-184/tool-acquisition/control-inputs/historical-source/v3-bootstrap.source @@ -0,0 +1,62 @@ +"""Unexecuted reviewed-source bootstrap; a new General decision is still required. + +For any future admission, these exact bytes are the inline python -I - stdin +bootstrap. The syntax guard below does not itself grant authorization. +""" +from __future__ import annotations + +import argparse +import hashlib +import os +from pathlib import Path +import re +import stat +from types import ModuleType + +OWNER_PATH = Path('/tmp/sdk-python-handoff/184-acquire-tools-v3.py') +OWNER_SHA = '0d25554f8f15409141708073060798478185a1d9990ac0383cf1cf5661f05997' +OLD_ADMISSION = 'https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-6649046802' + + +def require(value: bool, code: str) -> None: + if not value: + raise ValueError(code) + + +def stamp(value: os.stat_result) -> tuple: + return (value.st_dev, value.st_ino, value.st_mode, value.st_nlink, value.st_uid, + value.st_size, value.st_mtime_ns, value.st_ctime_ns) + + +def verified_source(path: Path, expected: str) -> tuple[bytes, tuple]: + descriptor = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK) + with os.fdopen(descriptor, 'rb') as stream: + before = os.fstat(stream.fileno()) + require(stat.S_ISREG(before.st_mode) and before.st_uid == os.geteuid() + and before.st_nlink == 1 and before.st_size <= 65536, 'BOOTSTRAP_SOURCE_TYPE_OR_SIZE') + raw = stream.read(65537) + require(stamp(before) == stamp(os.fstat(stream.fileno())), 'BOOTSTRAP_SOURCE_CHANGED_WHILE_READING') + require(hashlib.sha256(raw).hexdigest() == expected, 'BOOTSTRAP_SOURCE_HASH_MISMATCH') + require(stamp(os.stat(path, follow_symlinks=False)) == stamp(before), 'BOOTSTRAP_SOURCE_PATH_CHANGED') + return raw, stamp(before) + + +def launch(admission: str) -> None: + require(bool(re.fullmatch(r'https://github\.com/durable-workflow/sdk-python/issues/184#issuecomment-[1-9][0-9]{5,17}', admission)) + and admission != OLD_ADMISSION, 'NEW_GENERAL_ADMISSION_REQUIRED_OLD_ATTEMPT_CANNOT_RETRY') + raw, identity = verified_source(OWNER_PATH, OWNER_SHA) + code = compile(raw, '', 'exec') + require(stamp(os.stat(OWNER_PATH, follow_symlinks=False)) == identity, 'BOOTSTRAP_SOURCE_PATH_CHANGED') + owner = ModuleType('sdk_python_verified_data_owner_v3') + owner.__file__ = str(OWNER_PATH) + owner.VERIFIED_SOURCE_SHA256 = OWNER_SHA + exec(code, owner.__dict__) + require(stamp(os.stat(OWNER_PATH, follow_symlinks=False)) == identity, 'BOOTSTRAP_SOURCE_PATH_CHANGED') + owner.run_once(admission) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--new-general-admission', required=True) + args = parser.parse_args() + launch(args.new_general_admission) diff --git a/qualification/issue-184/tool-acquisition/control-inputs/historical-source/v3-owner.source b/qualification/issue-184/tool-acquisition/control-inputs/historical-source/v3-owner.source new file mode 100644 index 0000000..f82f79d --- /dev/null +++ b/qualification/issue-184/tool-acquisition/control-inputs/historical-source/v3-owner.source @@ -0,0 +1,667 @@ +"""One admitted data-only acquisition; never import/install any acquired wheel.""" +from __future__ import annotations + +import ast +from contextlib import ExitStack +import datetime as dt +import email.parser +import hashlib +import io +import json +import os +from pathlib import Path, PurePosixPath +import re +import signal +import ssl +import stat +import subprocess +import sys +import time +import urllib.parse +import urllib.request +import zipfile + +SELECTION = Path('/tmp/sdk-python-handoff/184-immutable-build-tool-input-proposal.json') +SELECTION_SHA = 'e9a7d639325b866569cdab83b81a2949437209a6bca91c881c2e365fdc573a1c' +CA_SOURCE = Path('/home/lab4/fleet/sessions/sdk-python/20261105-010242-eef95b/.venv/lib/python3.14/site-packages/certifi/cacert.pem') +CA_SHA = '2ef52ad56b4803e264045c915e55c236cbfa045b84b81efefc218a19eb44bd2b' +CA_WHEEL_SHA = 'e9b54de76e530bb6cf0ccd036787103acb58fd42ba930ea8bf948ddbaa2f61d1' +SOURCE = '14adc40b00b68cc8a3e1ae5647a885a42f273650' +POOL_BASE = Path('/home/lab4/fleet/tmp') +OLD_ADMISSION = 'https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-6649046802' +VERIFIED_SOURCE_SHA256 = globals().get('VERIFIED_SOURCE_SHA256') +ARCHIVE_BYTES = 2901292 +FILE_CAP, ARCHIVE_CAP, POOL_CAP = 2 * 1024**2, 3 * 1024**2, 4 * 1024**2 +JSON_CAP, ZIP_MEMBER_CAP = 65536, 65536 +BLOCKED = ('AAA Certificate Services', 'emSign ECC Root CA - G3') + + +class Refusal(ValueError): + def __init__(self, message: str) -> None: + self.code = re.sub(r'[^A-Za-z0-9]+', '_', message).strip('_').upper() + if not self.code or len(self.code) > 128: + raise ValueError('invalid authored refusal code') + super().__init__(self.code) + + +def require(value: bool, message: str) -> None: + if not value: + raise Refusal(message) + + +MARKER_ENV = { + 'python_version': '3.13', 'python_full_version': '3.13.0', 'implementation_version': '3.13.0', + 'implementation_name': 'cpython', 'platform_python_implementation': 'CPython', + 'os_name': 'posix', 'sys_platform': 'linux', 'platform_system': 'Linux', + 'platform_machine': 'x86_64', 'extra': '', +} +VERSION_MARKERS = {'python_version', 'python_full_version', 'implementation_version'} + + +def marker_tree(text: str): + tree = ast.parse(text.strip(), mode='eval').body + allowed = (ast.BoolOp, ast.And, ast.Or, ast.Compare, ast.Name, ast.Load, ast.Constant, + ast.Eq, ast.NotEq, ast.Lt, ast.LtE, ast.Gt, ast.GtE, ast.In, ast.NotIn) + require(all(isinstance(node, allowed) for node in ast.walk(tree)), 'unsupported marker syntax') + require(all(node.id in MARKER_ENV for node in ast.walk(tree) if isinstance(node, ast.Name)), + 'unsupported marker variable') + require(all(isinstance(node.value, str) for node in ast.walk(tree) if isinstance(node, ast.Constant)), + 'unsupported marker literal') + return tree + + +def requirement_parts(text: str) -> tuple: + """Bounded syntax normalization, not a resolver or execution of marker code.""" + require(isinstance(text, str) and len(text) <= 2048, 'requirement text bound') + body, separator, marker = text.partition(';') + match = re.fullmatch(r'\s*([A-Za-z0-9_.-]+)(?:\[([^\]]+)\])?\s*(.*?)\s*', body) + require(match is not None and '@' not in body, 'unsupported requirement form') + name = re.sub(r'[-_.]+', '-', match[1]).lower() + extras = tuple(sorted(re.sub(r'[-_.]+', '-', value.strip()).lower() + for value in (match[2] or '').split(',') if value.strip())) + spec = match[3].strip() + if spec.startswith('(') and spec.endswith(')'): + spec = spec[1:-1] + specs = tuple(sorted(value.strip().replace(' ', '') for value in spec.split(',') if value.strip())) + parsed_marker = marker_tree(marker) if separator else None + normalized_marker = ast.dump(parsed_marker, include_attributes=False) if parsed_marker is not None else '' + return name, extras, specs, normalized_marker + + +def marker_active(text: str) -> bool: + _, separator, marker = text.partition(';') + if not separator: + return True + tree = marker_tree(marker) + def value(node): + if isinstance(node, ast.Name): + require(node.id in MARKER_ENV, 'unsupported marker variable') + return MARKER_ENV[node.id] + require(isinstance(node, ast.Constant) and isinstance(node.value, str), 'unsupported marker operand') + return node.value + def evaluate(node): + if isinstance(node, ast.BoolOp): + require(isinstance(node.op, (ast.And, ast.Or)), 'unsupported marker boolean') + return all(evaluate(child) for child in node.values) if isinstance(node.op, ast.And) \ + else any(evaluate(child) for child in node.values) + require(isinstance(node, ast.Compare) and len(node.ops) == len(node.comparators) == 1, + 'unsupported marker comparison') + left, right = value(node.left), value(node.comparators[0]) + op = node.ops[0] + version = any(isinstance(part, ast.Name) and part.id in VERSION_MARKERS + for part in (node.left, node.comparators[0])) + if version and not isinstance(op, (ast.In, ast.NotIn)): + require(bool(re.fullmatch(r'\d+(?:\.\d+)*', left)) + and bool(re.fullmatch(r'\d+(?:\.\d+)*', right)), 'unsupported marker version') + first, second = tuple(map(int, left.split('.'))), tuple(map(int, right.split('.'))) + full_version = any(isinstance(part, ast.Name) and part.id in { + 'python_full_version', 'implementation_version', + } for part in (node.left, node.comparators[0])) + if full_version: + literal = node.comparators[0] if isinstance(node.left, ast.Name) else node.left + require(not isinstance(literal, ast.Constant) or not literal.value.startswith('3.13'), + 'patch sensitive marker requires exact runtime version') + length = max(len(first), len(second)) + left, right = first + (0,) * (length - len(first)), second + (0,) * (length - len(second)) + if isinstance(op, ast.Eq): return left == right + if isinstance(op, ast.NotEq): return left != right + if isinstance(op, ast.Lt): return left < right + if isinstance(op, ast.LtE): return left <= right + if isinstance(op, ast.Gt): return left > right + if isinstance(op, ast.GtE): return left >= right + if isinstance(op, ast.In): return left in right + if isinstance(op, ast.NotIn): return left not in right + raise Refusal('unsupported marker operator') + return bool(evaluate(tree)) + + +def sha(raw: bytes) -> str: + return hashlib.sha256(raw).hexdigest() + + +def utc() -> str: + return dt.datetime.now(dt.timezone.utc).isoformat() + + +def encoded(value: object) -> bytes: + return (json.dumps(value, sort_keys=True, indent=2) + '\n').encode() + + +def pairs(values: list[tuple[str, object]]) -> dict: + result = {} + for key, value in values: + require(key not in result, 'duplicate JSON key') + result[key] = value + return result + + +def validate_authority(authority: dict, tool: dict) -> None: + require(isinstance(authority, dict) and isinstance(authority.get('info'), dict) + and isinstance(authority.get('urls'), list) and len(authority['urls']) <= 16, + 'versioned metadata structure changed') + info = authority['info'] + normalize = lambda text: re.sub(r'[-_.]+', '-', text).lower() + require(isinstance(info.get('name'), str) and normalize(info['name']) == normalize(tool['name']) + and info.get('version') == tool['version'] and info.get('requires_python') == tool['requires_python'], + 'versioned metadata identity changed') + require(all(isinstance(row, dict) for row in authority['urls']), 'versioned metadata file shape') + matches = [row for row in authority['urls'] if row.get('filename') == tool['filename']] + require(len(matches) == 1, 'versioned metadata archive cardinality') + row = matches[0] + require(row.get('url') == tool['url'] and type(row.get('size')) is int and row['size'] == tool['bytes'] + and isinstance(row.get('digests'), dict) and row['digests'].get('sha256') == tool['sha256'] + and type(row.get('yanked')) is bool and row['yanked'] is False, + 'versioned archive tuple yank state changed') + + +def read_regular(path: Path, cap: int) -> bytes: + fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK) + with os.fdopen(fd, 'rb') as stream: + before = os.fstat(stream.fileno()) + require(stat.S_ISREG(before.st_mode) and before.st_uid == os.geteuid() + and before.st_size <= cap, 'invalid existing data file') + raw = stream.read(cap + 1) + after = os.fstat(stream.fileno()) + require((before.st_dev, before.st_ino, before.st_size, before.st_mtime_ns, before.st_ctime_ns) + == (after.st_dev, after.st_ino, after.st_size, after.st_mtime_ns, after.st_ctime_ns), + 'existing data changed while reading') + require(len(raw) == before.st_size, 'existing data size mismatch') + return raw + + +class NoRedirect(urllib.request.HTTPRedirectHandler): + def redirect_request(self, req, fp, code, msg, headers, newurl): + refusal = Refusal('redirect refused no second request') + refusal.http_status = code + raise refusal + + +class Acquisition: + def __init__(self, admission: str) -> None: + require(bool(re.fullmatch(r'https://github\.com/durable-workflow/sdk-python/issues/184#issuecomment-[1-9][0-9]{5,17}', admission)) + and admission != OLD_ADMISSION, 'new admission required old attempt cannot retry') + self.admission = admission + self.started = time.monotonic() + self.started_utc = utc() + self.io_deadline = self.started + 270 + self.ceiling = self.started + 300 + self.pool = POOL_BASE / ('sdk-python-184-tools-v3-14adc40b-admission-' + admission.rsplit('-', 1)[-1]) + self.created = False + self.used = 0 + self.archive_used = 0 + self.requests: list[dict] = [] + self.files: set[str] = set() + self.written_hashes: dict[str, str] = {} + self.written_sizes: dict[str, int] = {} + self.archive_nodes: dict[str, tuple[int, int]] = {} + self.archive_facts: list[dict] = [] + self.archive_opened = self.archive_written = self.archive_removed = 0 + self.current_tool = None + self.dir_ids: dict[str, tuple[int, int]] = {} + self.phase = 'selection-and-existing-trust' + + def write(self, name: str, raw: bytes) -> None: + require(name not in self.files and len(raw) + self.used <= POOL_CAP - (0 if name == 'failure.json' else 65536), + 'pool output bound or collision') + parts = PurePosixPath(name) + require(not parts.is_absolute() and '..' not in parts.parts and '\\' not in name + and str(parts) == name and len(parts.parts) in (1, 2) + and (len(parts.parts) == 1 or parts.parts[0] in {'metadata', 'wheelhouse', 'events'}), + 'invalid output path') + with ExitStack() as stack: + root_fd = os.open(self.pool, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) + stack.callback(os.close, root_fd) + current = os.fstat(root_fd) + require((current.st_dev, current.st_ino) == self.dir_ids.get('.'), 'pool directory changed') + parent_fd = root_fd + if len(parts.parts) == 2: + parent_fd = os.open(parts.parts[0], os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=root_fd) + stack.callback(os.close, parent_fd) + current = os.fstat(parent_fd) + require((current.st_dev, current.st_ino) == self.dir_ids.get(parts.parts[0]), + 'pool subdirectory changed') + fd = os.open(parts.parts[-1], os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, + 0o600, dir_fd=parent_fd) + stream = stack.enter_context(os.fdopen(fd, 'wb')) + if name.startswith('wheelhouse/'): + node = os.fstat(stream.fileno()) + self.archive_nodes[name] = (node.st_dev, node.st_ino) + self.archive_opened += 1 + require(stream.write(raw) == len(raw), 'short output write') + stream.flush() + os.fchmod(stream.fileno(), 0o400) + os.fsync(stream.fileno()) + self.used += len(raw) + self.files.add(name) + self.written_hashes[name] = sha(raw) + self.written_sizes[name] = len(raw) + if name.startswith('wheelhouse/'): + self.archive_written += 1 + + def remove_new_archives(self) -> dict: + """Unlink only archive names exclusively created by this owner, including partial writes.""" + result = {'complete': False, 'removed': 0, 'remaining_owned': len(self.archive_nodes)} + if not self.created: + require(not self.archive_nodes, 'archive tracking without owned pool') + return dict(result, complete=True) + with ExitStack() as stack: + root = os.open(self.pool, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) + stack.callback(os.close, root) + st = os.fstat(root) + require((st.st_dev, st.st_ino) == self.dir_ids.get('.'), 'cleanup pool directory changed') + directory = os.open('wheelhouse', os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=root) + stack.callback(os.close, directory) + st = os.fstat(directory) + require((st.st_dev, st.st_ino) == self.dir_ids.get('wheelhouse'), 'cleanup archive directory changed') + os.fchmod(directory, 0o700) + for name, identity in list(self.archive_nodes.items()): + leaf = PurePosixPath(name).name + st = os.stat(leaf, dir_fd=directory, follow_symlinks=False) + require(stat.S_ISREG(st.st_mode) and st.st_uid == os.geteuid() and st.st_nlink == 1 + and (st.st_dev, st.st_ino) == identity, 'cleanup archive identity collision') + os.unlink(leaf, dir_fd=directory) + del self.archive_nodes[name] + self.used -= self.written_sizes.pop(name, 0) + self.written_hashes.pop(name, None) + self.files.discard(name) + self.archive_removed += 1 + result['removed'] += 1 + self.archive_used = sum(self.written_sizes.get(name, 0) for name in self.archive_nodes) + result['remaining_owned'] = len(self.archive_nodes) + result['complete'] = not self.archive_nodes and not os.listdir(directory) + return result + + def failure_result(self, error: BaseException) -> dict: + """Keep the original failure primary even if cleanup or diagnostic custody also refuses.""" + code = error.code if isinstance(error, Refusal) else 'EXCEPTION_' + type(error).__name__.upper() + result = {'outcome': 'fail', 'pool': str(self.pool), 'phase': self.phase, + 'failure_type': type(error).__name__, 'refusal_code': code, + 'current_tool': self.current_tool, 'finished_at_utc': utc(), + 'http_requests': self.requests, 'archive_facts': self.archive_facts, + 'elapsed_seconds': time.monotonic() - self.started, 'retries': 0, + 'import_install_container_SDK_build': False, + 'failure_record_written': False, 'custody_seal_complete': False} + try: + result['archive_cleanup'] = self.remove_new_archives() + except BaseException as cleanup_error: + result['archive_cleanup'] = {'complete': False, + 'remaining_owned': len(self.archive_nodes), + 'refusal_code': cleanup_error.code if isinstance(cleanup_error, Refusal) + else 'EXCEPTION_' + type(cleanup_error).__name__.upper()} + result['archive_nodes'] = {'opened': self.archive_opened, 'written': self.archive_written, + 'removed': self.archive_removed, 'remaining_owned': len(self.archive_nodes)} + if self.created and time.monotonic() < self.ceiling: + try: + raw = encoded(dict(result, failure_record_written=True)) + require(len(raw) <= 65536, 'failure diagnostic size bound') + self.write('failure.json', raw) + result['failure_record_written'] = True + except BaseException as record_error: + result['failure_record_refusal_code'] = record_error.code if isinstance(record_error, Refusal) \ + else 'EXCEPTION_' + type(record_error).__name__.upper() + try: + self.seal() + result['custody_seal_complete'] = True + except BaseException as seal_error: + result['custody_seal_refusal_code'] = seal_error.code if isinstance(seal_error, Refusal) \ + else 'EXCEPTION_' + type(seal_error).__name__.upper() + return result + + def check_pool(self) -> list[dict]: + entries = [] + with ExitStack() as stack: + root = os.open(self.pool, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) + stack.callback(os.close, root) + st = os.fstat(root) + require((st.st_dev, st.st_ino) == self.dir_ids.get('.'), 'pool directory changed') + directories = {'metadata', 'wheelhouse', 'events'} + expected_root = directories | {name for name in self.files if '/' not in name} + require(set(os.listdir(root)) == expected_root, 'extra or missing pool output') + for directory in ('.', 'metadata', 'wheelhouse', 'events'): + parent = root + if directory != '.': + parent = os.open(directory, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=root) + stack.callback(os.close, parent) + st = os.fstat(parent) + require((st.st_dev, st.st_ino) == self.dir_ids[directory], 'pool subdirectory changed') + expected = {name for name in self.files if '/' not in name} if directory == '.' else { + name.split('/', 1)[1] for name in self.files if name.startswith(directory + '/')} + if directory != '.': + require(set(os.listdir(parent)) == expected, 'extra or missing pool output') + require(len(expected) <= 128, 'pool file count bound') + for leaf in sorted(expected): + name = leaf if directory == '.' else directory + '/' + leaf + fd = os.open(leaf, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=parent) + with os.fdopen(fd, 'rb') as stream: + st = os.fstat(stream.fileno()) + require(stat.S_ISREG(st.st_mode) and st.st_nlink == 1 and st.st_uid == os.geteuid() + and stat.S_IMODE(st.st_mode) == 0o400 and st.st_size <= POOL_CAP, + 'custody type mode or size mismatch') + raw = stream.read(POOL_CAP + 1) + after = os.fstat(stream.fileno()) + require((st.st_dev, st.st_ino, st.st_size, st.st_mtime_ns, st.st_ctime_ns) + == (after.st_dev, after.st_ino, after.st_size, after.st_mtime_ns, after.st_ctime_ns) + and len(raw) == st.st_size and sha(raw) == self.written_hashes[name], + 'written pool bytes changed') + entries.append({'path': name, 'bytes': len(raw), 'sha256': sha(raw), + 'device': st.st_dev, 'inode': st.st_ino, 'mode': '0400', 'links': st.st_nlink}) + require(sum(row['bytes'] for row in entries) == self.used <= POOL_CAP, 'pool accounting mismatch') + return sorted(entries, key=lambda row: row['path']) + + def seal(self) -> None: + with ExitStack() as stack: + root = os.open(self.pool, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) + stack.callback(os.close, root) + st = os.fstat(root) + require((st.st_dev, st.st_ino) == self.dir_ids.get('.'), 'pool directory changed') + for name in ('metadata', 'wheelhouse', 'events'): + fd = os.open(name, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=root) + stack.callback(os.close, fd) + st = os.fstat(fd) + require((st.st_dev, st.st_ino) == self.dir_ids[name], 'pool subdirectory changed') + os.fchmod(fd, 0o500) + os.fchmod(root, 0o500) + + def get(self, url: str, cap: int, kind: str, size: int | None = None) -> bytes: + require(kind in {'metadata', 'archive'} and 0 < cap <= (JSON_CAP if kind == 'metadata' else FILE_CAP), + 'invalid request kind or bound') + require(kind != 'archive' or type(size) is int and 0 < size <= cap, 'invalid archive request size') + parsed = urllib.parse.urlsplit(url) + allowed = 'pypi.org' if kind == 'metadata' else 'files.pythonhosted.org' + require(parsed.scheme == 'https' and parsed.hostname == allowed and not parsed.username + and not parsed.password and parsed.port is None and not parsed.query and not parsed.fragment, + 'unexpected public authority') + remaining = self.io_deadline - time.monotonic() + require(remaining > 0, 'acquisition I/O deadline') + number = len(self.requests) + 1 + entry = {'number': number, 'kind': kind, 'url': url, 'attempt': 1, 'stage': 'before-open'} + self.requests.append(entry) + self.write(f'events/{number:02d}-request.json', encoded(entry)) + signal.setitimer(signal.ITIMER_REAL, min(20, remaining)) + try: + request = urllib.request.Request(url, headers={ + 'User-Agent': 'sdk-python-source-input/1', 'Accept-Encoding': 'identity', + 'Accept': 'application/json' if kind == 'metadata' else 'application/octet-stream', + }) + with self.opener.open(request, timeout=min(15, remaining)) as response: + entry['stage'] = 'headers-received' + header_length = response.headers.get('Content-Length') + header_encoding = response.headers.get('Content-Encoding', 'identity') + self.write(f'events/{number:02d}-headers.json', encoded({ + 'number': number, 'status': response.status, + 'same_selected_url': response.geturl() == url, + 'content_length': int(header_length) if header_length and header_length.isdecimal() + and len(header_length) < 12 else 'missing-or-invalid', + 'content_encoding': header_encoding if re.fullmatch(r'[A-Za-z0-9_.-]{0,32}', header_encoding) + else 'invalid', + })) + require(response.status == 200 and response.geturl() == url, 'unexpected HTTP result') + require(response.headers.get('Content-Encoding', 'identity') == 'identity', 'encoded body refused') + length = response.headers.get('Content-Length') + if length is not None: + require(length.isdecimal() and int(length) <= cap, 'HTTP body exceeds bound') + if size is not None: + require(length is not None and int(length) == size, 'archive HTTP size changed') + raw = bytearray() + digest = hashlib.sha256() + entry['stage'] = 'body-reading' + while True: + require(time.monotonic() < self.io_deadline, 'acquisition I/O deadline') + chunk = response.read(min(65536, cap + 1 - len(raw))) + if not chunk: + break + raw.extend(chunk) + digest.update(chunk) + entry['bytes_received'] = len(raw) + entry['partial_body_sha256'] = digest.hexdigest() + require(len(raw) <= cap, 'HTTP body exceeds bound') + require(length is None or len(raw) == int(length), 'HTTP body length mismatch') + entry.update(stage='body-complete', bytes_received=len(raw), body_sha256=sha(raw)) + self.write(f'events/{number:02d}-complete.json', encoded(entry)) + return bytes(raw) + except BaseException as error: + code = error.code if isinstance(error, Refusal) else 'EXCEPTION_' + type(error).__name__.upper() + try: + self.write(f'events/{number:02d}-refusal.json', encoded({ + 'number': number, 'stage': entry['stage'], 'bytes_received': entry.get('bytes_received', 0), + 'refusal_code': code, + 'http_status': getattr(error, 'http_status', None) + if isinstance(getattr(error, 'http_status', None), int) + else getattr(error, 'code', None) if type(getattr(error, 'code', None)) is int else None, + })) + except BaseException: + entry['diagnostic_write_failed'] = True + raise + finally: + signal.setitimer(signal.ITIMER_REAL, 0) + + def cached(self, tool: dict) -> tuple[bytes | None, str | None]: + roots = sorted([*Path('/tmp').glob('sdk-python-184-tools*14adc40b-*'), + *POOL_BASE.glob('sdk-python-184-tools*14adc40b-*')]) + require(len(roots) <= 32, 'named cache search bound') + for root in roots: + if root == self.pool: + continue + require(not root.is_symlink() and root.is_dir() and root.stat().st_uid == os.geteuid(), + 'named cache ownership collision') + directory = root / 'wheelhouse' + require(not directory.is_symlink(), 'named cache directory symlink') + path = directory / tool['filename'] + if not path.exists() and not path.is_symlink(): + continue + raw = read_regular(path, FILE_CAP) + require(len(raw) == tool['bytes'] and sha(raw) == tool['sha256'], 'named cache tuple mismatch') + return raw, str(path) + return None, None + + def inspect_wheel(self, raw: bytes, tool: dict, authority: dict) -> dict: + with zipfile.ZipFile(io.BytesIO(raw)) as archive: + entries = archive.infolist() + require(0 < len(entries) <= 4096 and sum(row.file_size for row in entries) <= 64 * 1024**2, + 'ZIP member/count size bound') + names: set[str] = set() + for row in entries: + path = PurePosixPath(row.filename) + require(row.filename not in names and not path.is_absolute() and '..' not in path.parts + and str(path) == row.filename.rstrip('/') and not row.filename.endswith('//') + and len(row.filename.encode('utf-8')) <= 512 + and '\\' not in row.filename and ':' not in row.filename + and not any(ord(c) < 32 for c in row.filename) + and not row.flag_bits & 1, 'unsafe ZIP member') + mode = stat.S_IFMT(row.external_attr >> 16) + require(mode in (0, stat.S_IFDIR) if row.is_dir() else mode in (0, stat.S_IFREG), + 'ZIP special/link member') + names.add(row.filename) + metadata = [name for name in names if name.count('/') == 1 and name.endswith('.dist-info/METADATA')] + wheel = [name for name in names if name.count('/') == 1 and name.endswith('.dist-info/WHEEL')] + require(len(metadata) == len(wheel) == 1, 'ambiguous wheel metadata') + require(metadata[0].rsplit('/', 1)[0] == wheel[0].rsplit('/', 1)[0], 'wheel metadata root mismatch') + for name in metadata + wheel: + require(archive.getinfo(name).file_size <= ZIP_MEMBER_CAP, 'wheel metadata size bound') + msg = email.parser.BytesParser().parsebytes(archive.read(metadata[0])) + whl = email.parser.BytesParser().parsebytes(archive.read(wheel[0])) + require(all(len(msg.get_all(key, [])) == 1 for key in ('Name', 'Version', 'Requires-Python')), + 'wheel critical metadata cardinality') + normalize = lambda value: re.sub(r'[-_.]+', '-', value).lower() + require(normalize(msg['Name']) == normalize(tool['name']) and msg['Version'] == tool['version'], + 'wheel name/version mismatch') + require(msg['Requires-Python'] == tool['requires_python'], 'wheel Python bound mismatch') + actual = sorted(requirement_parts(value) for value in msg.get_all('Requires-Dist', [])) + published = sorted(requirement_parts(value) for value in authority['info'].get('requires_dist') or []) + require(actual == published, 'wheel dependency metadata mismatch') + active = sorted(requirement_parts(value)[:3] for value in msg.get_all('Requires-Dist', []) + if marker_active(value)) + expected_active = sorted(requirement_parts(value)[:3] for value in tool['active_requirements']) + require(active == expected_active, 'approved active dependency graph changed') + require(whl.get_all('Tag', []) == ['py3-none-any'] and whl['Root-Is-Purelib'] == 'true', + 'unexpected tool platform tag') + return {'name': tool['name'], 'version': tool['version'], 'metadata_member': metadata[0], + 'metadata_sha256': sha(archive.read(metadata[0])), 'requires_python': msg['Requires-Python'], + 'requires_dist': msg.get_all('Requires-Dist', []), + 'approved_active_requirements': tool['active_requirements'], + 'wheel_tags': whl.get_all('Tag', []), 'members': len(entries), + 'uncompressed_header_bytes': sum(row.file_size for row in entries), + 'extracted_or_imported': False} + + def retain_and_inspect(self, raw: bytes, tool: dict, authority: dict, cache_source=None) -> dict: + facts = {'name': tool['name'], 'filename': tool['filename'], 'expected_size': tool['bytes'], + 'bytes_received': len(raw), 'hash_started': False, 'hash_complete': False, + 'zip_started': False, 'zip_complete': False, 'cache_source': cache_source} + self.archive_facts.append(facts) + require(0 < len(raw) <= FILE_CAP, 'archive per file bound') + facts['hash_started'] = True + facts['hash_matches'] = sha(raw) == tool['sha256'] + facts['hash_complete'] = True + require(len(raw) == tool['bytes'] and facts['hash_matches'], 'archive bytes/hash mismatch') + require(self.archive_used + len(raw) <= ARCHIVE_CAP, 'combined archive size bound') + self.write('wheelhouse/' + tool['filename'], raw) + self.archive_used += len(raw) + self.phase = 'archive/' + tool['name'] + '/retained-before-semantic-validation' + facts['zip_started'] = True + try: + inspected = self.inspect_wheel(raw, tool, authority) + except BaseException: + try: + facts['archive_cleanup'] = self.remove_new_archives() + except BaseException as error: + facts['cleanup_refusal_code'] = error.code if isinstance(error, Refusal) \ + else 'EXCEPTION_' + type(error).__name__.upper() + raise + facts['zip_complete'] = True + inspected['cache_source'] = cache_source + return inspected + + def run(self) -> dict: + require(isinstance(VERIFIED_SOURCE_SHA256, str) + and bool(re.fullmatch(r'[0-9a-f]{64}', VERIFIED_SOURCE_SHA256)), 'verified source bootstrap required') + selection_raw = read_regular(SELECTION, 32768) + require(sha(selection_raw) == SELECTION_SHA, 'selected tuple source changed') + selection = json.loads(selection_raw, object_pairs_hook=pairs) + require(selection['source'] == SOURCE and len(selection['tools']) == 6, 'wrong selected source/tool set') + tools = selection['tools'] + require(sum(tool['bytes'] for tool in tools) == ARCHIVE_BYTES <= ARCHIVE_CAP, + 'selected total archive size changed') + require(all(0 < tool['bytes'] <= FILE_CAP and Path(tool['filename']).name == tool['filename'] + for tool in tools), 'selected archive bounds') + ca = read_regular(CA_SOURCE, 300000) + require(sha(ca) == CA_SHA, 'existing clean CA provenance changed') + certs = re.findall(rb'-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----', ca, re.S) + require(len(certs) == 118, 'existing clean CA count changed') + for cert in certs: + check = subprocess.run(['/usr/bin/openssl', 'x509', '-noout', '-subject', '-nameopt', 'RFC2253'], + input=cert, capture_output=True, timeout=2, check=True) + subject = check.stdout.decode().removeprefix('subject=').strip() + require(not any(re.search(r'(?:^|[,+])CN=' + re.escape(name) + r'(?:$|[,+])', subject) + for name in BLOCKED), 'blocked CA subject') + context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + context.minimum_version = ssl.TLSVersion.TLSv1_2 + context.load_verify_locations(cadata=ca.decode('ascii')) + self.opener = urllib.request.build_opener(urllib.request.ProxyHandler({}), + urllib.request.HTTPSHandler(context=context), NoRedirect()) + require(POOL_BASE.is_dir() and not POOL_BASE.is_symlink() and POOL_BASE.stat().st_uid == os.geteuid(), + 'persistent pool parent unavailable') + os.mkdir(self.pool, 0o700) + self.created = True + os.mkdir(self.pool / 'metadata', 0o700) + os.mkdir(self.pool / 'wheelhouse', 0o700) + os.mkdir(self.pool / 'events', 0o700) + for name in ('.', 'metadata', 'wheelhouse', 'events'): + st = (self.pool if name == '.' else self.pool / name).lstat() + require(stat.S_ISDIR(st.st_mode) and st.st_uid == os.geteuid(), 'pool directory ownership') + self.dir_ids[name] = (st.st_dev, st.st_ino) + self.write('selection.json', selection_raw) + self.write('ca.pem', ca) + self.write('launch.json', encoded({'started_at_utc': self.started_utc, 'owner_pid': os.getpid(), + 'pool': str(self.pool), 'admission': self.admission, 'source': SOURCE, + 'verified_acquisition_source_sha256': VERIFIED_SOURCE_SHA256, + 'io_seconds': 270, 'total_seconds': 300, + 'hard_cutoff_utc': (dt.datetime.fromisoformat(self.started_utc) + dt.timedelta(seconds=300)).isoformat(), + 'ca_sha256': CA_SHA, 'ca_certificates': 118, 'blocked_CA_subjects': 0, + 'CA_provenance': {'existing_pinned_wheel_sha256': CA_WHEEL_SHA, + 'sealed_original_input': '/Users/lab3/fleet/tmp/sdk-python-2150-connected-v1-once-01a1a5ef/inputs/wheelhouse/certifi-2026.10.23-py3-none-any.whl', + 'member': 'certifi/cacert.pem', 'member_sha256': CA_SHA}, + 'cache_policy': 'byte-verify existing regular named SDK archive or one request per missing archive', + 'credentials_proxy_default_trust': False})) + print(json.dumps({'phase': 'launched', 'pool': str(self.pool), 'started_at_utc': self.started_utc}), flush=True) + authorities = {} + self.phase = 'official-versioned-metadata-recheck' + for index, tool in enumerate(tools, 1): + self.current_tool = {'index': index, 'name': tool['name'], 'filename': tool['filename']} + self.phase = 'metadata/' + tool['name'] + '/request' + raw = self.get(tool['metadata_source'], JSON_CAP, 'metadata') + self.write('metadata/' + tool['name'] + '.json', raw) + self.phase = 'metadata/' + tool['name'] + '/retained-before-parse' + authority = json.loads(raw, object_pairs_hook=pairs) + validate_authority(authority, tool) + authorities[tool['name']] = authority + actual_metadata = [] + self.phase = 'six-exact-archive-requests' + for index, tool in enumerate(tools, 1): + self.current_tool = {'index': index, 'name': tool['name'], 'filename': tool['filename']} + self.phase = 'archive/' + tool['name'] + '/cache-or-request' + raw, cache_source = self.cached(tool) + if raw is None: + raw = self.get(tool['url'], FILE_CAP, 'archive', tool['bytes']) + actual_metadata.append(self.retain_and_inspect(raw, tool, authorities[tool['name']], cache_source)) + require(self.archive_used == ARCHIVE_BYTES, 'archive total mismatch') + self.write('wheel-metadata.json', encoded({'environment': 'CPython3.13 Linux amd64; no extras', + 'tools': actual_metadata, 'actual_requires_dist_matches_versioned_authority': True, + 'active_dependency_edges_match_approved_metadata_selection': True})) + require(time.monotonic() < self.ceiling, 'total acquisition deadline') + self.phase = 'sealing-custody' + self.write('acquisition-observation.json', encoded({'status': 'collected-before-final-custody-check', 'finished_at_utc': utc(), + 'elapsed_seconds': time.monotonic() - self.started, 'archive_bytes': self.archive_used, + 'http_requests': self.requests, 'archive_requests': sum(row['kind'] == 'archive' for row in self.requests), + 'retries': 0, 'import_install_container_SDK_build': False})) + entries = self.check_pool() + inventory_raw = encoded({'source': SOURCE, 'entries': entries, 'inventory_excludes_self': True}) + self.write('inventory.json', inventory_raw) + self.seal() + self.check_pool() + require(self.used <= POOL_CAP and time.monotonic() < self.ceiling, 'final pool/time bound') + return {'outcome': 'pass', 'pool': str(self.pool), 'source': SOURCE, 'files': len(self.files), + 'whole_pool_bytes': self.used, 'archive_bytes': self.archive_used, + 'inventory_sha256': sha(inventory_raw), 'finished_at_utc': utc(), + 'elapsed_seconds': time.monotonic() - self.started, + 'import_install_container_SDK_build': False} + + +def alarm(signum, frame): + raise TimeoutError('bounded request deadline') + + +def run_once(admission: str) -> None: + signal.signal(signal.SIGALRM, alarm) + owner = Acquisition(admission) + try: + result = owner.run() + except BaseException as error: + signal.setitimer(signal.ITIMER_REAL, 0) + result = owner.failure_result(error) + print(json.dumps(result), flush=True) + sys.exit(1) + print(json.dumps(result), flush=True) + + +if __name__ == '__main__': + raise SystemExit('VERIFIED_SOURCE_BOOTSTRAP_AND_NEW_ADMISSION_REQUIRED; proposal not launched') diff --git a/qualification/issue-184/tool-acquisition/control-inputs/metadata/build.json b/qualification/issue-184/tool-acquisition/control-inputs/metadata/build.json new file mode 100644 index 0000000..8ce7eab --- /dev/null +++ b/qualification/issue-184/tool-acquisition/control-inputs/metadata/build.json @@ -0,0 +1,41 @@ +{ + "info": { + "name": "build", + "requires_dist": [ + "packaging>=24.0", + "pyproject_hooks", + "colorama; os_name == \"nt\"", + "importlib-metadata>=4.6; python_full_version < \"3.10.2\"", + "tomli>=1.1.0; python_version < \"3.11\"", + "keyring; extra == \"keyring\"", + "uv>=0.1.18; extra == \"uv\"", + "virtualenv>=20.36.1; extra == \"virtualenv\"" + ], + "requires_python": ">=3.10", + "version": "1.6.1" + }, + "urls": [ + { + "digests": { + "blake2b_256": "ad9b9fb3585dabcd73a1b2a6267f63f62649347c9e6d072c9fde365b105abb2c", + "md5": "3e56b7429feb7269a90eb7bee67718d7", + "sha256": "ecd351a4be9d35a9eaaba244a7687143c9c7d4aea6ac964e7e7ddab20cbcf4e7" + }, + "filename": "build-1.6.1-py3-none-any.whl", + "size": 31179, + "url": "https://files.pythonhosted.org/packages/ad/9b/9fb3585dabcd73a1b2a6267f63f62649347c9e6d072c9fde365b105abb2c/build-1.6.1-py3-none-any.whl", + "yanked": false + }, + { + "digests": { + "blake2b_256": "bd674898a44ea4f3f8e213b0954ec0aa0a16971d62a6212d6ea3931e97115b99", + "md5": "56f61c2663493ebe4916b8acf40bb039", + "sha256": "51cc11666391ab6f092070437ac747002ff46f3e4113a3622177ee6b488bfc53" + }, + "filename": "build-1.6.1.tar.gz", + "size": 113427, + "url": "https://files.pythonhosted.org/packages/bd/67/4898a44ea4f3f8e213b0954ec0aa0a16971d62a6212d6ea3931e97115b99/build-1.6.1.tar.gz", + "yanked": false + } + ] +} diff --git a/qualification/issue-184/tool-acquisition/control-inputs/metadata/packaging.json b/qualification/issue-184/tool-acquisition/control-inputs/metadata/packaging.json new file mode 100644 index 0000000..0a84835 --- /dev/null +++ b/qualification/issue-184/tool-acquisition/control-inputs/metadata/packaging.json @@ -0,0 +1,32 @@ +{ + "info": { + "name": "packaging", + "requires_dist": null, + "requires_python": ">=3.9", + "version": "26.3" + }, + "urls": [ + { + "digests": { + "blake2b_256": "6334ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527", + "md5": "e5b3bd8ce52e8a8fe1ab400079171a1e", + "sha256": "d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c" + }, + "filename": "packaging-26.3-py3-none-any.whl", + "size": 129956, + "url": "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", + "yanked": false + }, + { + "digests": { + "blake2b_256": "7dfa3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093", + "md5": "c6809ec56986682ceffe573b8778a1ce", + "sha256": "94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79" + }, + "filename": "packaging-26.3.tar.gz", + "size": 313412, + "url": "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", + "yanked": false + } + ] +} diff --git a/qualification/issue-184/tool-acquisition/control-inputs/metadata/pip.json b/qualification/issue-184/tool-acquisition/control-inputs/metadata/pip.json new file mode 100644 index 0000000..339213d --- /dev/null +++ b/qualification/issue-184/tool-acquisition/control-inputs/metadata/pip.json @@ -0,0 +1,32 @@ +{ + "info": { + "name": "pip", + "requires_dist": null, + "requires_python": ">=3.10", + "version": "26.2.2" + }, + "urls": [ + { + "digests": { + "blake2b_256": "3f063e4420302363fbb71be3c7f35ce042f54bfca330e3e66b3a2e3c4cca579f", + "md5": "5cda60fa8d5701634ddf7635fb262b4b", + "sha256": "fd7f4dfa8190e014013b9f08f3e973f4e4c26ffd179a204895533a7254813a70" + }, + "filename": "pip-26.2.2-py3-none-any.whl", + "size": 1819850, + "url": "https://files.pythonhosted.org/packages/3f/06/3e4420302363fbb71be3c7f35ce042f54bfca330e3e66b3a2e3c4cca579f/pip-26.2.2-py3-none-any.whl", + "yanked": false + }, + { + "digests": { + "blake2b_256": "fae80842d757e483e895e670f655084cfe8b8c72580c6769a76b30b2789a0608", + "md5": "399f3e6053dd52028d2b60384332cea9", + "sha256": "3a8e5c800492787f33a4114ef54444546c340df428974ebba17e0fd47e11af2f" + }, + "filename": "pip-26.2.2.tar.gz", + "size": 1863336, + "url": "https://files.pythonhosted.org/packages/fa/e8/0842d757e483e895e670f655084cfe8b8c72580c6769a76b30b2789a0608/pip-26.2.2.tar.gz", + "yanked": false + } + ] +} diff --git a/qualification/issue-184/tool-acquisition/control-inputs/metadata/pyproject-hooks.json b/qualification/issue-184/tool-acquisition/control-inputs/metadata/pyproject-hooks.json new file mode 100644 index 0000000..0a51108 --- /dev/null +++ b/qualification/issue-184/tool-acquisition/control-inputs/metadata/pyproject-hooks.json @@ -0,0 +1,32 @@ +{ + "info": { + "name": "pyproject-hooks", + "requires_dist": null, + "requires_python": ">=3.8", + "version": "1.3.3" + }, + "urls": [ + { + "digests": { + "blake2b_256": "8511044d1ae1b4ec0d7af88ee5bc91e081be1022533032b906a7bdabdbb60977", + "md5": "ea17801ed7c110435878faf12aefa35a", + "sha256": "5fc53fdac9f7bd63fbcdc868fb5f90b4784d78a53a3d3388cd738b807441a20b" + }, + "filename": "pyproject_hooks-1.3.3-py3-none-any.whl", + "size": 10724, + "url": "https://files.pythonhosted.org/packages/85/11/044d1ae1b4ec0d7af88ee5bc91e081be1022533032b906a7bdabdbb60977/pyproject_hooks-1.3.3-py3-none-any.whl", + "yanked": false + }, + { + "digests": { + "blake2b_256": "6d5df2ddeef4a855a102aaae5e97826a0260007522ab504421b75addfdb1517c", + "md5": "4946aadbb25ac57019c431257e910583", + "sha256": "defda19b854fa0d3bd4f76ea4ddcba8abd7dcfcdd585a6690ade050744fc5f43" + }, + "filename": "pyproject_hooks-1.3.3.tar.gz", + "size": 21013, + "url": "https://files.pythonhosted.org/packages/6d/5d/f2ddeef4a855a102aaae5e97826a0260007522ab504421b75addfdb1517c/pyproject_hooks-1.3.3.tar.gz", + "yanked": false + } + ] +} diff --git a/qualification/issue-184/tool-acquisition/control-inputs/metadata/setuptools.json b/qualification/issue-184/tool-acquisition/control-inputs/metadata/setuptools.json new file mode 100644 index 0000000..1e9bd58 --- /dev/null +++ b/qualification/issue-184/tool-acquisition/control-inputs/metadata/setuptools.json @@ -0,0 +1,83 @@ +{ + "info": { + "name": "setuptools", + "requires_dist": [ + "pytest!=8.1.*,>=6; extra == \"test\"", + "virtualenv>=13.0.0; extra == \"test\"", + "wheel>=0.44.0; extra == \"test\"", + "pip>=19.1; extra == \"test\"", + "packaging>=26.3; extra == \"test\"", + "jaraco.envs>=2.2; extra == \"test\"", + "pytest-xdist>=3; extra == \"test\"", + "jaraco.path>=3.7.2; extra == \"test\"", + "build[virtualenv]>=1.0.3; extra == \"test\"", + "filelock>=3.4.0; extra == \"test\"", + "ini2toml[lite]>=0.14; extra == \"test\"", + "tomli-w>=1.0.0; extra == \"test\"", + "pytest-timeout; extra == \"test\"", + "pytest-perf; sys_platform != \"cygwin\" and extra == \"test\"", + "jaraco.develop>=7.21; (python_version >= \"3.9\" and sys_platform != \"cygwin\") and extra == \"test\"", + "pytest-home>=0.5; extra == \"test\"", + "pytest-subprocess; extra == \"test\"", + "pyproject-hooks!=1.1; extra == \"test\"", + "jaraco.test>=5.5; extra == \"test\"", + "sphinx>=3.5; extra == \"doc\"", + "jaraco.packaging>=9.3; extra == \"doc\"", + "rst.linker>=1.9; extra == \"doc\"", + "furo; extra == \"doc\"", + "sphinx-lint; extra == \"doc\"", + "jaraco.tidelift>=1.4; extra == \"doc\"", + "pygments-github-lexers==0.0.5; extra == \"doc\"", + "sphinx-favicon; extra == \"doc\"", + "sphinx-inline-tabs; extra == \"doc\"", + "sphinx-reredirects; extra == \"doc\"", + "sphinxcontrib-towncrier; extra == \"doc\"", + "sphinx-notfound-page<2,>=1; extra == \"doc\"", + "pyproject-hooks!=1.1; extra == \"doc\"", + "towncrier<24.7; extra == \"doc\"", + "packaging>=26.3; extra == \"core\"", + "more_itertools>=8.8; extra == \"core\"", + "jaraco.text>=3.7; extra == \"core\"", + "importlib_metadata>=6; python_version < \"3.10\" and extra == \"core\"", + "tomli>=2.0.1; python_version < \"3.11\" and extra == \"core\"", + "wheel>=0.43.0; extra == \"core\"", + "jaraco.functools>=4; extra == \"core\"", + "more_itertools; extra == \"core\"", + "pytest-checkdocs>=2.14; extra == \"check\"", + "pytest-ruff>=0.2.1; sys_platform != \"cygwin\" and extra == \"check\"", + "ruff>=0.13.0; sys_platform != \"cygwin\" and extra == \"check\"", + "pytest-cov; extra == \"cover\"", + "pytest-enabler>=3.4; extra == \"enabler\"", + "pytest-mypy>=1.0.1; platform_python_implementation != \"PyPy\" and extra == \"type\"", + "mypy==1.18.*; extra == \"type\"", + "importlib_metadata>=7.0.2; python_version < \"3.10\" and extra == \"type\"", + "jaraco.develop>=7.21; sys_platform != \"cygwin\" and extra == \"type\"" + ], + "requires_python": ">=3.10", + "version": "84.2.1" + }, + "urls": [ + { + "digests": { + "blake2b_256": "ef556dc6d6bb953bdfc52519a0059592c52b6e3b8ad51cf8256dffceb55b0610", + "md5": "d16d3f1f25175cbc466dd5dfe938ac6b", + "sha256": "ca140b45d5325a9478f880e351484f62fef15f336462e449ca0c03c873d589c2" + }, + "filename": "setuptools-84.2.1-py3-none-any.whl", + "size": 876263, + "url": "https://files.pythonhosted.org/packages/ef/55/6dc6d6bb953bdfc52519a0059592c52b6e3b8ad51cf8256dffceb55b0610/setuptools-84.2.1-py3-none-any.whl", + "yanked": false + }, + { + "digests": { + "blake2b_256": "70b3c19426f783614d625699a4b247faf628ea9ab1db228674491767b417f9e1", + "md5": "251cbfc8f83414155e0e595cc4cffdd2", + "sha256": "3ff8b3b5492a0aa66960f4419365a86b84e013070d4313e4d4a6894f5d64d6bc" + }, + "filename": "setuptools-84.2.1.tar.gz", + "size": 1230289, + "url": "https://files.pythonhosted.org/packages/70/b3/c19426f783614d625699a4b247faf628ea9ab1db228674491767b417f9e1/setuptools-84.2.1.tar.gz", + "yanked": false + } + ] +} diff --git a/qualification/issue-184/tool-acquisition/control-inputs/metadata/wheel.json b/qualification/issue-184/tool-acquisition/control-inputs/metadata/wheel.json new file mode 100644 index 0000000..fdc86a7 --- /dev/null +++ b/qualification/issue-184/tool-acquisition/control-inputs/metadata/wheel.json @@ -0,0 +1,34 @@ +{ + "info": { + "name": "wheel", + "requires_dist": [ + "packaging>=24.0" + ], + "requires_python": ">=3.9", + "version": "0.48.0" + }, + "urls": [ + { + "digests": { + "blake2b_256": "2e2969cfbb602cd91690c55d38ba9fe53e6a7e76a6fa647bf38f19c138d25449", + "md5": "75e4bbe15fefadbb91258c5e79b38aa3", + "sha256": "3217dcc807155e45db462d7ef2431f5ddda0d7273b700d05a67b271ceb1287ab" + }, + "filename": "wheel-0.48.0-py3-none-any.whl", + "size": 33320, + "url": "https://files.pythonhosted.org/packages/2e/29/69cfbb602cd91690c55d38ba9fe53e6a7e76a6fa647bf38f19c138d25449/wheel-0.48.0-py3-none-any.whl", + "yanked": false + }, + { + "digests": { + "blake2b_256": "d02050ed6bdf27dec98b568a8ae25dc599f35baa3d9709f9e83fd1edb56b9a90", + "md5": "f668e4885de814378b332eceea04a8a0", + "sha256": "94800765601e9171bf5d58d066e640662842bcedcbab982b2c90787a2c987322" + }, + "filename": "wheel-0.48.0.tar.gz", + "size": 66471, + "url": "https://files.pythonhosted.org/packages/d0/20/50ed6bdf27dec98b568a8ae25dc599f35baa3d9709f9e83fd1edb56b9a90/wheel-0.48.0.tar.gz", + "yanked": false + } + ] +} diff --git a/qualification/issue-184/tool-acquisition/control-inputs/provenance.json b/qualification/issue-184/tool-acquisition/control-inputs/provenance.json new file mode 100644 index 0000000..81898ac --- /dev/null +++ b/qualification/issue-184/tool-acquisition/control-inputs/provenance.json @@ -0,0 +1,63 @@ +{ + "origin": "https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-6649346255", + "original_failed_pool_modified": false, + "records": [ + { + "derived_fixture_bytes": 1482, + "derived_fixture_sha256": "62e815fa9001a01e6a0457352003698d07256ec1b2fc55d5cb783b3a16f27d26", + "original_record_bytes": 7720, + "original_record_sha256": "064e9dcb80e948b5de7a841c3fd9b5644a980eb27bb644fb7f562b7d773b157b", + "tool": "build" + }, + { + "derived_fixture_bytes": 1162, + "derived_fixture_sha256": "e48bbd4c46efbf4eff638602526c3a8f2fbb1e8193d4df86a76f3df0fb258b1b", + "original_record_bytes": 5748, + "original_record_sha256": "4f190d2f0b312b3086058cee64ab4333bf3ce67fb78b4c2bdb43899049b29f4d", + "tool": "packaging" + }, + { + "derived_fixture_bytes": 1145, + "derived_fixture_sha256": "586b7008e21ba663b15a5af3c531bc3c0dfc25d43e228100cbb70a9524241ff3", + "original_record_bytes": 6683, + "original_record_sha256": "d0b07879e414dc3347bf20a492a9e8a15cfb1962e3cae3843b93453d9940f5f9", + "tool": "pip" + }, + { + "derived_fixture_bytes": 1195, + "derived_fixture_sha256": "77baa40515e7d727d63c7b33d5cce32ad506039aacd255bcb12ab978d69a893b", + "original_record_bytes": 3545, + "original_record_sha256": "ef47170935f8a098c070a3cd65d637d5bbf646158d9559eebac9c465b83d0401", + "tool": "pyproject-hooks" + }, + { + "derived_fixture_bytes": 3785, + "derived_fixture_sha256": "82f7804d38814b9a27a9cb0dbaf3ad70fa57e990cd3dc6486abb654afae40ccb", + "original_record_bytes": 8162, + "original_record_sha256": "d75de8ba0d3304a3d74484fb9f340edb87ef02712da4fa02a2ed92e71b0572dd", + "tool": "setuptools" + }, + { + "derived_fixture_bytes": 1177, + "derived_fixture_sha256": "cf6a86abed869134225400ca2dc4f5b2974c40cff2bbb768049e26709e4ec647", + "original_record_bytes": 4390, + "original_record_sha256": "03f655da3a6dd010d9ac41d4771d5d973109cb662a1e10eb9d967535ffd145f8", + "tool": "wheel" + } + ], + "retained_fields": [ + "info.name", + "info.version", + "info.requires_python", + "info.requires_dist", + "urls.filename", + "urls.url", + "urls.size", + "urls.digests", + "urls.yanked" + ], + "scope": "derived semantic source-control fixtures, not original HTTP records or qualified archives", + "selected_archive_retrieval": false, + "selection_sha256": "e9a7d639325b866569cdab83b81a2949437209a6bca91c881c2e365fdc573a1c", + "source_acquisition_classification": "NONPASSING; exact original failure cause unproved" +} diff --git a/qualification/issue-184/tool-acquisition/control-inputs/selection.json b/qualification/issue-184/tool-acquisition/control-inputs/selection.json new file mode 100644 index 0000000..0ea2f0f --- /dev/null +++ b/qualification/issue-184/tool-acquisition/control-inputs/selection.json @@ -0,0 +1,99 @@ +{ + "checked_at_utc": "2026-11-05T03:02:05.184104+00:00", + "environment": "CPython3.13 Linux amd64, no extras", + "exact_build_owner_bootstrap_and_deadlines": null, + "new_SDK_build": false, + "new_tool_bundle_cache_and_hashes": null, + "pip_authority": "durable-workflow/.github bootstrap-patched-pip@75587fc33ff40c34f32ef777293dad56bb6f48c7", + "requires_before_any_build": [ + "Security exact source and tool-input review", + "General distinct artifact decision", + "download and byte-verify exact public tool wheel inputs into a new exclusive pool without changing old stage", + "review bounded offline build owner/bootstrap/trust source; no in-build dependency solve or network", + "new source/output inventory and package/RECORD proof" + ], + "runtime_image": "python@sha256:ae14d4ce8771606477a81446e7ef429edd6762305fc50c84507c1041fa8285d1", + "selected_tool_dependency_graph_closed_by_metadata": true, + "source": "14adc40b00b68cc8a3e1ae5647a885a42f273650", + "status": "immutable-tool-input-metadata-proposal-not-installed-or-build-admission", + "tools": [ + { + "active_requirements": [ + "packaging>=24.0", + "pyproject_hooks" + ], + "bytes": 31179, + "cached_bytes_verified": false, + "filename": "build-1.6.1-py3-none-any.whl", + "metadata_source": "https://pypi.org/pypi/build/1.6.1/json", + "name": "build", + "requires_python": ">=3.10", + "sha256": "ecd351a4be9d35a9eaaba244a7687143c9c7d4aea6ac964e7e7ddab20cbcf4e7", + "url": "https://files.pythonhosted.org/packages/ad/9b/9fb3585dabcd73a1b2a6267f63f62649347c9e6d072c9fde365b105abb2c/build-1.6.1-py3-none-any.whl", + "version": "1.6.1" + }, + { + "active_requirements": [], + "bytes": 129956, + "cached_bytes_verified": false, + "filename": "packaging-26.3-py3-none-any.whl", + "metadata_source": "https://pypi.org/pypi/packaging/26.3/json", + "name": "packaging", + "requires_python": ">=3.9", + "sha256": "d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", + "url": "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", + "version": "26.3" + }, + { + "active_requirements": [], + "bytes": 1819850, + "cached_bytes_verified": false, + "filename": "pip-26.2.2-py3-none-any.whl", + "metadata_source": "https://pypi.org/pypi/pip/26.2.2/json", + "name": "pip", + "requires_python": ">=3.10", + "sha256": "fd7f4dfa8190e014013b9f08f3e973f4e4c26ffd179a204895533a7254813a70", + "url": "https://files.pythonhosted.org/packages/3f/06/3e4420302363fbb71be3c7f35ce042f54bfca330e3e66b3a2e3c4cca579f/pip-26.2.2-py3-none-any.whl", + "version": "26.2.2" + }, + { + "active_requirements": [], + "bytes": 10724, + "cached_bytes_verified": false, + "filename": "pyproject_hooks-1.3.3-py3-none-any.whl", + "metadata_source": "https://pypi.org/pypi/pyproject-hooks/1.3.3/json", + "name": "pyproject-hooks", + "requires_python": ">=3.8", + "sha256": "5fc53fdac9f7bd63fbcdc868fb5f90b4784d78a53a3d3388cd738b807441a20b", + "url": "https://files.pythonhosted.org/packages/85/11/044d1ae1b4ec0d7af88ee5bc91e081be1022533032b906a7bdabdbb60977/pyproject_hooks-1.3.3-py3-none-any.whl", + "version": "1.3.3" + }, + { + "active_requirements": [], + "bytes": 876263, + "cached_bytes_verified": false, + "filename": "setuptools-84.2.1-py3-none-any.whl", + "metadata_source": "https://pypi.org/pypi/setuptools/84.2.1/json", + "name": "setuptools", + "requires_python": ">=3.10", + "sha256": "ca140b45d5325a9478f880e351484f62fef15f336462e449ca0c03c873d589c2", + "url": "https://files.pythonhosted.org/packages/ef/55/6dc6d6bb953bdfc52519a0059592c52b6e3b8ad51cf8256dffceb55b0610/setuptools-84.2.1-py3-none-any.whl", + "version": "84.2.1" + }, + { + "active_requirements": [ + "packaging>=24.0" + ], + "bytes": 33320, + "cached_bytes_verified": false, + "filename": "wheel-0.48.0-py3-none-any.whl", + "metadata_source": "https://pypi.org/pypi/wheel/0.48.0/json", + "name": "wheel", + "requires_python": ">=3.9", + "sha256": "3217dcc807155e45db462d7ef2431f5ddda0d7273b700d05a67b271ceb1287ab", + "url": "https://files.pythonhosted.org/packages/2e/29/69cfbb602cd91690c55d38ba9fe53e6a7e76a6fa647bf38f19c138d25449/wheel-0.48.0-py3-none-any.whl", + "version": "0.48.0" + } + ], + "wheel_bytes_retrieved_or_installed": false +} diff --git a/qualification/issue-184/tool-acquisition/review-packet.json b/qualification/issue-184/tool-acquisition/review-packet.json new file mode 100644 index 0000000..126ade5 --- /dev/null +++ b/qualification/issue-184/tool-acquisition/review-packet.json @@ -0,0 +1,176 @@ +{ + "actual_authority_sha256": null, + "actual_selected_archive_pool": null, + "all_source_control_processes_expected_absent": true, + "all_source_control_processes_verified_absent": true, + "combined_archive_cap": 3145728, + "connected_retry_admission": null, + "cpu_affinity": null, + "downloaded_wheel_imports_or_installs": 0, + "frozen_predecessor_control_result": { + "cases": 53, + "passed": 53, + "sha256": "30c7d4815ea68410ac839351bab09f10ff9591e9395151d26ce1a917d293b65d" + }, + "frozen_v4_control_result": { + "cases": 62, + "passed": 62, + "sha256": "2c7ee85f4b7b814cab4a9c78c1d150723d86f9cb8b78185644031cd775dc7a73" + }, + "frozen_v5_control_result": { + "cases": 66, + "passed": 66, + "sha256": "3ec6b52657fd172c3db493c8f16df677c473a688f44fcfc5af579b3c9f149394" + }, + "frozen_v6_control_result": { + "cases": 73, + "passed": 73, + "sha256": "f1b504d920d26468dff0f882b6f2e0041529bf3c0f0c4ecb9f88842bd239f4e4" + }, + "frozen_v6_session_result": { + "cases": 33, + "passed": 33, + "sha256": "2ac6e635fa7f8ba76091e85548e98db6d9f4cd1fd0313400702edfdf429c5974" + }, + "guardian_later_findings": { + "issues": [ + "https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-6660476483", + "https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-6662253657", + "https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-6663237183", + "https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-6663491930", + "https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-6663692366", + "https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-6665713217" + ], + "narrow_v6_PASS_not_whole_packet": "https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-6664016731", + "predecessor_v2_sha256": "afc5f198de12d4b26520957bc563ff0122f8535838a1cbc64c40272c5f3f7f70", + "predecessor_v3_sha256": "79ddc82b0095872885aceb943cbf9daa1eb3a05048bc08f9ee9cd093511de187", + "predecessor_v4_sha256": "d52bb389127cb81b5b0303b3a593ee1af74f8c4835793d67f08d0bf182e2101c", + "predecessor_v5_sha256": "4c92d889d89dbef2502ef5e9bc923d00c0314821b42aa77d4c0ee92080aba41e", + "predecessor_v6_sha256": "cbb1672855a0dec48c7d952f02be49ab3c0935d9bd7b1cf467638d73fc543e4c", + "proposed_successor": "guardian-v7/session; gated pre-allocation guardian setup recovery, exact no-owner pre-adoption removal or live keeper, and protected raw-FD ledger insertion; exact whole-source review pending", + "superseded_whole_source_PASS": "https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-6662795537" + }, + "host": "lab4, subject to fresh capacity/input/cache/name checks", + "inline_launcher_bootstrap_sha256": "0cd9d5309b1dd3314e5a7f288f7462c784e2bcf4cea9618eeff4aa401f516aaf", + "interpreter": "/usr/bin/python3.14 CPython3.14.4 root-owned, observed and pinned; host OS/stdlib remain trusted provisioned base", + "new_general_decision": null, + "new_sdk_wheel": null, + "open_files_limits": { + "outer_guardian": 128, + "owner": 64 + }, + "original_connected_classification": "PRODUCT-FAIL, unchanged", + "original_data_acquisition": "NONPASSING, exact actual cause unproved, unchanged", + "original_failed_pool_all_ten_hashes_reverified": true, + "original_frozen_ref": "781c068a0496e8a59fa40bb121eed88bda27cbf1", + "original_wheel_sha256": "a6d137f4584f60a1b47400bbe4843aacca62993106208500165e6c04f8b20d29", + "owner_has_child_launch_surface": false, + "pending_after_review": [ + "Security whole-source/input disposition", + "General exact new data decision and CPU assignment", + "filled authority bytes/hash and exact invocation binding", + "fresh host/input/root/cache and consumed-decision rechecks", + "one admitted invocation and independent terminal actual-pool/role readback" + ], + "per_archive_cap": 2097152, + "pre_adoption_policy": { + "guardian_pidfd_failure": "no START, no allocation; close known handles and stop/reap reserved child or retain setup session", + "known_empty_tree": "remove exact tracked inodes/directories, acknowledge removal, then reap guardian", + "owner_allocated": false, + "post_ARM_unknown_owner": "no no-owner drain or completion inference", + "unknown_or_unproven_tree": "keep exact guardian handles and partial tree; report nonterminal custody" + }, + "proposed_data_scope": "six exact public tool archives only; no solve/substitute/import/install/Docker/SDK build/runtime", + "proposed_entrypoint": "184-acquisition-launcher-bootstrap-proposal.py:run_and_report under exact reviewed inline bytes", + "proposed_invocation": null, + "real_network_requests": 0, + "release_tag_publish_guide_or_adoption": false, + "retained_tree_policy": { + "explicit_new_General_admission_required": true, + "maximum_retention_seconds": 86400, + "operation_retention_deadline": null, + "outer_owner_custody_required": true, + "process_handles_close_only_after_exact_exit_and_full_inventory_readback": true, + "retention_owner": "sdk-python", + "terminal_cleanup_complete": false + }, + "role_address_space_soft_bytes": { + "guardian": 134217728, + "outer": 134217728, + "owner": 402653184 + }, + "runtime_source_inputs": { + "bootstrap": "6ca5da88054b4c3ea27b2614dfd987f88ecb3db981271a995bb9941d06398fed", + "ca": "2ef52ad56b4803e264045c915e55c236cbfa045b84b81efefc218a19eb44bd2b", + "guardian": "032a2da74a26ea8ed036428d5470420dd49f099b7fe61cfcf4d7ab216d720066", + "interpreter": "52e0a13e60a981d8c4b6478be2ba5176f69da07948a056bf49cf6f077e30cb41", + "library": "982cb3768d1d8b00126255ee2785bccf8360c68b197fb3dec95858ab56195f87", + "owner": "234e099b5208aaafca379d3e5535cf44b3158a327764eec0ef4e12afcfc45ffb", + "selection": "e9a7d639325b866569cdab83b81a2949437209a6bca91c881c2e365fdc573a1c", + "terminal": "7e75b0929e612ec8605d095956fd446c2ad779a8f8b7e8403563f2764b1e70bf" + }, + "sdk_build_or_install_admission": null, + "sdk_source_or_feature_cohort_changed": false, + "selected_archive_bytes": 2901292, + "selected_archives_acquired": 0, + "selected_sdk_source": "14adc40b00b68cc8a3e1ae5647a885a42f273650", + "session_sha256": "608c968c38f95774f2cd68aa873b0a978ba48f363aebd86db14cfe61ce541a04", + "source_control_result_total": 284, + "source_control_results": { + "184-acquire-tools-v5-adapter-controls-result.json": { + "cases": 16, + "passed": 16, + "sha256": "fdeaa7cda910e83f057ced455411bbb360e875e8c9163f554d525fab5933c3be" + }, + "184-acquire-tools-v5-controls-result.json": { + "cases": 86, + "passed": 86, + "sha256": "2a84c6612fcd2fd57235f4844face425420dafb3217be7d6e82ef57cc6fab1cd" + }, + "184-acquisition-guardian-v7-controls-result.json": { + "cases": 81, + "passed": 81, + "sha256": "f29e2eda2681715389afd32b4dc0bc3c1f4d48decb352ffcad8195a54db2c228" + }, + "184-acquisition-launcher-bootstrap-controls-result.json": { + "cases": 27, + "passed": 27, + "sha256": "874b1b868975690e56818147c72b1eb7db197c51a961b42da0f1ae00312452b1" + }, + "184-acquisition-session-controls-result.json": { + "cases": 58, + "passed": 58, + "sha256": "95f2abfc9345198e667cfa742fcaadab280e8a1c689a8653abbb10226254d7c3" + }, + "184-acquisition-terminal-controls-result.json": { + "cases": 16, + "passed": 16, + "sha256": "32a30a3767d760befae96f77f906431f234dd2733c28b17c3deadb71921f3be0" + } + }, + "startup_environment": { + "LANG": "C.UTF-8", + "LC_ALL": "C.UTF-8", + "PATH": "/usr/bin:/bin", + "TZ": "UTC" + }, + "startup_flags": [ + "-I", + "-S" + ], + "status": "UNEXECUTED composed source/input packet for independent review; no acquisition admission", + "survivor_policy": { + "acquisition_work_beyond_bound": false, + "custody_keeper_can_outlive_failed_bound": true, + "explicit_new_General_admission_required": true, + "proposal": "retain-custody-and-escalate", + "unknown_handle_or_root_collision": "retain handles/names, no cleanup completion, immediate escalation; operator can stop" + }, + "time_bounds_seconds": { + "io_and_active": 270, + "operation_total": 300, + "setup_counted": true + }, + "tool_selection_sha256": "e9a7d639325b866569cdab83b81a2949437209a6bca91c881c2e365fdc573a1c", + "whole_pool_cap": 4194304 +} diff --git a/qualification/issue-184/tool-acquisition/source-manifest.json b/qualification/issue-184/tool-acquisition/source-manifest.json new file mode 100644 index 0000000..67d5d66 --- /dev/null +++ b/qualification/issue-184/tool-acquisition/source-manifest.json @@ -0,0 +1,334 @@ +{ + "actual_authority_sha256": null, + "actual_selected_archives": null, + "all_source_control_processes_verified_absent": true, + "cpu_affinity": null, + "downloaded_wheel_imports_or_installs": 0, + "files": [ + { + "bytes": 1898, + "file": "184-acquire-tools-v5-adapter-controls-result.json", + "sha256": "fdeaa7cda910e83f057ced455411bbb360e875e8c9163f554d525fab5933c3be" + }, + { + "bytes": 10492, + "file": "184-acquire-tools-v5-adapter-controls.py", + "sha256": "dfd74c20fa379062efca6110f151e5f52824da291f1591858e560cf7e8d09785" + }, + { + "bytes": 2875, + "file": "184-acquire-tools-v5-bootstrap.py", + "sha256": "6ca5da88054b4c3ea27b2614dfd987f88ecb3db981271a995bb9941d06398fed" + }, + { + "bytes": 8301, + "file": "184-acquire-tools-v5-controls-result.json", + "sha256": "2a84c6612fcd2fd57235f4844face425420dafb3217be7d6e82ef57cc6fab1cd" + }, + { + "bytes": 40590, + "file": "184-acquire-tools-v5-controls.py", + "sha256": "61a08e8538b8b7ec7448c6d30a3c929d99d45771411d296e0b6e5b9224e64c59" + }, + { + "bytes": 44676, + "file": "184-acquire-tools-v5.py", + "sha256": "234e099b5208aaafca379d3e5535cf44b3158a327764eec0ef4e12afcfc45ffb" + }, + { + "bytes": 11769, + "file": "184-acquisition-custody-guardian-v2-proposal.py", + "sha256": "afc5f198de12d4b26520957bc563ff0122f8535838a1cbc64c40272c5f3f7f70" + }, + { + "bytes": 19234, + "file": "184-acquisition-custody-guardian-v3-proposal.py", + "sha256": "79ddc82b0095872885aceb943cbf9daa1eb3a05048bc08f9ee9cd093511de187" + }, + { + "bytes": 20980, + "file": "184-acquisition-custody-guardian-v4-proposal.py", + "sha256": "d52bb389127cb81b5b0303b3a593ee1af74f8c4835793d67f08d0bf182e2101c" + }, + { + "bytes": 21371, + "file": "184-acquisition-custody-guardian-v5-proposal.py", + "sha256": "4c92d889d89dbef2502ef5e9bc923d00c0314821b42aa77d4c0ee92080aba41e" + }, + { + "bytes": 22524, + "file": "184-acquisition-custody-guardian-v6-proposal.py", + "sha256": "cbb1672855a0dec48c7d952f02be49ab3c0935d9bd7b1cf467638d73fc543e4c" + }, + { + "bytes": 23306, + "file": "184-acquisition-custody-guardian-v7-proposal.py", + "sha256": "032a2da74a26ea8ed036428d5470420dd49f099b7fe61cfcf4d7ab216d720066" + }, + { + "bytes": 4928, + "file": "184-acquisition-guardian-v3-controls-result.json", + "sha256": "30c7d4815ea68410ac839351bab09f10ff9591e9395151d26ce1a917d293b65d" + }, + { + "bytes": 9789, + "file": "184-acquisition-guardian-v3-controls.py", + "sha256": "9c8aca4ba5092f1d981556207b4eb435bbeed2bb0a32ba763af4c4cb7a510048" + }, + { + "bytes": 5884, + "file": "184-acquisition-guardian-v4-controls-result.json", + "sha256": "2c7ee85f4b7b814cab4a9c78c1d150723d86f9cb8b78185644031cd775dc7a73" + }, + { + "bytes": 14803, + "file": "184-acquisition-guardian-v4-controls.py", + "sha256": "cc7ed2f6cacb830a5a1bc2f10bfa01aa3eff196a03e776591aacdebd58d72cf0" + }, + { + "bytes": 6294, + "file": "184-acquisition-guardian-v5-controls-result.json", + "sha256": "3ec6b52657fd172c3db493c8f16df677c473a688f44fcfc5af579b3c9f149394" + }, + { + "bytes": 18040, + "file": "184-acquisition-guardian-v5-controls.py", + "sha256": "05fe57d28dbe9b921fa250f7752773d6f9a43cf464f8ec73937ddd4c77ab7ad4" + }, + { + "bytes": 6933, + "file": "184-acquisition-guardian-v6-controls-result.json", + "sha256": "f1b504d920d26468dff0f882b6f2e0041529bf3c0f0c4ecb9f88842bd239f4e4" + }, + { + "bytes": 21671, + "file": "184-acquisition-guardian-v6-controls.py", + "sha256": "6f963d6195d3f7244890f0f391a8f7640f4266c9bad7881cda34614f515dd82d" + }, + { + "bytes": 7877, + "file": "184-acquisition-guardian-v7-controls-result.json", + "sha256": "f29e2eda2681715389afd32b4dc0bc3c1f4d48decb352ffcad8195a54db2c228" + }, + { + "bytes": 25028, + "file": "184-acquisition-guardian-v7-controls.py", + "sha256": "3219a930f460460ea1d7ea09366cf0a9fc623574644c53a2c2252f9929f544ff" + }, + { + "bytes": 1010, + "file": "184-acquisition-interpreter-authority-proposal.json", + "sha256": "cb819468f659c6235176fabcd29894dda552ffb85f0bf54664d83b7dcdd3d2e1" + }, + { + "bytes": 2936, + "file": "184-acquisition-launcher-bootstrap-controls-result.json", + "sha256": "874b1b868975690e56818147c72b1eb7db197c51a961b42da0f1ae00312452b1" + }, + { + "bytes": 7807, + "file": "184-acquisition-launcher-bootstrap-controls.py", + "sha256": "bfe3bfae2442aea2f3f9b8a35bb50015b4168e500bb6c1a5e804c286bd901357" + }, + { + "bytes": 7533, + "file": "184-acquisition-launcher-bootstrap-proposal.py", + "sha256": "0cd9d5309b1dd3314e5a7f288f7462c784e2bcf4cea9618eeff4aa401f516aaf" + }, + { + "bytes": 6311, + "file": "184-acquisition-session-controls-result.json", + "sha256": "95f2abfc9345198e667cfa742fcaadab280e8a1c689a8653abbb10226254d7c3" + }, + { + "bytes": 41303, + "file": "184-acquisition-session-controls.py", + "sha256": "4ff34e4c60859619403a414b5d57fc7bf05411c5720abda4010a7484c263a630" + }, + { + "bytes": 52040, + "file": "184-acquisition-session-proposal.py", + "sha256": "608c968c38f95774f2cd68aa873b0a978ba48f363aebd86db14cfe61ce541a04" + }, + { + "bytes": 1826, + "file": "184-acquisition-terminal-controls-result.json", + "sha256": "32a30a3767d760befae96f77f906431f234dd2733c28b17c3deadb71921f3be0" + }, + { + "bytes": 12198, + "file": "184-acquisition-terminal-controls.py", + "sha256": "bf13c83983567ae04e21fe871625817f85b3bef0b3524b85576296e1267e3dff" + }, + { + "bytes": 12215, + "file": "184-acquisition-terminal-proposal.py", + "sha256": "7e75b0929e612ec8605d095956fd446c2ad779a8f8b7e8403563f2764b1e70bf" + }, + { + "bytes": 4550, + "file": "184-immutable-build-tool-input-proposal.json", + "sha256": "e9a7d639325b866569cdab83b81a2949437209a6bca91c881c2e365fdc573a1c" + }, + { + "bytes": 10676, + "file": "README.md", + "sha256": "7b2cf2765620a1fee9d246ca9ed727e2586b36257dd492461e18991b209910c3" + }, + { + "bytes": 1209, + "file": "authority-template.json", + "sha256": "22479d136d6f23e3022100d09ff4dda356e54c3b29208f4d815ed5c099efe560" + }, + { + "bytes": 1520, + "file": "control-inputs/historical-source/manifest.json", + "sha256": "c5d33f28439e92554bd603cb9c677c8ebb42e3fb2ce109cebc3d42f6a63a321c" + }, + { + "bytes": 17804, + "file": "control-inputs/historical-source/original-owner.source", + "sha256": "e0b8955c8c26c68333e1439c4eb195ef803d710d68f4a4b297b3dd17fcfd7c54" + }, + { + "bytes": 41341, + "file": "control-inputs/historical-source/session-v6.source", + "sha256": "e721bba90c8cf7fae9eede4b3a2df2b7abb25b59f84b62f8ee5be5b9ee381e89" + }, + { + "bytes": 34138, + "file": "control-inputs/historical-source/v2-owner.source", + "sha256": "16200705aee523e1171ad9c2c1f8522fa089c3479c18d474fdd5ff6ed7b29ff7" + }, + { + "bytes": 2769, + "file": "control-inputs/historical-source/v3-bootstrap.source", + "sha256": "8797366c75b3f55d61c21c19ac3e98ba0bed5b65bbf0a04e4bf3e2b73d326fcc" + }, + { + "bytes": 39357, + "file": "control-inputs/historical-source/v3-owner.source", + "sha256": "0d25554f8f15409141708073060798478185a1d9990ac0383cf1cf5661f05997" + }, + { + "bytes": 1482, + "file": "control-inputs/metadata/build.json", + "sha256": "62e815fa9001a01e6a0457352003698d07256ec1b2fc55d5cb783b3a16f27d26" + }, + { + "bytes": 1162, + "file": "control-inputs/metadata/packaging.json", + "sha256": "e48bbd4c46efbf4eff638602526c3a8f2fbb1e8193d4df86a76f3df0fb258b1b" + }, + { + "bytes": 1145, + "file": "control-inputs/metadata/pip.json", + "sha256": "586b7008e21ba663b15a5af3c531bc3c0dfc25d43e228100cbb70a9524241ff3" + }, + { + "bytes": 1195, + "file": "control-inputs/metadata/pyproject-hooks.json", + "sha256": "77baa40515e7d727d63c7b33d5cce32ad506039aacd255bcb12ab978d69a893b" + }, + { + "bytes": 3785, + "file": "control-inputs/metadata/setuptools.json", + "sha256": "82f7804d38814b9a27a9cb0dbaf3ad70fa57e990cd3dc6486abb654afae40ccb" + }, + { + "bytes": 1177, + "file": "control-inputs/metadata/wheel.json", + "sha256": "cf6a86abed869134225400ca2dc4f5b2974c40cff2bbb768049e26709e4ec647" + }, + { + "bytes": 2568, + "file": "control-inputs/provenance.json", + "sha256": "52296697e2f155f17bfd5f38ef21c9f3fc1a19c25961a1ed00cb667bc5dcead2" + }, + { + "bytes": 4550, + "file": "control-inputs/selection.json", + "sha256": "e9a7d639325b866569cdab83b81a2949437209a6bca91c881c2e365fdc573a1c" + }, + { + "bytes": 8293, + "file": "review-packet.json", + "sha256": "969ef8c6e1191cbf85a6bd58bbaf131e8c418dc491c98c11518dfb02a48394eb" + } + ], + "guardian_later_findings": { + "issues": [ + "https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-6660476483", + "https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-6662253657", + "https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-6663237183", + "https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-6663491930", + "https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-6663692366", + "https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-6665713217" + ], + "narrow_v6_PASS_not_whole_packet": "https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-6664016731", + "predecessor_v2_sha256": "afc5f198de12d4b26520957bc563ff0122f8535838a1cbc64c40272c5f3f7f70", + "predecessor_v3_sha256": "79ddc82b0095872885aceb943cbf9daa1eb3a05048bc08f9ee9cd093511de187", + "predecessor_v4_sha256": "d52bb389127cb81b5b0303b3a593ee1af74f8c4835793d67f08d0bf182e2101c", + "predecessor_v5_sha256": "4c92d889d89dbef2502ef5e9bc923d00c0314821b42aa77d4c0ee92080aba41e", + "predecessor_v6_sha256": "cbb1672855a0dec48c7d952f02be49ab3c0935d9bd7b1cf467638d73fc543e4c", + "proposed_successor": "guardian-v7/session; gated pre-allocation guardian setup recovery, exact no-owner pre-adoption removal or live keeper, and protected raw-FD ledger insertion; exact whole-source review pending", + "superseded_whole_source_PASS": "https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-6662795537" + }, + "manifest_excludes_self": true, + "new_general_decision": null, + "new_sdk_wheel": null, + "original_failed_pool_all_ten_hashes_unchanged": true, + "real_network_requests": 0, + "remaining_gates": [ + "Security whole-source/input disposition", + "General exact new data decision and CPU assignment", + "filled authority bytes/hash and exact invocation binding", + "fresh host/input/root/cache and consumed-decision rechecks", + "one admitted invocation and independent terminal actual-pool/role readback" + ], + "retained_tree_policy": { + "explicit_new_General_admission_required": true, + "maximum_retention_seconds": 86400, + "operation_retention_deadline": null, + "outer_owner_custody_required": true, + "process_handles_close_only_after_exact_exit_and_full_inventory_readback": true, + "retention_owner": "sdk-python", + "terminal_cleanup_complete": false + }, + "sdk_source_or_feature_cohort_changed": false, + "selected_sdk_source": "14adc40b00b68cc8a3e1ae5647a885a42f273650", + "source_control_result_total": 284, + "source_control_results": { + "184-acquire-tools-v5-adapter-controls-result.json": { + "cases": 16, + "passed": 16, + "sha256": "fdeaa7cda910e83f057ced455411bbb360e875e8c9163f554d525fab5933c3be" + }, + "184-acquire-tools-v5-controls-result.json": { + "cases": 86, + "passed": 86, + "sha256": "2a84c6612fcd2fd57235f4844face425420dafb3217be7d6e82ef57cc6fab1cd" + }, + "184-acquisition-guardian-v7-controls-result.json": { + "cases": 81, + "passed": 81, + "sha256": "f29e2eda2681715389afd32b4dc0bc3c1f4d48decb352ffcad8195a54db2c228" + }, + "184-acquisition-launcher-bootstrap-controls-result.json": { + "cases": 27, + "passed": 27, + "sha256": "874b1b868975690e56818147c72b1eb7db197c51a961b42da0f1ae00312452b1" + }, + "184-acquisition-session-controls-result.json": { + "cases": 58, + "passed": 58, + "sha256": "95f2abfc9345198e667cfa742fcaadab280e8a1c689a8653abbb10226254d7c3" + }, + "184-acquisition-terminal-controls-result.json": { + "cases": 16, + "passed": 16, + "sha256": "32a30a3767d760befae96f77f906431f234dd2733c28b17c3deadb71921f3be0" + } + }, + "stage": "UNEXECUTED guardian-v7/session successor for composed HOLD6665713217; exact whole-source review pending; no operational admission" +}