From 3417f9ddf6b1e5f6214bdb118d7e984b0c1df273 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Thu, 5 Nov 2026 13:30:09 +0000 Subject: [PATCH 1/8] Prepare unexecuted guarded tool acquisition source components --- ...uire-tools-v5-adapter-controls-result.json | 77 ++ .../184-acquire-tools-v5-adapter-controls.py | 194 +++++ .../184-acquire-tools-v5-bootstrap.py | 65 ++ .../184-acquire-tools-v5-controls-result.json | 355 ++++++++ .../184-acquire-tools-v5-controls.py | 789 ++++++++++++++++++ .../tool-acquisition/184-acquire-tools-v5.py | 762 +++++++++++++++++ ...cquisition-custody-guardian-v2-proposal.py | 227 +++++ ...sition-interpreter-authority-proposal.json | 32 + ...-acquisition-terminal-controls-result.json | 78 ++ .../184-acquisition-terminal-controls.py | 183 ++++ .../184-acquisition-terminal-proposal.py | 201 +++++ ...4-immutable-build-tool-input-proposal.json | 99 +++ .../issue-184/tool-acquisition/README.md | 42 + .../tool-acquisition/source-manifest.json | 77 ++ 14 files changed, 3181 insertions(+) create mode 100644 qualification/issue-184/tool-acquisition/184-acquire-tools-v5-adapter-controls-result.json create mode 100644 qualification/issue-184/tool-acquisition/184-acquire-tools-v5-adapter-controls.py create mode 100644 qualification/issue-184/tool-acquisition/184-acquire-tools-v5-bootstrap.py create mode 100644 qualification/issue-184/tool-acquisition/184-acquire-tools-v5-controls-result.json create mode 100644 qualification/issue-184/tool-acquisition/184-acquire-tools-v5-controls.py create mode 100644 qualification/issue-184/tool-acquisition/184-acquire-tools-v5.py create mode 100644 qualification/issue-184/tool-acquisition/184-acquisition-custody-guardian-v2-proposal.py create mode 100644 qualification/issue-184/tool-acquisition/184-acquisition-interpreter-authority-proposal.json create mode 100644 qualification/issue-184/tool-acquisition/184-acquisition-terminal-controls-result.json create mode 100644 qualification/issue-184/tool-acquisition/184-acquisition-terminal-controls.py create mode 100644 qualification/issue-184/tool-acquisition/184-acquisition-terminal-proposal.py create mode 100644 qualification/issue-184/tool-acquisition/184-immutable-build-tool-input-proposal.json create mode 100644 qualification/issue-184/tool-acquisition/README.md create mode 100644 qualification/issue-184/tool-acquisition/source-manifest.json 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..1513f46 --- /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": "afc5f198de12d4b26520957bc563ff0122f8535838a1cbc64c40272c5f3f7f70", + "owner_sha256": "96d4a5b1b5687d11779a244a2a6946555c24020b3b54e93e43e61be2f6722e2c", + "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..3a6be1f --- /dev/null +++ b/qualification/issue-184/tool-acquisition/184-acquire-tools-v5-adapter-controls.py @@ -0,0 +1,194 @@ +"""Synthetic preallocated-pool adapter controls; no acquisition or tool execution.""" +from __future__ import annotations + +from contextlib import contextmanager +from copy import deepcopy +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-v2-proposal.py') +assert hashlib.sha256((BASE/'184-acquisition-custody-guardian-v2-proposal.py').read_bytes()).hexdigest()== \ + 'afc5f198de12d4b26520957bc563ff0122f8535838a1cbc64c40272c5f3f7f70' +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-9999999999',['one.whl','two.whl']) + 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':{'.':list(custody.root_id),'wheelhouse':list(custody.wheelhouse_id)}, + '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 (item.pool/'metadata').exists() + + +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()['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()['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-v2-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} +(BASE/'184-acquire-tools-v5-adapter-controls-result.json').write_text(json.dumps(report,sort_keys=True,indent=2)+'\n') +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..0de1815 --- /dev/null +++ b/qualification/issue-184/tool-acquisition/184-acquire-tools-v5-bootstrap.py @@ -0,0 +1,65 @@ +"""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-v5.py') +OWNER_SHA = '96d4a5b1b5687d11779a244a2a6946555c24020b3b54e93e43e61be2f6722e2c' +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__': + 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/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..74abf7c --- /dev/null +++ b/qualification/issue-184/tool-acquisition/184-acquire-tools-v5-controls-result.json @@ -0,0 +1,355 @@ +{ + "actual_failed_archive_cause_established": false, + "bootstrap_sha256": "00349dc4ff6bec00072bcdfd01fffe5c13730de64f78e8dc940324c755adfe20", + "cases": 85, + "downloaded_wheel_imports_or_installs": 0, + "failed": 0, + "original_source_sha256": "e0b8955c8c26c68333e1439c4eb195ef803d710d68f4a4b297b3dd17fcfd7c54", + "passed": 85, + "real_network_requests": 0, + "results": [ + { + "id": "old-refuses-equivalent-marker-quotes-new-accepts", + "outcome": "pass" + }, + { + "id": "retained-authority-tuple-build", + "outcome": "pass" + }, + { + "id": "authority-derived-synthetic-headers-build", + "outcome": "pass" + }, + { + "id": "retained-authority-tuple-packaging", + "outcome": "pass" + }, + { + "id": "authority-derived-synthetic-headers-packaging", + "outcome": "pass" + }, + { + "id": "retained-authority-tuple-pip", + "outcome": "pass" + }, + { + "id": "authority-derived-synthetic-headers-pip", + "outcome": "pass" + }, + { + "id": "retained-authority-tuple-pyproject-hooks", + "outcome": "pass" + }, + { + "id": "authority-derived-synthetic-headers-pyproject-hooks", + "outcome": "pass" + }, + { + "id": "retained-authority-tuple-setuptools", + "outcome": "pass" + }, + { + "id": "authority-derived-synthetic-headers-setuptools", + "outcome": "pass" + }, + { + "id": "retained-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": "openssl-child-actual-effective-closed-env", + "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": "96d4a5b1b5687d11779a244a2a6946555c24020b3b54e93e43e61be2f6722e2c" +} 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..175706a --- /dev/null +++ b/qualification/issue-184/tool-acquisition/184-acquire-tools-v5-controls.py @@ -0,0 +1,789 @@ +"""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 importlib.util +import io +import json +import os +from pathlib import Path +import socket +import sys +import subprocess +import tempfile +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 = Path('/tmp/sdk-python-184-tools-14adc40b-20261105T035512Z-6d90ccfb71') +NEW_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) + result = importlib.util.module_from_spec(spec) + spec.loader.exec_module(result) + return result + + +old = load('original_acquisition_source_only', BASE / '184-acquire-tools.py') +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((BASE / '184-acquire-tools.py').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', BASE / '184-acquire-tools-v2.py') + 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', BASE / '184-acquire-tools-v3.py') + 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', BASE / '184-acquire-tools-v3-bootstrap.py') + 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 openssl_environment_control(): + real_run = subprocess.run + observed = [] + def child_probe(argv, **kwargs): + assert argv == ['/usr/bin/openssl', 'x509', '-noout', '-subject', '-nameopt', 'RFC2253'] + assert kwargs['env'] == new.MINIMAL_ENV and kwargs['close_fds'] is True + # Exercise the effective child environment with local stdlib only; no tool wheel or CA/network work. + probe = real_run(['/usr/bin/python3', '-I', '-c', + 'import json,os; print(json.dumps(dict(os.environ)))'], + env=kwargs['env'], close_fds=kwargs['close_fds'], capture_output=True, + timeout=2, check=True) + observed.append(json.loads(probe.stdout)) + return subprocess.CompletedProcess(argv, 0, stdout=b'subject=CN=Synthetic Control\n') + with patch.dict(os.environ, SENTINELS, clear=False), patch.object(new.subprocess, 'run', side_effect=child_probe): + assert new.ca_subject(b'synthetic non-PEM control') == 'CN=Synthetic Control' + assert observed == [new.MINIMAL_ENV] + assert not set(SENTINELS) & set(observed[0]) + + +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('retained-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('openssl-child-actual-effective-closed-env', openssl_environment_control) + 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((BASE / '184-acquire-tools.py').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} +(BASE / '184-acquire-tools-v5-controls-result.json').write_text(json.dumps(report, sort_keys=True, indent=2)+'\n') +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..118cb74 --- /dev/null +++ b/qualification/issue-184/tool-acquisition/184-acquire-tools-v5.py @@ -0,0 +1,762 @@ +"""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') +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 ca_subject(cert: bytes) -> str: + check = subprocess.run(['/usr/bin/openssl', 'x509', '-noout', '-subject', '-nameopt', 'RFC2253'], + input=cert, capture_output=True, timeout=2, check=True, + env=dict(MINIMAL_ENV), close_fds=True) + return check.stdout.decode().removeprefix('subject=').strip() + + +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-' + 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) == {'.', 'wheelhouse'}, 'guardian directory proof shape') + for name in ('.', 'wheelhouse'): + 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)) == {'wheelhouse'}, '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'): + os.mkdir(name, 0o700, dir_fd=root) + st = os.stat(name, dir_fd=root, follow_symlinks=False) + 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) + 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) + 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: + subject = ca_subject(cert) + 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') + 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_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-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-terminal-controls-result.json b/qualification/issue-184/tool-acquisition/184-acquisition-terminal-controls-result.json new file mode 100644 index 0000000..3faefef --- /dev/null +++ b/qualification/issue-184/tool-acquisition/184-acquisition-terminal-controls-result.json @@ -0,0 +1,78 @@ +{ + "cases": 16, + "downloaded_wheel_imports_or_installs": 0, + "failed": 0, + "launcher_pending": true, + "new_general_decision": null, + "passed": 16, + "real_network_requests": 0, + "results": [ + { + "id": "six-synthetic-header-tuples-semantic-and-ledger-readback", + "outcome": "pass" + }, + { + "id": "live-owner-cannot-enter-readback", + "outcome": "pass" + }, + { + "id": "failed-owner-cannot-preserve", + "outcome": "pass" + }, + { + "id": "bool-exitcode-cannot-masquerade-as-zero", + "outcome": "pass" + }, + { + "id": "expired-terminal-ceiling-refused", + "outcome": "pass" + }, + { + "id": "owner-pass-marker-not-independent-candidate", + "outcome": "pass" + }, + { + "id": "candidate-file-count-mismatch-refused", + "outcome": "pass" + }, + { + "id": "candidate-byte-count-mismatch-refused", + "outcome": "pass" + }, + { + "id": "candidate-inventory-hash-mismatch-refused", + "outcome": "pass" + }, + { + "id": "changed-selected-source-bytes-refused", + "outcome": "pass" + }, + { + "id": "changed-trust-file-refused", + "outcome": "pass" + }, + { + "id": "changed-launch-admission-refused", + "outcome": "pass" + }, + { + "id": "changed-wheel-body-refused", + "outcome": "pass" + }, + { + "id": "unknown-extra-output-refused", + "outcome": "pass" + }, + { + "id": "bool-inventory-link-count-refused", + "outcome": "pass" + }, + { + "id": "different-exact-admission-refused", + "outcome": "pass" + } + ], + "scope": "synthetic six-tool terminal readback, not actual input acquisition or launcher", + "selected_archives_acquired": 0, + "source_sha256": "45f1fc8224eff3f8811de00b07756b5f404c6924d798f2d1aac39ec8e69b6465" +} diff --git a/qualification/issue-184/tool-acquisition/184-acquisition-terminal-controls.py b/qualification/issue-184/tool-acquisition/184-acquisition-terminal-controls.py new file mode 100644 index 0000000..e445a9d --- /dev/null +++ b/qualification/issue-184/tool-acquisition/184-acquisition-terminal-controls.py @@ -0,0 +1,183 @@ +"""Synthetic six-tool terminal readback; no network/downloaded-wheel imports/SDK.""" +from __future__ import annotations + +from contextlib import contextmanager +from copy import deepcopy +import datetime as dt +import hashlib +import importlib.util +import io +import json +import os +from pathlib import Path +import socket +import sys +import tempfile +import time +from unittest.mock import patch +import zipfile + +sys.dont_write_bytecode=True +BASE=Path('/tmp/sdk-python-handoff') +FAILED=Path('/tmp/sdk-python-184-tools-14adc40b-20261105T035512Z-6d90ccfb71') +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 + + +o=load('trusted_owner_terminal_control',BASE/'184-acquire-tools-v5.py') +g=load('trusted_guard_terminal_control',BASE/'184-acquisition-custody-guardian-v2-proposal.py') +t=load('trusted_terminal_readback_control',BASE/'184-acquisition-terminal-proposal.py') +OWNER_SHA=hashlib.sha256((BASE/'184-acquire-tools-v5.py').read_bytes()).hexdigest() +results=[] + + +def sha(raw):return hashlib.sha256(raw).hexdigest() +def encoded(value):return (json.dumps(value,sort_keys=True,indent=2)+'\n').encode() + + +def synthetic_selection(): + selection=json.loads((FAILED/'selection.json').read_text()) + bodies={};authorities={} + for tool in selection['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: '+text+'\n' for text in authority['info'].get('requires_dist') or []) + body=io.BytesIO();root=tool['name'].replace('-','_')+'-'+tool['version']+'.dist-info/' + with zipfile.ZipFile(body,'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=body.getvalue();tool['bytes']=len(raw);tool['sha256']=sha(raw) + for row in authority['urls']: + if row.get('filename')==tool['filename']: + row['size']=len(raw);row['digests']['sha256']=sha(raw) + bodies[tool['filename']]=raw;authorities[tool['name']]=authority + return selection,bodies,authorities + + +@contextmanager +def candidate_pool(): + with tempfile.TemporaryDirectory(prefix='sdk-python-184-terminal-controls-') as temporary: + selection,bodies,authorities=synthetic_selection();raw_selection=encoded(selection) + parent=Path(temporary) + lease=g.ArchiveCustody(parent,'sdk-python-184-tools-guardian-9999999999',[tool['filename'] for tool in selection['tools']]) + borrowed={name:lease.duplicate_for_owner(name) for name in lease.archive_nodes} + proof={'pool':str(lease.path),'ready':True,'started_monotonic':time.monotonic(), + 'started_utc':dt.datetime.now(dt.timezone.utc).isoformat(), + 'dir_ids':{'.':list(lease.root_id),'wheelhouse':list(lease.wheelhouse_id)},'archive_fds':borrowed} + with patch.object(o,'POOL_BASE',parent),patch.object(o,'SELECTION_SHA',sha(raw_selection)), \ + patch.object(o,'ARCHIVE_BYTES',sum(tool['bytes'] for tool in selection['tools'])), \ + patch.object(o,'CA_SHA',sha(b'synthetic trust fixture')): + item=o.Acquisition(REF);item.attach_guarded_pool(proof);item.prepare_pool(selection['tools']) + item.write('selection.json',raw_selection);item.write('ca.pem',b'synthetic trust fixture') + item.write('launch.json',encoded({'admission':REF,'source':o.SOURCE, + 'verified_acquisition_source_sha256':OWNER_SHA,'credentials_proxy_default_trust':False})) + metadata=[];requests=[] + for tool in selection['tools']: + item.write('metadata/'+tool['name']+'.json',encoded(authorities[tool['name']])) + for tool in selection['tools']: + metadata.append(item.retain_and_inspect(bodies[tool['filename']],tool,authorities[tool['name']])) + for kind in ('metadata','archive'): + for tool in selection['tools']: + number=len(requests)+1;url=tool['metadata_source' if kind=='metadata' else 'url'] + body=encoded(authorities[tool['name']]) if kind=='metadata' else bodies[tool['filename']] + before={'number':number,'kind':kind,'url':url,'attempt':1,'stage':'before-open'} + complete=dict(before,stage='body-complete',bytes_received=len(body),body_sha256=sha(body)) + prefix=f'events/{number:02d}-' + item.write(prefix+'request.json',encoded(before)) + item.write(prefix+'headers.json',encoded({'number':number,'status':200,'same_selected_url':True, + 'content_length':len(body),'content_encoding':'identity'})) + item.write(prefix+'complete.json',encoded(complete));requests.append(complete) + item.write('wheel-metadata.json',encoded({'environment':'CPython3.13 Linux amd64; no extras','tools':metadata, + 'actual_requires_dist_matches_versioned_authority':True,'active_dependency_edges_match_approved_metadata_selection':True})) + item.write('acquisition-observation.json',encoded({'status':'collected-before-final-custody-check','retries':0, + 'import_install_container_SDK_build':False,'archive_bytes':item.archive_used,'http_requests':requests})) + inventory_raw=encoded({'source':o.SOURCE,'entries':item.check_pool(),'inventory_excludes_self':True}) + item.write('inventory.json',inventory_raw);item.seal() + candidate={'outcome':'candidate','pool':str(item.pool),'source':o.SOURCE,'files':len(item.files), + 'whole_pool_bytes':item.used,'archive_bytes':item.archive_used,'inventory_sha256':sha(inventory_raw)} + context={'lease':lease,'candidate':candidate,'selection':raw_selection,'owner':item} + try:yield context + finally: + for directory in (lease.path,lease.path/'wheelhouse',lease.path/'metadata',lease.path/'events'): + directory.chmod(0o700) + for fd in borrowed.values():os.close(fd) + lease.close() + + +def validate(context,**overrides): + args={'custody':context['lease'],'candidate':context['candidate'],'selection_raw':context['selection'], + 'owner_module':o,'owner_source_sha':OWNER_SHA,'admission':REF,'owner_exited':True, + 'owner_exit_code':0,'cleanup_ceiling':time.monotonic()+10} + args.update(overrides);return t.validate_candidate(**args) + + +def changed_file(context,name,raw): + path=context['lease'].path/name;path.chmod(0o600);path.write_bytes(raw);path.chmod(0o400) + + +def positive(): + with candidate_pool() as context: + result=validate(context) + assert result['status']=='validated-candidate-for-outer-readback' and len(result['archives'])==6 + assert len(result['semantic_tools'])==6 and not result['acquisition_accepted'] + assert result['launcher_and_new_general_decision_pending'] + + +def refusal_case(mutate=None,override=None,code=None): + with candidate_pool() as context: + if mutate:mutate(context) + try:validate(context,**(override or {})) + except ValueError as error: + actual=getattr(error,'code',str(error)) + if code is not None:assert actual==code,(actual,code) + else:raise AssertionError('mutated candidate accepted') + + +def extra_file(context): + path=context['lease'].path;path.chmod(0o700) + (path/'unexpected').write_bytes(b'unknown');(path/'unexpected').chmod(0o400);path.chmod(0o500) + context['candidate']['files']+=1;context['candidate']['whole_pool_bytes']+=7 + + +def inventory_bool(context): + raw=json.loads((context['lease'].path/'inventory.json').read_text()) + raw['entries'][0]['links']=True + changed_file(context,'inventory.json',encoded(raw));context['candidate']['inventory_sha256']=sha(encoded(raw)) + context['candidate']['whole_pool_bytes']=sum(p.stat().st_size for p in context['lease'].path.rglob('*') if p.is_file()) + + +def check(name,operation): + try:operation() + except BaseException as error:results.append({'id':name,'outcome':'fail','type':type(error).__name__,'detail':str(error)[:180]}) + else:results.append({'id':name,'outcome':'pass'}) + + +with patch.object(socket,'create_connection',side_effect=AssertionError('NO_NETWORK_IN_TERMINAL_CONTROLS')): + check('six-synthetic-header-tuples-semantic-and-ledger-readback',positive) + check('live-owner-cannot-enter-readback',lambda:refusal_case(override={'owner_exited':False},code='EXACT_OWNER_SUCCESSFUL_EXIT_REQUIRED')) + check('failed-owner-cannot-preserve',lambda:refusal_case(override={'owner_exit_code':1},code='EXACT_OWNER_SUCCESSFUL_EXIT_REQUIRED')) + check('bool-exitcode-cannot-masquerade-as-zero',lambda:refusal_case(override={'owner_exit_code':False},code='EXACT_OWNER_SUCCESSFUL_EXIT_REQUIRED')) + check('expired-terminal-ceiling-refused',lambda:refusal_case(override={'cleanup_ceiling':time.monotonic()-1},code='TERMINAL_DEADLINE_EXCEEDED')) + check('owner-pass-marker-not-independent-candidate',lambda:refusal_case(lambda c:c['candidate'].update(outcome='pass'),code='OWNER_CANDIDATE_MISMATCH')) + check('candidate-file-count-mismatch-refused',lambda:refusal_case(lambda c:c['candidate'].update(files=0),code='CANDIDATE_COUNTS_CHANGED')) + check('candidate-byte-count-mismatch-refused',lambda:refusal_case(lambda c:c['candidate'].update(whole_pool_bytes=0),code='CANDIDATE_COUNTS_CHANGED')) + check('candidate-inventory-hash-mismatch-refused',lambda:refusal_case(lambda c:c['candidate'].update(inventory_sha256='0'*64),code='CANDIDATE_INVENTORY_CHANGED')) + check('changed-selected-source-bytes-refused',lambda:refusal_case(override={'selection_raw':b'changed'},code='SELECTION_AUTHORITY_CHANGED')) + check('changed-trust-file-refused',lambda:refusal_case(lambda c:changed_file(c,'ca.pem',b'changed'))) + check('changed-launch-admission-refused',lambda:refusal_case(lambda c:changed_file(c,'launch.json',encoded({'admission':'changed'})))) + check('changed-wheel-body-refused',lambda:refusal_case(lambda c:changed_file(c,'wheelhouse/'+next(iter(c['lease'].archive_nodes)),b'changed'))) + check('unknown-extra-output-refused',lambda:refusal_case(extra_file,code='OWNER_INVENTORY_INCOMPLETE_OR_EXTRA')) + check('bool-inventory-link-count-refused',lambda:refusal_case(inventory_bool,code='OWNER_INVENTORY_BYTES_OR_IDENTITY_CHANGED')) + check('different-exact-admission-refused',lambda:refusal_case(override={'admission':'different'},code='LAUNCH_AUTHORITY_CHANGED')) + +report={'scope':'synthetic six-tool terminal readback, not actual input acquisition or launcher', + 'source_sha256':sha((BASE/'184-acquisition-terminal-proposal.py').read_bytes()),'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,'launcher_pending':True} +(BASE/'184-acquisition-terminal-controls-result.json').write_text(json.dumps(report,sort_keys=True,indent=2)+'\n') +print(json.dumps(report,indent=2));raise SystemExit(bool(report['failed'])) diff --git a/qualification/issue-184/tool-acquisition/184-acquisition-terminal-proposal.py b/qualification/issue-184/tool-acquisition/184-acquisition-terminal-proposal.py new file mode 100644 index 0000000..b7e69fc --- /dev/null +++ b/qualification/issue-184/tool-acquisition/184-acquisition-terminal-proposal.py @@ -0,0 +1,201 @@ +"""Unexecuted independent candidate readback; no launcher or acquisition admission.""" +from __future__ import annotations + +from contextlib import ExitStack +import hashlib +import json +import os +from pathlib import PurePosixPath +import re +import stat +import time + + +class Refusal(ValueError): + pass + + +def require(value, code): + if not value:raise Refusal(code) + + +def sha(raw):return hashlib.sha256(raw).hexdigest() + + +def unique_pairs(pairs): + result={} + for key,value in pairs: + require(key not in result,'DUPLICATE_JSON_KEY') + result[key]=value + return result + + +def parsed(raw):return json.loads(raw,object_pairs_hook=unique_pairs) + + +def snapshot(custody, cap=4*1024**2): + """Closed tree, pinned archive identities and stable no-follow regular-file reads.""" + custody.scope() + rows={} + with ExitStack() as stack: + root=custody.root_fd + st=os.fstat(root) + require(stat.S_IMODE(st.st_mode)==0o500,'ROOT_NOT_SEALED') + children=set(os.listdir(root)) + directories={'metadata','wheelhouse','events'} + require(directories<=children,'MISSING_POOL_DIRECTORY') + for directory in ('.','metadata','wheelhouse','events'): + parent=root + names=children-directories + 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_uid==os.geteuid() and stat.S_IMODE(st.st_mode)==0o500,'DIRECTORY_NOT_SEALED') + names=set(os.listdir(parent)) + require(len(names)<=128,'FILE_COUNT_BOUND') + for leaf in sorted(names): + require(isinstance(leaf,str) and leaf not in {'.','..'} and '/' not in leaf + and '\\' not in leaf and not any(ord(c)<32 for c in leaf),'UNSAFE_POOL_LEAF') + 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: + before=os.fstat(stream.fileno()) + require(stat.S_ISREG(before.st_mode) and before.st_uid==os.geteuid() + and before.st_nlink==1 and stat.S_IMODE(before.st_mode)==0o400 + and 0<=before.st_size<=cap,'POOL_FILE_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,'POOL_FILE_CHANGED_DURING_READ') + if directory=='wheelhouse': + require(leaf in custody.archive_nodes + and (before.st_dev,before.st_ino)==custody.archive_nodes[leaf]['identity'], + 'ARCHIVE_IDENTITY_CHANGED') + rows[name]={'bytes':len(raw),'sha256':sha(raw),'device':before.st_dev, + 'inode':before.st_ino,'mode':'0400','links':1,'raw':raw} + require(len(rows)<=128 and sum(row['bytes'] for row in rows.values())<=cap,'WHOLE_POOL_BOUND') + custody.scope() + return rows + + +def validate_candidate(custody, candidate, selection_raw, owner_module, owner_source_sha, + admission, owner_exited, owner_exit_code, cleanup_ceiling): + """Caller must establish exact pidfd exit/code from its created child before calling.""" + require(owner_exited is True and type(owner_exit_code) is int and owner_exit_code==0, + 'EXACT_OWNER_SUCCESSFUL_EXIT_REQUIRED') + require(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..ff2ad15 --- /dev/null +++ b/qualification/issue-184/tool-acquisition/README.md @@ -0,0 +1,42 @@ +# Issue 184 guarded tool-input acquisition source proposal + +This is an **unexecuted source composition checkpoint**, not a launch-ready packet, +an acquisition admission, an actual tool-pool acceptance, or an SDK artifact. +The launcher and a new exact General decision remain pending. No selected tool +archive was acquired, imported or installed by these controls; no Docker or SDK +build ran. + +This draft is stacked on selected SDK source `14adc40b` / tree `9bdf4d38` only to +keep its source cohort visible. **Do not merge it into the frozen candidate or +selected SDK source refs.** The SDK source itself is unchanged. Original frozen H, +old wheel, offline-v5 PASS, connected PRODUCT-FAIL, failed data acquisition and +its exact unproved cause keep their original attribution. + +The owner-v5 adapter requires the independent guardian's preallocated pool before +network access and writes only through the pinned archive descriptors. It returns +a candidate, never final acceptance. Its 85 retained parser/transport/custody +controls and 16 new handoff controls pass locally against synthetic transport or +owned temporary files. + +The independently reviewed guardian-v2 primitive requires exact owner exit before +archive cleanup; a survivor retains named nodes and outer custody. Its bounded +source PASS is https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-6658714041 . +The new terminal readback checks six raw archive tuples, ZIP/METADATA/WHEEL, +closed dependency graph, fresh metadata/request event linkage, trust, complete +inventory, stable bytes/inodes and exact owner exit. Its 16 controls use synthetic +header-only ZIPs and fabricated response records, not real acquired archives. + +`source-manifest.json` binds the exact source/result bytes at this checkpoint. +The scripts retain explicit local paths from the reviewed packet; they are not +installed tooling or an automatic CI entry point. Controls also reference retained +source-only countercontrols and the failed pool's six public metadata records on +lab4. Their original durable sources are recorded on issue184, including owner-v4 +SOURCE PASS6656664025 and the preceding original/v2/v3 control packets. Those +forensic inputs are not reconstructed or silently fetched by this draft. + +Next source work: compose the isolated interpreter/bootstrap launcher, independent +owner/guardian handoff and successful preserve/outer-readback semantics; add whole +protocol failure controls; return a new exact composed source/input packet for +Security before any distinct General acquisition decision. The current direct +entry points refuse or require a proof that no approved invocation yet supplies. +No release, tag, artifact replacement, SDK install, or connected retry is implied. 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..8216f72 --- /dev/null +++ b/qualification/issue-184/tool-acquisition/source-manifest.json @@ -0,0 +1,77 @@ +{ + "actual_selected_archives": null, + "downloaded_wheel_imports_or_installs": 0, + "files": [ + { + "bytes": 44721, + "file": "184-acquire-tools-v5.py", + "sha256": "96d4a5b1b5687d11779a244a2a6946555c24020b3b54e93e43e61be2f6722e2c" + }, + { + "bytes": 2975, + "file": "184-acquire-tools-v5-bootstrap.py", + "sha256": "00349dc4ff6bec00072bcdfd01fffe5c13730de64f78e8dc940324c755adfe20" + }, + { + "bytes": 39547, + "file": "184-acquire-tools-v5-controls.py", + "sha256": "786fb0346a25d46a0eb75bc9f21f9dd6a455eee422ec08041cea83860d4a15a1" + }, + { + "bytes": 8195, + "file": "184-acquire-tools-v5-controls-result.json", + "sha256": "437f47ba42687bb80039a1915bb0b4d45d2055571554cea725a560cbfc67e5f7" + }, + { + "bytes": 9884, + "file": "184-acquire-tools-v5-adapter-controls.py", + "sha256": "840c8bd2237498abf2f333edc1d5af45dbb9c181cf60f4cad474d51f1176dd9f" + }, + { + "bytes": 1898, + "file": "184-acquire-tools-v5-adapter-controls-result.json", + "sha256": "529390459d99fc262ff84cd52f607034f2af72acaa11fea53d4ebe31cd2bc45a" + }, + { + "bytes": 11769, + "file": "184-acquisition-custody-guardian-v2-proposal.py", + "sha256": "afc5f198de12d4b26520957bc563ff0122f8535838a1cbc64c40272c5f3f7f70" + }, + { + "bytes": 11937, + "file": "184-acquisition-terminal-proposal.py", + "sha256": "45f1fc8224eff3f8811de00b07756b5f404c6924d798f2d1aac39ec8e69b6465" + }, + { + "bytes": 11638, + "file": "184-acquisition-terminal-controls.py", + "sha256": "b32bcf3d3ac933e4abba1ddb002d72cf9b6fb16fa0b0718de3bd5fa01c84b47f" + }, + { + "bytes": 1826, + "file": "184-acquisition-terminal-controls-result.json", + "sha256": "570abc1b842372e7e8773f8d7d64f22ac2d696940639f08d6e1cdd09a6facae3" + }, + { + "bytes": 1010, + "file": "184-acquisition-interpreter-authority-proposal.json", + "sha256": "cb819468f659c6235176fabcd29894dda552ffb85f0bf54664d83b7dcdd3d2e1" + }, + { + "bytes": 4550, + "file": "184-immutable-build-tool-input-proposal.json", + "sha256": "e9a7d639325b866569cdab83b81a2949437209a6bca91c881c2e365fdc573a1c" + } + ], + "launcher": null, + "new_general_decision": null, + "new_sdk_wheel": null, + "real_network_requests": 0, + "selected_sdk_source": "14adc40b00b68cc8a3e1ae5647a885a42f273650", + "source_control_results": { + "independent_terminal_controls": 16, + "preallocated_adapter_controls": 16, + "retained_owner_controls": 85 + }, + "stage": "UNEXECUTED source composition checkpoint; not launch ready" +} From 3117ffc8c20b1985ba9051355015161ae0a31620 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Thu, 5 Nov 2026 14:24:31 +0000 Subject: [PATCH 2/8] Compose unexecuted launcher and independent guarded readback protocol --- ...uire-tools-v5-adapter-controls-result.json | 2 +- .../184-acquire-tools-v5-adapter-controls.py | 10 +- .../184-acquire-tools-v5-bootstrap.py | 2 +- .../184-acquire-tools-v5-controls-result.json | 14 +- .../184-acquire-tools-v5-controls.py | 39 +- .../tool-acquisition/184-acquire-tools-v5.py | 38 +- ...on-launcher-bootstrap-controls-result.json | 61 ++ ...acquisition-launcher-bootstrap-controls.py | 77 +++ ...acquisition-launcher-bootstrap-proposal.py | 87 +++ ...4-acquisition-session-controls-result.json | 86 +++ .../184-acquisition-session-controls.py | 279 +++++++++ .../184-acquisition-session-proposal.py | 541 ++++++++++++++++++ ...-acquisition-terminal-controls-result.json | 2 +- .../184-acquisition-terminal-controls.py | 8 +- .../184-acquisition-terminal-proposal.py | 8 +- .../issue-184/tool-acquisition/README.md | 69 ++- .../tool-acquisition/authority-template.json | 28 + .../tool-acquisition/source-manifest.json | 106 ++-- 18 files changed, 1335 insertions(+), 122 deletions(-) create mode 100644 qualification/issue-184/tool-acquisition/184-acquisition-launcher-bootstrap-controls-result.json create mode 100644 qualification/issue-184/tool-acquisition/184-acquisition-launcher-bootstrap-controls.py create mode 100644 qualification/issue-184/tool-acquisition/184-acquisition-launcher-bootstrap-proposal.py create mode 100644 qualification/issue-184/tool-acquisition/184-acquisition-session-controls-result.json create mode 100644 qualification/issue-184/tool-acquisition/184-acquisition-session-controls.py create mode 100644 qualification/issue-184/tool-acquisition/184-acquisition-session-proposal.py create mode 100644 qualification/issue-184/tool-acquisition/authority-template.json 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 index 1513f46..e8b5c65 100644 --- 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 @@ -3,7 +3,7 @@ "downloaded_wheel_imports_or_installs": 0, "failed": 0, "guardian_sha256": "afc5f198de12d4b26520957bc563ff0122f8535838a1cbc64c40272c5f3f7f70", - "owner_sha256": "96d4a5b1b5687d11779a244a2a6946555c24020b3b54e93e43e61be2f6722e2c", + "owner_sha256": "234e099b5208aaafca379d3e5535cf44b3158a327764eec0ef4e12afcfc45ffb", "passed": 16, "real_network_requests": 0, "results": [ 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 index 3a6be1f..2ed8f67 100644 --- 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 @@ -50,11 +50,15 @@ def refuses(operation,code): 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-9999999999',['one.whl','two.whl']) + 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':{'.':list(custody.root_id),'wheelhouse':list(custody.wheelhouse_id)}, + 'dir_ids':ids, 'archive_fds':dict(borrowed)} with patch.object(owner,'POOL_BASE',parent):item=owner.Acquisition(REF) try:yield item,custody,proof @@ -110,7 +114,7 @@ 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 (item.pool/'metadata').exists() + assert not list((item.pool/'metadata').iterdir()) def fdopen_failure_closes_duplicate_cleans_all_preallocated(): 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 index 0de1815..df71064 100644 --- a/qualification/issue-184/tool-acquisition/184-acquire-tools-v5-bootstrap.py +++ b/qualification/issue-184/tool-acquisition/184-acquire-tools-v5-bootstrap.py @@ -14,7 +14,7 @@ from types import ModuleType OWNER_PATH = Path('/tmp/sdk-python-handoff/184-acquire-tools-v5.py') -OWNER_SHA = '96d4a5b1b5687d11779a244a2a6946555c24020b3b54e93e43e61be2f6722e2c' +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'} 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 index 74abf7c..3addfba 100644 --- 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 @@ -1,11 +1,11 @@ { "actual_failed_archive_cause_established": false, - "bootstrap_sha256": "00349dc4ff6bec00072bcdfd01fffe5c13730de64f78e8dc940324c755adfe20", - "cases": 85, + "bootstrap_sha256": "12679c567452dd4cdda7dbd789ac600ef0b5455a1225c95cf59d066fda472569", + "cases": 86, "downloaded_wheel_imports_or_installs": 0, "failed": 0, "original_source_sha256": "e0b8955c8c26c68333e1439c4eb195ef803d710d68f4a4b297b3dd17fcfd7c54", - "passed": 85, + "passed": 86, "real_network_requests": 0, "results": [ { @@ -185,7 +185,11 @@ "outcome": "pass" }, { - "id": "openssl-child-actual-effective-closed-env", + "id": "exact-reviewed-CA-reused-without-child-changed-bytes-refuse", + "outcome": "pass" + }, + { + "id": "guarded-owner-has-no-child-launch-surface", "outcome": "pass" }, { @@ -351,5 +355,5 @@ ], "scope": "synthetic source controls, not archive acquisition/SDK/runtime qualification", "simulated_transport_calls": 17, - "successor_source_sha256": "96d4a5b1b5687d11779a244a2a6946555c24020b3b54e93e43e61be2f6722e2c" + "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 index 175706a..e00174c 100644 --- a/qualification/issue-184/tool-acquisition/184-acquire-tools-v5-controls.py +++ b/qualification/issue-184/tool-acquisition/184-acquire-tools-v5-controls.py @@ -4,6 +4,7 @@ from contextlib import contextmanager from copy import deepcopy import hashlib +import ast import importlib.util import io import json @@ -399,23 +400,24 @@ def record_exec(code, namespace): assert all(observed[0].get(key) == value for key, value in SENTINELS.items()) -def openssl_environment_control(): - real_run = subprocess.run - observed = [] - def child_probe(argv, **kwargs): - assert argv == ['/usr/bin/openssl', 'x509', '-noout', '-subject', '-nameopt', 'RFC2253'] - assert kwargs['env'] == new.MINIMAL_ENV and kwargs['close_fds'] is True - # Exercise the effective child environment with local stdlib only; no tool wheel or CA/network work. - probe = real_run(['/usr/bin/python3', '-I', '-c', - 'import json,os; print(json.dumps(dict(os.environ)))'], - env=kwargs['env'], close_fds=kwargs['close_fds'], capture_output=True, - timeout=2, check=True) - observed.append(json.loads(probe.stdout)) - return subprocess.CompletedProcess(argv, 0, stdout=b'subject=CN=Synthetic Control\n') - with patch.dict(os.environ, SENTINELS, clear=False), patch.object(new.subprocess, 'run', side_effect=child_probe): - assert new.ca_subject(b'synthetic non-PEM control') == 'CN=Synthetic Control' - assert observed == [new.MINIMAL_ENV] - assert not set(SENTINELS) & set(observed[0]) +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(): @@ -722,7 +724,8 @@ def headers_for_name(name=name): 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('openssl-child-actual-effective-closed-env', openssl_environment_control) + 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) diff --git a/qualification/issue-184/tool-acquisition/184-acquire-tools-v5.py b/qualification/issue-184/tool-acquisition/184-acquire-tools-v5.py index 118cb74..7f82d1f 100644 --- a/qualification/issue-184/tool-acquisition/184-acquire-tools-v5.py +++ b/qualification/issue-184/tool-acquisition/184-acquire-tools-v5.py @@ -14,7 +14,6 @@ import signal import ssl import stat -import subprocess import sys import time import urllib.parse @@ -144,11 +143,13 @@ def utc() -> str: return dt.datetime.now(dt.timezone.utc).isoformat() -def ca_subject(cert: bytes) -> str: - check = subprocess.run(['/usr/bin/openssl', 'x509', '-noout', '-subject', '-nameopt', 'RFC2253'], - input=cert, capture_output=True, timeout=2, check=True, - env=dict(MINIMAL_ENV), close_fds=True) - return check.stdout.decode().removeprefix('subject=').strip() +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: @@ -213,7 +214,7 @@ def __init__(self, admission: str) -> None: self.started_utc = utc() self.io_deadline = self.started + 270 self.ceiling = self.started + 300 - self.pool = POOL_BASE / ('sdk-python-184-tools-guardian-' + admission.rsplit('-', 1)[-1]) + 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 @@ -245,15 +246,18 @@ def attach_guarded_pool(self, proof: dict) -> None: - (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) == {'.', 'wheelhouse'}, 'guardian directory proof shape') - for name in ('.', 'wheelhouse'): + 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)) == {'wheelhouse'}, 'guardian root not initially exclusive') + 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') @@ -280,10 +284,9 @@ def prepare_pool(self, tools: list[dict]) -> None: st = os.fstat(root) require((st.st_dev, st.st_ino) == self.dir_ids['.'], 'guardian root changed before adoption') for name in ('metadata', 'events'): - os.mkdir(name, 0o700, dir_fd=root) st = os.stat(name, dir_fd=root, follow_symlinks=False) - 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) + 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) @@ -652,13 +655,7 @@ def run(self) -> dict: 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: - subject = ca_subject(cert) - require(not any(re.search(r'(?:^|[,+])CN=' + re.escape(name) + r'(?:$|[,+])', subject) - for name in BLOCKED), 'blocked CA subject') + 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')) @@ -675,6 +672,7 @@ def run(self) -> dict: '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}, 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..4aa6c24 --- /dev/null +++ b/qualification/issue-184/tool-acquisition/184-acquisition-launcher-bootstrap-controls-result.json @@ -0,0 +1,61 @@ +{ + "cases": 12, + "downloaded_wheel_imports_or_installs": 0, + "failed": 0, + "new_general_decision": null, + "passed": 12, + "real_network_requests": 0, + "results": [ + { + "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" + } + ], + "scope": "bootstrap file/refusal controls; no admitted start or selected archive access", + "selected_archives_acquired": 0, + "source_sha256": "9205ae8ea069fb12cc35ea28dbe5ced8a4ccbd2a1b12bb6d4ae76633816c2c36" +} 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..05d2f92 --- /dev/null +++ b/qualification/issue-184/tool-acquisition/184-acquisition-launcher-bootstrap-controls.py @@ -0,0 +1,77 @@ +"""Local bootstrap refusal/resource controls. No acquisition or admitted start.""" +from __future__ import annotations + +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') +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 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','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')) + +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} +(BASE/'184-acquisition-launcher-bootstrap-controls-result.json').write_text(json.dumps(report,sort_keys=True,indent=2)+'\n') +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..e362069 --- /dev/null +++ b/qualification/issue-184/tool-acquisition/184-acquisition-launcher-bootstrap-proposal.py @@ -0,0 +1,87 @@ +"""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 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 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)) + + +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=json.loads(raw,object_pairs_hook=pairs) + 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) + 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 + + +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..38d9d3d --- /dev/null +++ b/qualification/issue-184/tool-acquisition/184-acquisition-session-controls-result.json @@ -0,0 +1,86 @@ +{ + "acquisition_accepted": false, + "cases": 18, + "downloaded_wheel_imports_or_installs": 0, + "failed": 0, + "new_general_decision": null, + "passed": 18, + "real_network_requests": 0, + "results": [ + { + "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": "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" + } + ], + "scope": "synthetic role-protocol controls; start/admitted-data API never called with a decision", + "selected_archives_acquired": 0, + "source_sha256": "08773d12f2fe52249907eae97af4cf4d97db43f47125436d41ccaa70b8db0994" +} 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..5bde432 --- /dev/null +++ b/qualification/issue-184/tool-acquisition/184-acquisition-session-controls.py @@ -0,0 +1,279 @@ +"""Synthetic private role protocol; never calls the admitted-data start API.""" +from __future__ import annotations + +import datetime as dt +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 urllib.request +import zipfile +from types import 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-v2-proposal.py') +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 + if self.mode=='fail':raise RuntimeError('synthetic failure only') + if self.mode in {'block','abort','guardian-crash'}: + os.write(next(iter(proof['archive_fds'].values())),b'synthetic partial') + signal.pause() + if self.mode=='survivor': + 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=Path('/tmp/sdk-python-184-tools-14adc40b-20261105T035512Z-6d90ccfb71') + 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): + 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}) + 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) + s.POOL_PARENT=parent + 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=s.guardian_job(inner,authority_raw,authority,started,dt.datetime.now(dt.timezone.utc).isoformat(), + modules,bounds=(0.05 if mode=='survivor' 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] + 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() + else:message=session.wait() + reaped=session.guardian_reaped + result=message['result'];assert not result['acquisition_accepted'] + if 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['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 result['terminal_cleanup_complete'] and result['retained_custody_sealed'] + assert not list((expected_pool/'wheelhouse').iterdir()) + assert session.finished + assert select.select([pidfd],[],[],0)[0] if not session.finished else True + 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 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; 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__); 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 check(name,operation): + try:operation() + except BaseException as error:results.append({'id':name,'outcome':'fail','type':type(error).__name__,'detail':str(error)[:600]}) + else:results.append({'id':name,'outcome':'pass'}) + + +with patch.object(socket,'create_connection',side_effect=AssertionError('NO_NETWORK_IN_SESSION_CONTROLS')): + check('synthetic-owner-exit-independent-check-and-outer-readback',lambda:run_protocol('positive')) + check('owner-failure-is-refusal-zero-archives',lambda:run_protocol('fail')) + check('active-deadline-stops-and-reaps-exact-owner',lambda:run_protocol('block')) + check('candidate-marker-required-before-preserve',lambda:run_protocol('bad-marker')) + check('independent-readback-refusal-cleans-archives',lambda:run_protocol('readback-refusal')) + check('explicit-outer-abort-stops-owner-cleans-archives',lambda:run_protocol('abort')) + check('guardian-crash-outer-stops-exact-owner-cleans-archives',lambda:run_protocol('guardian-crash')) + check('survivor-keeps-all-custody-until-confirmed-deferred-exit',lambda:run_protocol('survivor')) + check('actual-owner-and-terminal-compose-with-synthetic-six-tool-closure',lambda:run_protocol('integrated-positive')) + check('actual-owner-transport-timeout-refuses-integrated-preserve',lambda:run_protocol('integrated-timeout')) + check('outer-disappears-before-arming-no-owner-work',lambda:run_protocol('positive','register')) + check('outer-disappears-with-blocked-owner-stops-before-work',lambda:run_protocol('positive','owner')) + check('descriptor-frame-roundtrip-preserves-scoped-handle',fd_frame_roundtrip) + check('oversized-wire-frame-refused',lambda:frame_control(b'X'*(s.FRAME_CAP+1),'FRAME_EOF_OR_TRUNCATION')) + check('duplicate-frame-key-refused',lambda:frame_control(b'{"type":"one","type":"two"}','DUPLICATE_FRAME_KEY')) + check('non-object-frame-refused',lambda:frame_control(b'[]','FRAME_OBJECT_REQUIRED')) + check('actual-root-owned-interpreter-clean-env-I-S-startup',actual_closed_startup) + check('missing-new-decision-refuses-before-authority-or-fork',live_start_refuses_no_decision) + +report={'scope':'synthetic role-protocol controls; start/admitted-data API never called with a decision', + 'source_sha256':hashlib.sha256((BASE/'184-acquisition-session-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,'selected_archives_acquired':0,'downloaded_wheel_imports_or_installs':0, + 'new_general_decision':None,'acquisition_accepted':False} +(BASE/'184-acquisition-session-controls-result.json').write_text(json.dumps(report,sort_keys=True,indent=2)+'\n') +print(json.dumps(report,indent=2));raise SystemExit(bool(report['failed'])) diff --git a/qualification/issue-184/tool-acquisition/184-acquisition-session-proposal.py b/qualification/issue-184/tool-acquisition/184-acquisition-session-proposal.py new file mode 100644 index 0000000..11d206d --- /dev/null +++ b/qualification/issue-184/tool-acquisition/184-acquisition-session-proposal.py @@ -0,0 +1,541 @@ +"""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 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': '12679c567452dd4cdda7dbd789ac600ef0b5455a1225c95cf59d066fda472569', 'guardian': 'afc5f198de12d4b26520957bc563ff0122f8535838a1cbc64c40272c5f3f7f70', 'terminal': 'd050992a68c09db741fa4a0afda234c60e30a3412441b5dfce16be5e26efedce', 'selection': 'e9a7d639325b866569cdab83b81a2949437209a6bca91c881c2e365fdc573a1c', 'interpreter': '52e0a13e60a981d8c4b6478be2ba5176f69da07948a056bf49cf6f077e30cb41', 'library': '982cb3768d1d8b00126255ee2785bccf8360c68b197fb3dec95858ab56195f87', 'ca': '2ef52ad56b4803e264045c915e55c236cbfa045b84b81efefc218a19eb44bd2b'} +FRAME_CAP=65536 +POOL_CAP=4*1024**2 + + +class Refusal(ValueError):pass + + +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(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) + fd=os.open(name,os.O_RDONLY|os.O_DIRECTORY|os.O_NOFOLLOW,dir_fd=custody.root_fd) + custody.stack.callback(os.close,fd) + st=os.fstat(fd);custody.dir_ids[name]=[st.st_dev,st.st_ino];custody.directory_fds[name]=fd + return custody + except BaseException: + custody.cleanup();custody.close();raise + + +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): + 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) + return {'pid':pid,'pidfd':os.pidfd_open(pid),'status':status_read,'gate':gate_write} + + +def stop_owner(role,ceiling): + if not select.select([role['pidfd']],[],[],0)[0]: + try:signal.pidfd_send_signal(role['pidfd'],signal.SIGKILL) + except ProcessLookupError:pass + exited=bool(select.select([role['pidfd']],[],[],max(0,min(5,ceiling-time.monotonic())))[0]) + if not exited:return {'owner_exited':False,'owner_exit_code':None} + _,status=os.waitpid(role['pid'],0) + return {'owner_exited':True,'owner_exit_code':os.waitstatus_to_exitcode(status)} + + +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() + 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.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'}) + 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 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 abort(self,reason): + """Keep custody if either writer/guardian exit or exact cleanup is unproved.""" + 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: + try:signal.pidfd_send_signal(self.owner_pidfd,signal.SIGKILL) + except ProcessLookupError:pass + 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.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['terminal_cleanup_complete']=False + if result['terminal_cleanup_complete']: + 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,'guardian_process_exited':guardian_gone, + 'outer_owner_custody_required':True,'acquisition_accepted':False} + 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()) + json.loads(raw,object_pairs_hook=pairs) + guard=module('reviewed_guardian','184-acquisition-custody-guardian-v2-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'},'UNKNOWN_AUTHORITY_FIELD') + 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') + 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 Date: Thu, 5 Nov 2026 15:35:53 +0000 Subject: [PATCH 3/8] Complete source-only guarded acquisition review packet and failure controls --- .../184-acquire-tools-v5-adapter-controls.py | 12 +- .../184-acquire-tools-v5-bootstrap.py | 6 +- .../184-acquire-tools-v5-controls-result.json | 14 +- .../184-acquire-tools-v5-controls.py | 36 +- ...on-launcher-bootstrap-controls-result.json | 14 +- ...acquisition-launcher-bootstrap-controls.py | 43 +- ...acquisition-launcher-bootstrap-proposal.py | 32 + ...4-acquisition-session-controls-result.json | 22 +- .../184-acquisition-session-controls.py | 106 ++- .../184-acquisition-session-proposal.py | 97 ++- ...-acquisition-terminal-controls-result.json | 2 +- .../184-acquisition-terminal-controls.py | 14 +- .../184-acquisition-terminal-proposal.py | 1 - .../issue-184/tool-acquisition/README.md | 132 +++- .../tool-acquisition/authority-template.json | 6 +- .../historical-source/manifest.json | 29 + .../historical-source/original-owner.source | 313 ++++++++ .../historical-source/v2-owner.source | 583 +++++++++++++++ .../historical-source/v3-bootstrap.source | 62 ++ .../historical-source/v3-owner.source | 667 ++++++++++++++++++ .../control-inputs/metadata/build.json | 41 ++ .../control-inputs/metadata/packaging.json | 32 + .../control-inputs/metadata/pip.json | 32 + .../metadata/pyproject-hooks.json | 32 + .../control-inputs/metadata/setuptools.json | 83 +++ .../control-inputs/metadata/wheel.json | 34 + .../control-inputs/provenance.json | 63 ++ .../control-inputs/selection.json | 99 +++ .../tool-acquisition/review-packet.json | 111 +++ .../tool-acquisition/source-manifest.json | 174 ++++- 30 files changed, 2763 insertions(+), 129 deletions(-) create mode 100644 qualification/issue-184/tool-acquisition/control-inputs/historical-source/manifest.json create mode 100644 qualification/issue-184/tool-acquisition/control-inputs/historical-source/original-owner.source create mode 100644 qualification/issue-184/tool-acquisition/control-inputs/historical-source/v2-owner.source create mode 100644 qualification/issue-184/tool-acquisition/control-inputs/historical-source/v3-bootstrap.source create mode 100644 qualification/issue-184/tool-acquisition/control-inputs/historical-source/v3-owner.source create mode 100644 qualification/issue-184/tool-acquisition/control-inputs/metadata/build.json create mode 100644 qualification/issue-184/tool-acquisition/control-inputs/metadata/packaging.json create mode 100644 qualification/issue-184/tool-acquisition/control-inputs/metadata/pip.json create mode 100644 qualification/issue-184/tool-acquisition/control-inputs/metadata/pyproject-hooks.json create mode 100644 qualification/issue-184/tool-acquisition/control-inputs/metadata/setuptools.json create mode 100644 qualification/issue-184/tool-acquisition/control-inputs/metadata/wheel.json create mode 100644 qualification/issue-184/tool-acquisition/control-inputs/provenance.json create mode 100644 qualification/issue-184/tool-acquisition/control-inputs/selection.json create mode 100644 qualification/issue-184/tool-acquisition/review-packet.json 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 index 2ed8f67..98316ba 100644 --- 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 @@ -2,7 +2,6 @@ from __future__ import annotations from contextlib import contextmanager -from copy import deepcopy import datetime as dt import hashlib import importlib.util @@ -193,6 +192,15 @@ def wrong_descriptor_refused(): '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} -(BASE/'184-acquire-tools-v5-adapter-controls-result.json').write_text(json.dumps(report,sort_keys=True,indent=2)+'\n') +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 index df71064..33effec 100644 --- a/qualification/issue-184/tool-acquisition/184-acquire-tools-v5-bootstrap.py +++ b/qualification/issue-184/tool-acquisition/184-acquire-tools-v5-bootstrap.py @@ -5,7 +5,6 @@ """ from __future__ import annotations -import argparse import hashlib import os from pathlib import Path @@ -59,7 +58,4 @@ def launch(admission: str, proof: dict | None = None) -> dict: 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) + 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 index 3addfba..c5ed060 100644 --- 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 @@ -1,6 +1,6 @@ { "actual_failed_archive_cause_established": false, - "bootstrap_sha256": "12679c567452dd4cdda7dbd789ac600ef0b5455a1225c95cf59d066fda472569", + "bootstrap_sha256": "6ca5da88054b4c3ea27b2614dfd987f88ecb3db981271a995bb9941d06398fed", "cases": 86, "downloaded_wheel_imports_or_installs": 0, "failed": 0, @@ -13,7 +13,7 @@ "outcome": "pass" }, { - "id": "retained-authority-tuple-build", + "id": "derived-authority-tuple-build", "outcome": "pass" }, { @@ -21,7 +21,7 @@ "outcome": "pass" }, { - "id": "retained-authority-tuple-packaging", + "id": "derived-authority-tuple-packaging", "outcome": "pass" }, { @@ -29,7 +29,7 @@ "outcome": "pass" }, { - "id": "retained-authority-tuple-pip", + "id": "derived-authority-tuple-pip", "outcome": "pass" }, { @@ -37,7 +37,7 @@ "outcome": "pass" }, { - "id": "retained-authority-tuple-pyproject-hooks", + "id": "derived-authority-tuple-pyproject-hooks", "outcome": "pass" }, { @@ -45,7 +45,7 @@ "outcome": "pass" }, { - "id": "retained-authority-tuple-setuptools", + "id": "derived-authority-tuple-setuptools", "outcome": "pass" }, { @@ -53,7 +53,7 @@ "outcome": "pass" }, { - "id": "retained-authority-tuple-wheel", + "id": "derived-authority-tuple-wheel", "outcome": "pass" }, { 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 index e00174c..12674ce 100644 --- a/qualification/issue-184/tool-acquisition/184-acquire-tools-v5-controls.py +++ b/qualification/issue-184/tool-acquisition/184-acquire-tools-v5-controls.py @@ -14,6 +14,7 @@ import sys import subprocess import tempfile +from types import ModuleType import urllib.error import urllib.request from unittest.mock import patch @@ -21,21 +22,29 @@ sys.dont_write_bytecode = True BASE = Path('/tmp/sdk-python-handoff') -FAILED = Path('/tmp/sdk-python-184-tools-14adc40b-20261105T035512Z-6d90ccfb71') +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 -old = load('original_acquisition_source_only', BASE / '184-acquire-tools.py') +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((BASE / '184-acquire-tools.py').read_bytes()).hexdigest() \ +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()) @@ -293,7 +302,7 @@ def untracked_failure_record_collision_cleanup(): def v2_semantic_refusal_countercontrol(): - previous = load('unexecuted_v2_source_only', BASE / '184-acquire-tools-v2.py') + 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: @@ -330,7 +339,7 @@ def redirect_failure_cleans_prior_archive(): def fdopen_failure_countercontrol(): - previous = load('held_v3_source_only', BASE / '184-acquire-tools-v3.py') + 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) @@ -375,7 +384,7 @@ def failed_wrap(fd, mode): def bootstrap_environment_countercontrol(): - previous = load('held_v3_bootstrap_source_only', BASE / '184-acquire-tools-v3-bootstrap.py') + 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. @@ -684,7 +693,7 @@ def bootstrap_old_admission_refuses_before_source_read(): 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('retained-authority-tuple-' + name, lambda name=name: new.validate_authority(authorities[name], tools[name])) + 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: @@ -780,13 +789,22 @@ def headers_for_name(name=name): 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((BASE / '184-acquire-tools.py').read_bytes()).hexdigest(), + '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} -(BASE / '184-acquire-tools-v5-controls-result.json').write_text(json.dumps(report, sort_keys=True, indent=2)+'\n') +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-acquisition-launcher-bootstrap-controls-result.json b/qualification/issue-184/tool-acquisition/184-acquisition-launcher-bootstrap-controls-result.json index 4aa6c24..5a0232e 100644 --- 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 @@ -1,9 +1,9 @@ { - "cases": 12, + "cases": 14, "downloaded_wheel_imports_or_installs": 0, "failed": 0, "new_general_decision": null, - "passed": 12, + "passed": 14, "real_network_requests": 0, "results": [ { @@ -53,9 +53,17 @@ { "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": "9205ae8ea069fb12cc35ea28dbe5ced8a4ccbd2a1b12bb6d4ae76633816c2c36" + "source_sha256": "1eeb491accbb7f129c8e4c329c238a26ef6cd929a6204e949d16becc604621a4" } 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 index 05d2f92..4e3eefb 100644 --- a/qualification/issue-184/tool-acquisition/184-acquisition-launcher-bootstrap-controls.py +++ b/qualification/issue-184/tool-acquisition/184-acquisition-launcher-bootstrap-controls.py @@ -2,6 +2,8 @@ from __future__ import annotations import hashlib +import io +from contextlib import redirect_stdout import importlib.util import json import os @@ -54,6 +56,34 @@ def affinity_refusal(value): 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 check(name,operation): try:operation() except BaseException as error:results.append({'id':name,'outcome':'fail','type':type(error).__name__,'detail':str(error)[:160]}) @@ -67,11 +97,22 @@ def check(name,operation): 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} -(BASE/'184-acquisition-launcher-bootstrap-controls-result.json').write_text(json.dumps(report,sort_keys=True,indent=2)+'\n') +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 index e362069..d377224 100644 --- a/qualification/issue-184/tool-acquisition/184-acquisition-launcher-bootstrap-proposal.py +++ b/qualification/issue-184/tool-acquisition/184-acquisition-launcher-bootstrap-proposal.py @@ -13,6 +13,7 @@ from pathlib import Path import re import resource +import signal import stat import sys import time @@ -58,6 +59,7 @@ def configure_resources(affinity): 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): @@ -77,11 +79,41 @@ def launch(authority_path,expected_authority_sha,expected_decision,expected_sess 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 index 38d9d3d..8842f35 100644 --- a/qualification/issue-184/tool-acquisition/184-acquisition-session-controls-result.json +++ b/qualification/issue-184/tool-acquisition/184-acquisition-session-controls-result.json @@ -1,10 +1,10 @@ { "acquisition_accepted": false, - "cases": 18, + "cases": 22, "downloaded_wheel_imports_or_installs": 0, "failed": 0, "new_general_decision": null, - "passed": 18, + "passed": 22, "real_network_requests": 0, "results": [ { @@ -47,6 +47,22 @@ "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" @@ -82,5 +98,5 @@ ], "scope": "synthetic role-protocol controls; start/admitted-data API never called with a decision", "selected_archives_acquired": 0, - "source_sha256": "08773d12f2fe52249907eae97af4cf4d97db43f47125436d41ccaa70b8db0994" + "source_sha256": "e92aa088b9de553cbe2d0ec76dcb6d097d07148f6a9e9a751f80ab4680cc9c48" } diff --git a/qualification/issue-184/tool-acquisition/184-acquisition-session-controls.py b/qualification/issue-184/tool-acquisition/184-acquisition-session-controls.py index 5bde432..b3212b8 100644 --- a/qualification/issue-184/tool-acquisition/184-acquisition-session-controls.py +++ b/qualification/issue-184/tool-acquisition/184-acquisition-session-controls.py @@ -44,11 +44,12 @@ 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'}: + 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=='survivor': + 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(): @@ -80,7 +81,7 @@ 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=Path('/tmp/sdk-python-184-tools-14adc40b-20261105T035512Z-6d90ccfb71') + 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" @@ -131,6 +132,12 @@ def run_protocol(mode,disconnect=None): try: outer.close();os.environ.clear();os.environ.update(s.MINIMAL_ENV) s.POOL_PARENT=parent + 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): @@ -139,7 +146,7 @@ def stop_with_one_survivor(role,ceiling): return real_stop(role,ceiling) s.stop_owner=stop_with_one_survivor code=s.guardian_job(inner,authority_raw,authority,started,dt.datetime.now(dt.timezone.utc).isoformat(), - modules,bounds=(0.05 if mode=='survivor' else 1 if mode.startswith('integrated-') else 0.35,3)) + 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 @@ -169,10 +176,21 @@ def stop_with_one_survivor(role,ceiling): 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=='survivor': + if 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] @@ -189,6 +207,10 @@ def stop_with_one_survivor(role,ceiling): assert result['terminal_cleanup_complete'] and result['retained_custody_sealed'] 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' assert select.select([pidfd],[],[],0)[0] if not session.finished else True finally: if not reaped: @@ -215,6 +237,63 @@ def frame_control(raw,code): 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']['terminal_cleanup_complete'] and session.finished + 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=[] @@ -230,7 +309,7 @@ def fd_frame_roundtrip(): 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; 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__); 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')" + 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' @@ -260,6 +339,10 @@ def check(name,operation): check('survivor-keeps-all-custody-until-confirmed-deferred-exit',lambda:run_protocol('survivor')) check('actual-owner-and-terminal-compose-with-synthetic-six-tool-closure',lambda:run_protocol('integrated-positive')) check('actual-owner-transport-timeout-refuses-integrated-preserve',lambda:run_protocol('integrated-timeout')) + check('post-fork-pidfd-failure-exits-reserved-child-before-cleanup',lambda:run_protocol('pidfd-fail')) + check('unreported-live-owner-is-unknown-retains-custody',unreported_live_owner_keeps_custody) + check('stop-permission-refusal-still-requires-confirmed-exit',lambda:run_protocol('stop-refusal')) + check('late-quiescence-releases-custody-without-late-pass',lambda:run_protocol('late-reconcile')) check('outer-disappears-before-arming-no-owner-work',lambda:run_protocol('positive','register')) check('outer-disappears-with-blocked-owner-stops-before-work',lambda:run_protocol('positive','owner')) check('descriptor-frame-roundtrip-preserves-scoped-handle',fd_frame_roundtrip) @@ -275,5 +358,14 @@ def check(name,operation): '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,'acquisition_accepted':False} -(BASE/'184-acquisition-session-controls-result.json').write_text(json.dumps(report,sort_keys=True,indent=2)+'\n') +report_path=BASE/'184-acquisition-session-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-session-proposal.py b/qualification/issue-184/tool-acquisition/184-acquisition-session-proposal.py index 11d206d..e649e6b 100644 --- a/qualification/issue-184/tool-acquisition/184-acquisition-session-proposal.py +++ b/qualification/issue-184/tool-acquisition/184-acquisition-session-proposal.py @@ -30,7 +30,7 @@ INTERPRETER_SHA='52e0a13e60a981d8c4b6478be2ba5176f69da07948a056bf49cf6f077e30cb41' LIBRARY='/usr/lib/x86_64-linux-gnu/libpython3.14.so.1.0' LIBRARY_SHA='982cb3768d1d8b00126255ee2785bccf8360c68b197fb3dec95858ab56195f87' -PINNED={'owner': '234e099b5208aaafca379d3e5535cf44b3158a327764eec0ef4e12afcfc45ffb', 'bootstrap': '12679c567452dd4cdda7dbd789ac600ef0b5455a1225c95cf59d066fda472569', 'guardian': 'afc5f198de12d4b26520957bc563ff0122f8535838a1cbc64c40272c5f3f7f70', 'terminal': 'd050992a68c09db741fa4a0afda234c60e30a3412441b5dfce16be5e26efedce', 'selection': 'e9a7d639325b866569cdab83b81a2949437209a6bca91c881c2e365fdc573a1c', 'interpreter': '52e0a13e60a981d8c4b6478be2ba5176f69da07948a056bf49cf6f077e30cb41', 'library': '982cb3768d1d8b00126255ee2785bccf8360c68b197fb3dec95858ab56195f87', 'ca': '2ef52ad56b4803e264045c915e55c236cbfa045b84b81efefc218a19eb44bd2b'} +PINNED={'owner': '234e099b5208aaafca379d3e5535cf44b3158a327764eec0ef4e12afcfc45ffb', 'bootstrap': '6ca5da88054b4c3ea27b2614dfd987f88ecb3db981271a995bb9941d06398fed', 'guardian': 'afc5f198de12d4b26520957bc563ff0122f8535838a1cbc64c40272c5f3f7f70', 'terminal': '7e75b0929e612ec8605d095956fd446c2ad779a8f8b7e8403563f2764b1e70bf', 'selection': 'e9a7d639325b866569cdab83b81a2949437209a6bca91c881c2e365fdc573a1c', 'interpreter': '52e0a13e60a981d8c4b6478be2ba5176f69da07948a056bf49cf6f077e30cb41', 'library': '982cb3768d1d8b00126255ee2785bccf8360c68b197fb3dec95858ab56195f87', 'ca': '2ef52ad56b4803e264045c915e55c236cbfa045b84b81efefc218a19eb44bd2b'} FRAME_CAP=65536 POOL_CAP=4*1024**2 @@ -38,6 +38,12 @@ 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) @@ -82,6 +88,7 @@ def verify_startup(): '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) @@ -158,7 +165,7 @@ def progress_bound(custody): return total -def spawn_owner(bootstrap,decision,proof,custody): +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} @@ -194,17 +201,49 @@ def spawn_owner(bootstrap,decision,proof,custody): os.close(status_write);os.close(gate_read) for fd in borrowed.values():os.close(fd) os.set_blocking(status_read,False) - return {'pid':pid,'pidfd':os.pidfd_open(pid),'status':status_read,'gate':gate_write} + 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'): @@ -312,7 +351,7 @@ def guardian_job(channel,authority_raw,authority,started,started_utc,modules,bou channel.settimeout(max(0.001,min(10,active-time.monotonic()))) message,received=recv_frame(channel) require(not received and message=={'type':'ARM'},'GUARDIAN_ARM_MISMATCH') - role=spawn_owner(bootstrap,authority['decision'],proof,custody) + role=spawn_owner(bootstrap,authority['decision'],proof,custody,ceiling) send_frame(channel,{'type':'OWNER','pid':role['pid']},[role['pidfd']]) message,received=recv_frame(channel) require(not received and message=={'type':'RUN'},'OWNER_RUN_MISMATCH') @@ -351,6 +390,7 @@ def guardian_job(channel,authority_raw,authority,started,started_utc,modules,bou raise Refusal(reason) except BaseException as error: code=getattr(error,'code',None) + if isinstance(error,OwnerSetupRefusal):role,stop=error.role,error.stop if not isinstance(code,str): code=str(error) if isinstance(error,(Refusal,guard.Refusal,terminal.Refusal)) else '' reason=code if re.fullmatch(r'[A-Z0-9_]{1,128}',code) else 'GUARDIAN_'+type(error).__name__.upper() @@ -362,6 +402,11 @@ def guardian_job(channel,authority_raw,authority,started,started_utc,modules,bou result['owner_was_allocated']=False;result['owner_process_exited']=None else:result=cleanup_after_exit(custody,stop,reason,ceiling) if isinstance(owner_result,dict):result['owner_result']=owner_result + if role is not None: + result['owner_pid']=role['pid'];result['owner_pidfd_available']=role['pidfd'] is not None + result['owner_exit_confirmation']=('pidfd-and-parent-waitpid' if role['pidfd'] is not None + else 'reserved-child-waitpid') if stop['owner_exited'] else 'unconfirmed' + if stop.get('stop_refusal_code'):result['stop_refusal_code']=stop['stop_refusal_code'] if custody is not None and (role is None or stop['owner_exited']): try: record_terminal(custody,result,authority_raw);result['retained_terminal_record_written']=True @@ -373,8 +418,15 @@ def guardian_job(channel,authority_raw,authority,started,started_utc,modules,bou # Custody only: no work/retry or acceptance beyond the failed bound. # The separately admitted survivor policy must allow this keeper; # the outer session already holds duplicate inode and pidfd handles. - while not select.select([role['pidfd']],[],[],60)[0]:pass - stop=stop_owner(role,time.monotonic()+1) + if role['pidfd'] is None: + while True: + waited,status=os.waitpid(role['pid'],os.WNOHANG) + if waited: + stop={'owner_exited':True,'owner_exit_code':os.waitstatus_to_exitcode(status)};break + time.sleep(1) + else: + while not select.select([role['pidfd']],[],[],60)[0]:pass + stop=stop_owner(role,time.monotonic()+1) final=cleanup_after_exit(custody,stop,'DEFERRED_EXACT_OWNER_EXIT',ceiling) try:record_terminal(custody,final,authority_raw) except BaseException:final['terminal_cleanup_complete']=False @@ -384,7 +436,7 @@ def guardian_job(channel,authority_raw,authority,started,started_utc,modules,bou finally: if role is not None: for name in ('status','gate','pidfd'): - if role[name]>=0:os.close(role[name]) + if type(role[name]) is int and role[name]>=0:os.close(role[name]) if custody is not None:custody.close() @@ -435,6 +487,8 @@ def receive(self): 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'] @@ -467,18 +521,35 @@ def wait(self): 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('archive_cleanup',{}).get('complete') and record.get('retained_custody_sealed'): + 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: + 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: @@ -489,8 +560,10 @@ def abort(self,reason): 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,'guardian_process_exited':guardian_gone, + '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 return {'type':'TERMINAL','result':result} @@ -515,6 +588,8 @@ def start(authority_path,expected_sha,expected_decision,expected_session_sha,sta 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()) diff --git a/qualification/issue-184/tool-acquisition/184-acquisition-terminal-controls-result.json b/qualification/issue-184/tool-acquisition/184-acquisition-terminal-controls-result.json index fa0b744..f9637fe 100644 --- a/qualification/issue-184/tool-acquisition/184-acquisition-terminal-controls-result.json +++ b/qualification/issue-184/tool-acquisition/184-acquisition-terminal-controls-result.json @@ -74,5 +74,5 @@ ], "scope": "synthetic six-tool terminal readback, not actual input acquisition or launcher", "selected_archives_acquired": 0, - "source_sha256": "d050992a68c09db741fa4a0afda234c60e30a3412441b5dfce16be5e26efedce" + "source_sha256": "7e75b0929e612ec8605d095956fd446c2ad779a8f8b7e8403563f2764b1e70bf" } diff --git a/qualification/issue-184/tool-acquisition/184-acquisition-terminal-controls.py b/qualification/issue-184/tool-acquisition/184-acquisition-terminal-controls.py index 818ed64..a7591c5 100644 --- a/qualification/issue-184/tool-acquisition/184-acquisition-terminal-controls.py +++ b/qualification/issue-184/tool-acquisition/184-acquisition-terminal-controls.py @@ -2,7 +2,6 @@ from __future__ import annotations from contextlib import contextmanager -from copy import deepcopy import datetime as dt import hashlib import importlib.util @@ -19,7 +18,7 @@ sys.dont_write_bytecode=True BASE=Path('/tmp/sdk-python-handoff') -FAILED=Path('/tmp/sdk-python-184-tools-14adc40b-20261105T035512Z-6d90ccfb71') +FAILED=BASE/'control-inputs' REF='https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-9999999999' @@ -183,5 +182,14 @@ def check(name,operation): '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,'launcher_pending':True} -(BASE/'184-acquisition-terminal-controls-result.json').write_text(json.dumps(report,sort_keys=True,indent=2)+'\n') +report_path=BASE/'184-acquisition-terminal-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-terminal-proposal.py b/qualification/issue-184/tool-acquisition/184-acquisition-terminal-proposal.py index 97c8274..ff6f4b5 100644 --- a/qualification/issue-184/tool-acquisition/184-acquisition-terminal-proposal.py +++ b/qualification/issue-184/tool-acquisition/184-acquisition-terminal-proposal.py @@ -6,7 +6,6 @@ import json import os from pathlib import PurePosixPath -import re import stat import time diff --git a/qualification/issue-184/tool-acquisition/README.md b/qualification/issue-184/tool-acquisition/README.md index 61435e5..97309dd 100644 --- a/qualification/issue-184/tool-acquisition/README.md +++ b/qualification/issue-184/tool-acquisition/README.md @@ -1,37 +1,95 @@ -# Issue 184 guarded acquisition source composition - -**Unexecuted, not launch ready.** New General decision/CPU assignment remain null; -prestart failure controls and final whole-packet review are unfinished. No real -network, selected archive acquisition/import/install, Docker, SDK build or runtime -retry ran. This draft is review-only, stacked on14adc40b/tree9bdf4d38; do not merge -into either selected/frozen release ref. SDK source/pyproject/workflows stay unchanged. -Original H/wheel, offline PASS, connected PRODUCT-FAIL and failed data action retain -their original identities and evidence distinctions. - -The owner adopts four pinned directories/six archive fds before network access and -returns only a candidate. It validates exact previously reviewed CA bytes in-process -(SHA2ef52ad5/118certificates); subject facts are attributed to that byte identity. -The owner has no subprocess/fork/spawn/exec surface. Parser/transport/custody/trust -controls now total86;16 descriptor-handoff and16 independent readback controls pass. - -The session protocol keeps separate owner/guardian/controller handles, read-only -outer archive duplicates, bounded frames, exact resource/CPU startup, and setup -inside270s active/300s total. Normal preservation requires owner exit0, independent -six-tool/ZIP/metadata/dependency/request/inventory readback, guardian exit and a -second outer inventory readback. Survivors and missing handles retain custody. -The proposed survivor keeper policy needs explicit General review/admission; it is -not a grant to continue acquisition beyond the failed bound. Prestart/post-fork -negative cases remain in progress, so this is not a completed operation packet. - -Current private protocol controls:18, including real local roles, guardian death, -EOF/deadline, deferred survivor cleanup and integrated actual owner/readback with -a synthetic six-tool closure and fake HTTP.12 bootstrap refusal controls pass; -actual local pinned CPython3.14.4 -I-S/minimal-env/resource startup is source-tested. -The admitted-data start API is never called with a decision by these controls. -The authority template is deliberately inert. Direct source entry points refuse. - -All exact code/results are bound in source-manifest.json. Scripts retain explicit -lab4 review paths and original retained public-metadata/source countercontrols; -these forensic inputs are not silently recreated/fetched by this draft. Next work -is sdk-python's remaining prestart negatives and final Security source/input review, -then General's distinct decision. No release/tag/artifact/adoption carry exists. +# 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. +- `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. +- The accepted guardian-v2 primitive remains byte-identical to bounded source + PASS6658714041. That prior component PASS is not whole-packet acceptance. + +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. + +## 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. + +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 | +| Role protocol |22| real local roles, EOF/deadline, guardian loss, survivors, setup refusal, unknown owner, late nonpass and integrated fake HTTP | +| Launcher bootstrap |14| exact reads, 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 index dc76afd..5878e16 100644 --- a/qualification/issue-184/tool-acquisition/authority-template.json +++ b/qualification/issue-184/tool-acquisition/authority-template.json @@ -9,17 +9,17 @@ }, "host": "lab4", "inputs": { - "bootstrap": "12679c567452dd4cdda7dbd789ac600ef0b5455a1225c95cf59d066fda472569", + "bootstrap": "6ca5da88054b4c3ea27b2614dfd987f88ecb3db981271a995bb9941d06398fed", "ca": "2ef52ad56b4803e264045c915e55c236cbfa045b84b81efefc218a19eb44bd2b", "guardian": "afc5f198de12d4b26520957bc563ff0122f8535838a1cbc64c40272c5f3f7f70", "interpreter": "52e0a13e60a981d8c4b6478be2ba5176f69da07948a056bf49cf6f077e30cb41", "library": "982cb3768d1d8b00126255ee2785bccf8360c68b197fb3dec95858ab56195f87", "owner": "234e099b5208aaafca379d3e5535cf44b3158a327764eec0ef4e12afcfc45ffb", "selection": "e9a7d639325b866569cdab83b81a2949437209a6bca91c881c2e365fdc573a1c", - "terminal": "d050992a68c09db741fa4a0afda234c60e30a3412441b5dfce16be5e26efedce" + "terminal": "7e75b0929e612ec8605d095956fd446c2ad779a8f8b7e8403563f2764b1e70bf" }, "io_seconds": 270, - "launcher_sha256": "08773d12f2fe52249907eae97af4cf4d97db43f47125436d41ccaa70b8db0994", + "launcher_sha256": "e92aa088b9de553cbe2d0ec76dcb6d097d07148f6a9e9a751f80ab4680cc9c48", "pool_parent": "/home/lab4/fleet/tmp", "retries": 0, "selected_source": "14adc40b00b68cc8a3e1ae5647a885a42f273650", 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..9e1acbe --- /dev/null +++ b/qualification/issue-184/tool-acquisition/control-inputs/historical-source/manifest.json @@ -0,0 +1,29 @@ +{ + "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" + } + ], + "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/v2-owner.source b/qualification/issue-184/tool-acquisition/control-inputs/historical-source/v2-owner.source new file mode 100644 index 0000000..d2b1770 --- /dev/null +++ b/qualification/issue-184/tool-acquisition/control-inputs/historical-source/v2-owner.source @@ -0,0 +1,583 @@ +"""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-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..30ce46f --- /dev/null +++ b/qualification/issue-184/tool-acquisition/review-packet.json @@ -0,0 +1,111 @@ +{ + "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, + "host": "lab4, subject to fresh capacity/input/cache/name checks", + "inline_launcher_bootstrap_sha256": "1eeb491accbb7f129c8e4c329c238a26ef6cd929a6204e949d16becc604621a4", + "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, + "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, + "role_address_space_soft_bytes": { + "guardian": 134217728, + "outer": 134217728, + "owner": 402653184 + }, + "runtime_source_inputs": { + "bootstrap": "6ca5da88054b4c3ea27b2614dfd987f88ecb3db981271a995bb9941d06398fed", + "ca": "2ef52ad56b4803e264045c915e55c236cbfa045b84b81efefc218a19eb44bd2b", + "guardian": "afc5f198de12d4b26520957bc563ff0122f8535838a1cbc64c40272c5f3f7f70", + "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": "e92aa088b9de553cbe2d0ec76dcb6d097d07148f6a9e9a751f80ab4680cc9c48", + "source_control_results": { + "184-acquire-tools-v5-adapter-controls-result.json": { + "cases": 16, + "passed": 16, + "sha256": "e4fd09a7e1ddf5dd56c1ed556fa5b3ae303a9d3b281640d8c3b6d6bf4a7d62c9" + }, + "184-acquire-tools-v5-controls-result.json": { + "cases": 86, + "passed": 86, + "sha256": "2a84c6612fcd2fd57235f4844face425420dafb3217be7d6e82ef57cc6fab1cd" + }, + "184-acquisition-launcher-bootstrap-controls-result.json": { + "cases": 14, + "passed": 14, + "sha256": "0cf4499e090e8b70ed884feb44697a14c86a9036b93ef77a89a0d31f2ca9e0d7" + }, + "184-acquisition-session-controls-result.json": { + "cases": 22, + "passed": 22, + "sha256": "8a7b2fba91965fa10b97a60083bdc9e87b1d0d118b2bd88bd3ffad770e2a37a2" + }, + "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 index f7b79f8..2b36fce 100644 --- a/qualification/issue-184/tool-acquisition/source-manifest.json +++ b/qualification/issue-184/tool-acquisition/source-manifest.json @@ -1,5 +1,7 @@ { + "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": [ @@ -9,24 +11,24 @@ "sha256": "e4fd09a7e1ddf5dd56c1ed556fa5b3ae303a9d3b281640d8c3b6d6bf4a7d62c9" }, { - "bytes": 10083, + "bytes": 10474, "file": "184-acquire-tools-v5-adapter-controls.py", - "sha256": "7419231c1c1fd6dcba97fbb9de59d2fef454ea467c7f337d2068936065fcb7df" + "sha256": "bc7696baf8582fef27cfc0c7005d564fe3d613bb615035daee50b2cb21db79d7" }, { - "bytes": 2975, + "bytes": 2875, "file": "184-acquire-tools-v5-bootstrap.py", - "sha256": "12679c567452dd4cdda7dbd789ac600ef0b5455a1225c95cf59d066fda472569" + "sha256": "6ca5da88054b4c3ea27b2614dfd987f88ecb3db981271a995bb9941d06398fed" }, { - "bytes": 8307, + "bytes": 8301, "file": "184-acquire-tools-v5-controls-result.json", - "sha256": "c5719cbd5a3bb89606d81582b24c7ad28d3b0fde2d5fc747cefaeb3744d63ccb" + "sha256": "2a84c6612fcd2fd57235f4844face425420dafb3217be7d6e82ef57cc6fab1cd" }, { - "bytes": 39730, + "bytes": 40590, "file": "184-acquire-tools-v5-controls.py", - "sha256": "422a6de8471a1e2aca81b30cea13ab79cb1c5dc74cc0517492a5aabd0964c7f8" + "sha256": "61a08e8538b8b7ec7448c6d30a3c929d99d45771411d296e0b6e5b9224e64c59" }, { "bytes": 44676, @@ -44,76 +46,176 @@ "sha256": "cb819468f659c6235176fabcd29894dda552ffb85f0bf54664d83b7dcdd3d2e1" }, { - "bytes": 1462, + "bytes": 1665, "file": "184-acquisition-launcher-bootstrap-controls-result.json", - "sha256": "bb9e9256176a2bfbd40d820f83d1c4a2a31cf699a107cefbdf9aba4a4e1f2379" + "sha256": "0cf4499e090e8b70ed884feb44697a14c86a9036b93ef77a89a0d31f2ca9e0d7" }, { - "bytes": 3994, + "bytes": 6183, "file": "184-acquisition-launcher-bootstrap-controls.py", - "sha256": "18cf6ba98a233721465afbffc74ee7e2a9bacbf8c321f893291de0b3f6d8f777" + "sha256": "dd18efdc3d5e3cac03281be93fe55f7f0a68c2629bd06fb1b25105a328d2fa22" }, { - "bytes": 4110, + "bytes": 6104, "file": "184-acquisition-launcher-bootstrap-proposal.py", - "sha256": "9205ae8ea069fb12cc35ea28dbe5ced8a4ccbd2a1b12bb6d4ae76633816c2c36" + "sha256": "1eeb491accbb7f129c8e4c329c238a26ef6cd929a6204e949d16becc604621a4" }, { - "bytes": 2219, + "bytes": 2641, "file": "184-acquisition-session-controls-result.json", - "sha256": "05e16cd72a5b2c02006ef066197b82670fb9ef88d49c3cc90f0c97d18ec3cfbb" + "sha256": "8a7b2fba91965fa10b97a60083bdc9e87b1d0d118b2bd88bd3ffad770e2a37a2" }, { - "bytes": 16071, + "bytes": 22445, "file": "184-acquisition-session-controls.py", - "sha256": "597023f6dea906fe4ac180380970725fb4d07be011c523b128ef19cf7704d38c" + "sha256": "9716b0ca70ac8ec3d6495e930c9c347f7c6d39c210b300a669576637b856bca8" }, { - "bytes": 31294, + "bytes": 35654, "file": "184-acquisition-session-proposal.py", - "sha256": "08773d12f2fe52249907eae97af4cf4d97db43f47125436d41ccaa70b8db0994" + "sha256": "e92aa088b9de553cbe2d0ec76dcb6d097d07148f6a9e9a751f80ab4680cc9c48" }, { "bytes": 1826, "file": "184-acquisition-terminal-controls-result.json", - "sha256": "107046c7d947343430782ce6a17d06df2b4ec967b50b613b76fe48cad53f366d" + "sha256": "32a30a3767d760befae96f77f906431f234dd2733c28b17c3deadb71921f3be0" }, { - "bytes": 11856, + "bytes": 12198, "file": "184-acquisition-terminal-controls.py", - "sha256": "5dddcc0400bff258d471903c79cef9a868115cf2c14013a58e41a70f00739d24" + "sha256": "bf13c83983567ae04e21fe871625817f85b3bef0b3524b85576296e1267e3dff" }, { - "bytes": 12225, + "bytes": 12215, "file": "184-acquisition-terminal-proposal.py", - "sha256": "d050992a68c09db741fa4a0afda234c60e30a3412441b5dfce16be5e26efedce" + "sha256": "7e75b0929e612ec8605d095956fd446c2ad779a8f8b7e8403563f2764b1e70bf" }, { "bytes": 4550, "file": "184-immutable-build-tool-input-proposal.json", "sha256": "e9a7d639325b866569cdab83b81a2949437209a6bca91c881c2e365fdc573a1c" }, + { + "bytes": 6094, + "file": "README.md", + "sha256": "db6a476c92b7341f68ca06a31e30e8c8de4a6508adf7308e55449835b0b8dd8e" + }, { "bytes": 1175, "file": "authority-template.json", - "sha256": "1ac57d2edb84b98d9e6699f5ce1e195c26f566262d396ee626c7ae8fdefe0720" + "sha256": "df29ae23e96d0321204cebda714523a8a9489b9602c40ef902471c4de86b6c33" + }, + { + "bytes": 1170, + "file": "control-inputs/historical-source/manifest.json", + "sha256": "9ab040cbd1ecfe0890ff1a257bca76a411e8d8d0b4e05481cd4d3da652504e81" + }, + { + "bytes": 17804, + "file": "control-inputs/historical-source/original-owner.source", + "sha256": "e0b8955c8c26c68333e1439c4eb195ef803d710d68f4a4b297b3dd17fcfd7c54" + }, + { + "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": 4886, + "file": "review-packet.json", + "sha256": "d036d702a0fae12c46de62fd47bc5cad2fef5f40f597eac092f97afa8551a5fa" } ], + "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_source_work": [ - "post-fork pidfd prerequisite failure cleanup", - "unreported-owner-handle crash countercontrol", - "final exact composed source/input review packet" + "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" ], + "sdk_source_or_feature_cohort_changed": false, "selected_sdk_source": "14adc40b00b68cc8a3e1ae5647a885a42f273650", "source_control_results": { - "184-acquire-tools-v5-adapter-controls-result.json": 16, - "184-acquire-tools-v5-controls-result.json": 86, - "184-acquisition-launcher-bootstrap-controls-result.json": 12, - "184-acquisition-session-controls-result.json": 18, - "184-acquisition-terminal-controls-result.json": 16 + "184-acquire-tools-v5-adapter-controls-result.json": { + "cases": 16, + "passed": 16, + "sha256": "e4fd09a7e1ddf5dd56c1ed556fa5b3ae303a9d3b281640d8c3b6d6bf4a7d62c9" + }, + "184-acquire-tools-v5-controls-result.json": { + "cases": 86, + "passed": 86, + "sha256": "2a84c6612fcd2fd57235f4844face425420dafb3217be7d6e82ef57cc6fab1cd" + }, + "184-acquisition-launcher-bootstrap-controls-result.json": { + "cases": 14, + "passed": 14, + "sha256": "0cf4499e090e8b70ed884feb44697a14c86a9036b93ef77a89a0d31f2ca9e0d7" + }, + "184-acquisition-session-controls-result.json": { + "cases": 22, + "passed": 22, + "sha256": "8a7b2fba91965fa10b97a60083bdc9e87b1d0d118b2bd88bd3ffad770e2a37a2" + }, + "184-acquisition-terminal-controls-result.json": { + "cases": 16, + "passed": 16, + "sha256": "32a30a3767d760befae96f77f906431f234dd2733c28b17c3deadb71921f3be0" + } }, - "stage": "UNEXECUTED composed source checkpoint; prestart failure controls unfinished; not launch ready" + "stage": "UNEXECUTED composed whole-source/input review packet; no operational admission" } From 49dddb2857f469100ce52e4636292e5c98d17706 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Thu, 5 Nov 2026 15:55:49 +0000 Subject: [PATCH 4/8] Close guardian authority and whole-tree custody findings in source packet --- ...uire-tools-v5-adapter-controls-result.json | 2 +- .../184-acquire-tools-v5-adapter-controls.py | 12 +- ...cquisition-custody-guardian-v3-proposal.py | 353 ++++++++++++++++++ ...quisition-guardian-v3-controls-result.json | 225 +++++++++++ .../184-acquisition-guardian-v3-controls.py | 180 +++++++++ ...on-launcher-bootstrap-controls-result.json | 58 ++- ...acquisition-launcher-bootstrap-controls.py | 27 ++ ...acquisition-launcher-bootstrap-proposal.py | 25 +- ...4-acquisition-session-controls-result.json | 42 ++- .../184-acquisition-session-controls.py | 49 ++- .../184-acquisition-session-proposal.py | 124 ++++-- .../issue-184/tool-acquisition/README.md | 31 +- .../tool-acquisition/authority-template.json | 5 +- .../tool-acquisition/review-packet.json | 42 ++- .../tool-acquisition/source-manifest.json | 95 +++-- 15 files changed, 1184 insertions(+), 86 deletions(-) create mode 100644 qualification/issue-184/tool-acquisition/184-acquisition-custody-guardian-v3-proposal.py create mode 100644 qualification/issue-184/tool-acquisition/184-acquisition-guardian-v3-controls-result.json create mode 100644 qualification/issue-184/tool-acquisition/184-acquisition-guardian-v3-controls.py 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 index e8b5c65..f017a0a 100644 --- 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 @@ -2,7 +2,7 @@ "cases": 16, "downloaded_wheel_imports_or_installs": 0, "failed": 0, - "guardian_sha256": "afc5f198de12d4b26520957bc563ff0122f8535838a1cbc64c40272c5f3f7f70", + "guardian_sha256": "79ddc82b0095872885aceb943cbf9daa1eb3a05048bc08f9ee9cd093511de187", "owner_sha256": "234e099b5208aaafca379d3e5535cf44b3158a327764eec0ef4e12afcfc45ffb", "passed": 16, "real_network_requests": 0, 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 index 98316ba..9b31e91 100644 --- 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 @@ -26,9 +26,9 @@ def load(name,path): owner=load('trusted_v5_owner_adapter',BASE/'184-acquire-tools-v5.py') -guard=load('trusted_reviewed_guard_custody',BASE/'184-acquisition-custody-guardian-v2-proposal.py') -assert hashlib.sha256((BASE/'184-acquisition-custody-guardian-v2-proposal.py').read_bytes()).hexdigest()== \ - 'afc5f198de12d4b26520957bc563ff0122f8535838a1cbc64c40272c5f3f7f70' +guard=load('trusted_reviewed_guard_custody',BASE/'184-acquisition-custody-guardian-v3-proposal.py') +assert hashlib.sha256((BASE/'184-acquisition-custody-guardian-v3-proposal.py').read_bytes()).hexdigest()== \ + '79ddc82b0095872885aceb943cbf9daa1eb3a05048bc08f9ee9cd093511de187' results=[] @@ -132,7 +132,7 @@ def refuse_wrap(fd,mode):duplicate.append(fd);raise OSError('synthetic wrap fail 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()['complete'] + assert custody.cleanup_archives()['complete'] def late_failure_cleans_unused_preallocations(): @@ -140,7 +140,7 @@ def late_failure_cleans_unused_preallocations(): 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()['complete'] + assert not list((item.pool/'wheelhouse').iterdir()) and custody.cleanup_archives()['complete'] def replaced_archive_not_written(): @@ -187,7 +187,7 @@ def 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-v2-proposal.py').read_bytes()).hexdigest(), + 'guardian_sha256':hashlib.sha256((BASE/'184-acquisition-custody-guardian-v3-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, 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-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-launcher-bootstrap-controls-result.json b/qualification/issue-184/tool-acquisition/184-acquisition-launcher-bootstrap-controls-result.json index 5a0232e..e9dd499 100644 --- 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 @@ -1,11 +1,63 @@ { - "cases": 14, + "cases": 27, "downloaded_wheel_imports_or_installs": 0, "failed": 0, "new_general_decision": null, - "passed": 14, + "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" @@ -65,5 +117,5 @@ ], "scope": "bootstrap file/refusal controls; no admitted start or selected archive access", "selected_archives_acquired": 0, - "source_sha256": "1eeb491accbb7f129c8e4c329c238a26ef6cd929a6204e949d16becc604621a4" + "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 index 4e3eefb..7b41aa8 100644 --- a/qualification/issue-184/tool-acquisition/184-acquisition-launcher-bootstrap-controls.py +++ b/qualification/issue-184/tool-acquisition/184-acquisition-launcher-bootstrap-controls.py @@ -84,6 +84,29 @@ class BoundedControlStop(Exception):pass 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]}) @@ -91,6 +114,10 @@ def check(name,operation): 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) 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 index d377224..c31c476 100644 --- a/qualification/issue-184/tool-acquisition/184-acquisition-launcher-bootstrap-proposal.py +++ b/qualification/issue-184/tool-acquisition/184-acquisition-launcher-bootstrap-proposal.py @@ -35,6 +35,29 @@ def pairs(values): 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: @@ -70,7 +93,7 @@ def launch(authority_path,expected_authority_sha,expected_decision,expected_sess 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=json.loads(raw,object_pairs_hook=pairs) + 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) 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 index 8842f35..0d4c833 100644 --- a/qualification/issue-184/tool-acquisition/184-acquisition-session-controls-result.json +++ b/qualification/issue-184/tool-acquisition/184-acquisition-session-controls-result.json @@ -1,12 +1,48 @@ { "acquisition_accepted": false, - "cases": 22, + "cases": 31, "downloaded_wheel_imports_or_installs": 0, "failed": 0, "new_general_decision": null, - "passed": 22, + "passed": 31, "real_network_requests": 0, "results": [ + { + "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" @@ -98,5 +134,5 @@ ], "scope": "synthetic role-protocol controls; start/admitted-data API never called with a decision", "selected_archives_acquired": 0, - "source_sha256": "e92aa088b9de553cbe2d0ec76dcb6d097d07148f6a9e9a751f80ab4680cc9c48" + "source_sha256": "3ef6df413565d36f63b2e4722c7ee0bd9eb5c62a29a1d79b450acfa0b5ae8d3d" } diff --git a/qualification/issue-184/tool-acquisition/184-acquisition-session-controls.py b/qualification/issue-184/tool-acquisition/184-acquisition-session-controls.py index b3212b8..7286ce3 100644 --- a/qualification/issue-184/tool-acquisition/184-acquisition-session-controls.py +++ b/qualification/issue-184/tool-acquisition/184-acquisition-session-controls.py @@ -31,7 +31,7 @@ def load(name,path): s=load('trusted_session_protocol_control',BASE/'184-acquisition-session-proposal.py') -g=load('trusted_guard_protocol_control',BASE/'184-acquisition-custody-guardian-v2-proposal.py') +g=load('trusted_guard_protocol_control',BASE/'184-acquisition-custody-guardian-v3-proposal.py') 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()) @@ -124,6 +124,7 @@ def run_protocol(mode,disconnect=None): 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) @@ -132,6 +133,17 @@ def run_protocol(mode,disconnect=None): try: outer.close();os.environ.clear();os.environ.update(s.MINIMAL_ENV) s.POOL_PARENT=parent + 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 @@ -186,7 +198,16 @@ def stop_with_one_survivor(role,ceiling): else:message=session.wait() reaped=session.guardian_reaped result=message['result'];assert not result['acquisition_accepted'] - if mode=='late-reconcile': + if 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']=='ALLOCATION_REFUSED' 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()) @@ -196,7 +217,7 @@ def stop_with_one_survivor(role,ceiling): 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['terminal_cleanup_complete'] + 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 @@ -204,14 +225,16 @@ def stop_with_one_survivor(role,ceiling): assert (expected_pool/'operation-authority.json').read_bytes()==authority_raw else: assert result['status']=='REFUSED',result - assert result['terminal_cleanup_complete'] and result['retained_custody_sealed'] + 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' - assert select.select([pidfd],[],[],0)[0] if not session.finished else True + assert select.select([pidfd],[],[],0)[0] if not session.finished and mode!='alloc-unknown' else True finally: if not reaped: try:os.kill(child,signal.SIGKILL) @@ -283,7 +306,8 @@ def unreported_live_owner_keeps_custody(): 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']['terminal_cleanup_complete'] and session.finished + 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: @@ -322,6 +346,15 @@ def live_start_refuses_no_decision(): 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 check(name,operation): try:operation() except BaseException as error:results.append({'id':name,'outcome':'fail','type':type(error).__name__,'detail':str(error)[:600]}) @@ -329,6 +362,10 @@ def check(name,operation): with patch.object(socket,'create_connection',side_effect=AssertionError('NO_NETWORK_IN_SESSION_CONTROLS')): + for seconds,accepted in [(0,False),(300,False),(301,True),(86400,True),(86401,False)]: + check('retention-deadline-boundary-'+str(seconds),lambda seconds=seconds,accepted=accepted:retention_boundary(seconds,accepted)) + for mode in ('alloc-after-root','alloc-after-wheelhouse','alloc-after-metadata','alloc-unknown'): + check(mode+'-root-custody-is-not-discarded',lambda mode=mode:run_protocol(mode)) check('synthetic-owner-exit-independent-check-and-outer-readback',lambda:run_protocol('positive')) check('owner-failure-is-refusal-zero-archives',lambda:run_protocol('fail')) check('active-deadline-stops-and-reaps-exact-owner',lambda:run_protocol('block')) diff --git a/qualification/issue-184/tool-acquisition/184-acquisition-session-proposal.py b/qualification/issue-184/tool-acquisition/184-acquisition-session-proposal.py index e649e6b..fac5537 100644 --- a/qualification/issue-184/tool-acquisition/184-acquisition-session-proposal.py +++ b/qualification/issue-184/tool-acquisition/184-acquisition-session-proposal.py @@ -8,6 +8,7 @@ from array import array from contextlib import ExitStack +import datetime as dt import hashlib import json import os @@ -30,7 +31,7 @@ INTERPRETER_SHA='52e0a13e60a981d8c4b6478be2ba5176f69da07948a056bf49cf6f077e30cb41' LIBRARY='/usr/lib/x86_64-linux-gnu/libpython3.14.so.1.0' LIBRARY_SHA='982cb3768d1d8b00126255ee2785bccf8360c68b197fb3dec95858ab56195f87' -PINNED={'owner': '234e099b5208aaafca379d3e5535cf44b3158a327764eec0ef4e12afcfc45ffb', 'bootstrap': '6ca5da88054b4c3ea27b2614dfd987f88ecb3db981271a995bb9941d06398fed', 'guardian': 'afc5f198de12d4b26520957bc563ff0122f8535838a1cbc64c40272c5f3f7f70', 'terminal': '7e75b0929e612ec8605d095956fd446c2ad779a8f8b7e8403563f2764b1e70bf', 'selection': 'e9a7d639325b866569cdab83b81a2949437209a6bca91c881c2e365fdc573a1c', 'interpreter': '52e0a13e60a981d8c4b6478be2ba5176f69da07948a056bf49cf6f077e30cb41', 'library': '982cb3768d1d8b00126255ee2785bccf8360c68b197fb3dec95858ab56195f87', 'ca': '2ef52ad56b4803e264045c915e55c236cbfa045b84b81efefc218a19eb44bd2b'} +PINNED={'owner': '234e099b5208aaafca379d3e5535cf44b3158a327764eec0ef4e12afcfc45ffb', 'bootstrap': '6ca5da88054b4c3ea27b2614dfd987f88ecb3db981271a995bb9941d06398fed', 'guardian': '79ddc82b0095872885aceb943cbf9daa1eb3a05048bc08f9ee9cd093511de187', 'terminal': '7e75b0929e612ec8605d095956fd446c2ad779a8f8b7e8403563f2764b1e70bf', 'selection': 'e9a7d639325b866569cdab83b81a2949437209a6bca91c881c2e365fdc573a1c', 'interpreter': '52e0a13e60a981d8c4b6478be2ba5176f69da07948a056bf49cf6f077e30cb41', 'library': '982cb3768d1d8b00126255ee2785bccf8360c68b197fb3dec95858ab56195f87', 'ca': '2ef52ad56b4803e264045c915e55c236cbfa045b84b81efefc218a19eb44bd2b'} FRAME_CAP=65536 POOL_CAP=4*1024**2 @@ -127,12 +128,28 @@ def prepare_tree(guard,parent,leaf,filenames): try: for name in ('metadata','events'): os.mkdir(name,0o700,dir_fd=custody.root_fd) + 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.stack.callback(os.close,fd) - st=os.fstat(fd);custody.dir_ids[name]=[st.st_dev,st.st_ino];custody.directory_fds[name]=fd + st=os.fstat(fd) + require([st.st_dev,st.st_ino]==custody.dir_ids[name],'DIRECTORY_ALLOCATION_COLLISION') + custody.directory_fds[name]=fd return custody - except BaseException: - custody.cleanup();custody.close();raise + 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): @@ -307,7 +324,7 @@ def cleanup_after_exit(custody,stop,reason,ceiling): '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() + 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) @@ -316,7 +333,22 @@ def cleanup_after_exit(custody,stop,reason,ceiling): return {'status':'REFUSED','stop_reason':reason,'owner_process_exited':True, 'owner_exit_code':stop['owner_exit_code'],'archive_cleanup':cleanup, 'within_cleanup_ceiling':within,'retained_custody_sealed':sealed, - 'terminal_cleanup_complete':bool(cleanup['complete']) and sealed and within,'acquisition_accepted':False} + 'terminal_cleanup_complete':False,'root_tree_retained':True, + 'outer_owner_custody_required':True,'retention_owner':'sdk-python', + 'retained_forensic_custody_ready':bool(cleanup['complete']) and sealed, + 'acquisition_accepted':False} + + +def retained_inventory(custody,terminal): + current=terminal.snapshot(custody) + return {name:{key:value for key,value in row.items() if key!='raw'} for name,row in current.items()} + + +def verify_retention_deadline(value,wall_now=None): + now=time.time() if wall_now is None else wall_now + require(type(value) is str,'EXACT_RETENTION_DEADLINE_REQUIRED') + retention=dt.datetime.strptime(value,'%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=dt.timezone.utc) + require(now+300 Date: Thu, 5 Nov 2026 16:31:39 +0000 Subject: [PATCH 5/8] Require tracked descendant unlink proof before root cleanup completion --- ...uire-tools-v5-adapter-controls-result.json | 2 +- .../184-acquire-tools-v5-adapter-controls.py | 8 +- ...cquisition-custody-guardian-v4-proposal.py | 387 ++++++++++++++++++ ...quisition-guardian-v4-controls-result.json | 261 ++++++++++++ .../184-acquisition-guardian-v4-controls.py | 259 ++++++++++++ ...4-acquisition-session-controls-result.json | 2 +- .../184-acquisition-session-controls.py | 2 +- .../184-acquisition-session-proposal.py | 5 +- .../issue-184/tool-acquisition/README.md | 14 +- .../tool-acquisition/authority-template.json | 4 +- .../tool-acquisition/review-packet.json | 28 +- .../tool-acquisition/source-manifest.json | 58 ++- 12 files changed, 984 insertions(+), 46 deletions(-) create mode 100644 qualification/issue-184/tool-acquisition/184-acquisition-custody-guardian-v4-proposal.py create mode 100644 qualification/issue-184/tool-acquisition/184-acquisition-guardian-v4-controls-result.json create mode 100644 qualification/issue-184/tool-acquisition/184-acquisition-guardian-v4-controls.py 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 index f017a0a..26b82af 100644 --- 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 @@ -2,7 +2,7 @@ "cases": 16, "downloaded_wheel_imports_or_installs": 0, "failed": 0, - "guardian_sha256": "79ddc82b0095872885aceb943cbf9daa1eb3a05048bc08f9ee9cd093511de187", + "guardian_sha256": "d52bb389127cb81b5b0303b3a593ee1af74f8c4835793d67f08d0bf182e2101c", "owner_sha256": "234e099b5208aaafca379d3e5535cf44b3158a327764eec0ef4e12afcfc45ffb", "passed": 16, "real_network_requests": 0, 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 index 9b31e91..3fc8292 100644 --- 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 @@ -26,9 +26,9 @@ def load(name,path): owner=load('trusted_v5_owner_adapter',BASE/'184-acquire-tools-v5.py') -guard=load('trusted_reviewed_guard_custody',BASE/'184-acquisition-custody-guardian-v3-proposal.py') -assert hashlib.sha256((BASE/'184-acquisition-custody-guardian-v3-proposal.py').read_bytes()).hexdigest()== \ - '79ddc82b0095872885aceb943cbf9daa1eb3a05048bc08f9ee9cd093511de187' +guard=load('trusted_reviewed_guard_custody',BASE/'184-acquisition-custody-guardian-v4-proposal.py') +assert hashlib.sha256((BASE/'184-acquisition-custody-guardian-v4-proposal.py').read_bytes()).hexdigest()== \ + 'd52bb389127cb81b5b0303b3a593ee1af74f8c4835793d67f08d0bf182e2101c' results=[] @@ -187,7 +187,7 @@ def 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-v3-proposal.py').read_bytes()).hexdigest(), + 'guardian_sha256':hashlib.sha256((BASE/'184-acquisition-custody-guardian-v4-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, 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-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-session-controls-result.json b/qualification/issue-184/tool-acquisition/184-acquisition-session-controls-result.json index 0d4c833..809ab81 100644 --- a/qualification/issue-184/tool-acquisition/184-acquisition-session-controls-result.json +++ b/qualification/issue-184/tool-acquisition/184-acquisition-session-controls-result.json @@ -134,5 +134,5 @@ ], "scope": "synthetic role-protocol controls; start/admitted-data API never called with a decision", "selected_archives_acquired": 0, - "source_sha256": "3ef6df413565d36f63b2e4722c7ee0bd9eb5c62a29a1d79b450acfa0b5ae8d3d" + "source_sha256": "4ed6698d3c6f93ea5040312c7c17f067aa8b1642376e00bb07f9b4302d51266f" } diff --git a/qualification/issue-184/tool-acquisition/184-acquisition-session-controls.py b/qualification/issue-184/tool-acquisition/184-acquisition-session-controls.py index 7286ce3..2ae7d22 100644 --- a/qualification/issue-184/tool-acquisition/184-acquisition-session-controls.py +++ b/qualification/issue-184/tool-acquisition/184-acquisition-session-controls.py @@ -31,7 +31,7 @@ def load(name,path): s=load('trusted_session_protocol_control',BASE/'184-acquisition-session-proposal.py') -g=load('trusted_guard_protocol_control',BASE/'184-acquisition-custody-guardian-v3-proposal.py') +g=load('trusted_guard_protocol_control',BASE/'184-acquisition-custody-guardian-v4-proposal.py') 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()) diff --git a/qualification/issue-184/tool-acquisition/184-acquisition-session-proposal.py b/qualification/issue-184/tool-acquisition/184-acquisition-session-proposal.py index fac5537..5e3b2f3 100644 --- a/qualification/issue-184/tool-acquisition/184-acquisition-session-proposal.py +++ b/qualification/issue-184/tool-acquisition/184-acquisition-session-proposal.py @@ -31,7 +31,7 @@ INTERPRETER_SHA='52e0a13e60a981d8c4b6478be2ba5176f69da07948a056bf49cf6f077e30cb41' LIBRARY='/usr/lib/x86_64-linux-gnu/libpython3.14.so.1.0' LIBRARY_SHA='982cb3768d1d8b00126255ee2785bccf8360c68b197fb3dec95858ab56195f87' -PINNED={'owner': '234e099b5208aaafca379d3e5535cf44b3158a327764eec0ef4e12afcfc45ffb', 'bootstrap': '6ca5da88054b4c3ea27b2614dfd987f88ecb3db981271a995bb9941d06398fed', 'guardian': '79ddc82b0095872885aceb943cbf9daa1eb3a05048bc08f9ee9cd093511de187', 'terminal': '7e75b0929e612ec8605d095956fd446c2ad779a8f8b7e8403563f2764b1e70bf', 'selection': 'e9a7d639325b866569cdab83b81a2949437209a6bca91c881c2e365fdc573a1c', 'interpreter': '52e0a13e60a981d8c4b6478be2ba5176f69da07948a056bf49cf6f077e30cb41', 'library': '982cb3768d1d8b00126255ee2785bccf8360c68b197fb3dec95858ab56195f87', 'ca': '2ef52ad56b4803e264045c915e55c236cbfa045b84b81efefc218a19eb44bd2b'} +PINNED={'owner': '234e099b5208aaafca379d3e5535cf44b3158a327764eec0ef4e12afcfc45ffb', 'bootstrap': '6ca5da88054b4c3ea27b2614dfd987f88ecb3db981271a995bb9941d06398fed', 'guardian': 'd52bb389127cb81b5b0303b3a593ee1af74f8c4835793d67f08d0bf182e2101c', 'terminal': '7e75b0929e612ec8605d095956fd446c2ad779a8f8b7e8403563f2764b1e70bf', 'selection': 'e9a7d639325b866569cdab83b81a2949437209a6bca91c881c2e365fdc573a1c', 'interpreter': '52e0a13e60a981d8c4b6478be2ba5176f69da07948a056bf49cf6f077e30cb41', 'library': '982cb3768d1d8b00126255ee2785bccf8360c68b197fb3dec95858ab56195f87', 'ca': '2ef52ad56b4803e264045c915e55c236cbfa045b84b81efefc218a19eb44bd2b'} FRAME_CAP=65536 POOL_CAP=4*1024**2 @@ -128,6 +128,7 @@ def prepare_tree(guard,parent,leaf,filenames): 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) @@ -654,7 +655,7 @@ def start(authority_path,expected_sha,expected_decision,expected_session_sha,sta 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-v3-proposal.py',PINNED['guardian']) + guard=module('reviewed_guardian','184-acquisition-custody-guardian-v4-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) diff --git a/qualification/issue-184/tool-acquisition/README.md b/qualification/issue-184/tool-acquisition/README.md index 8df46e4..b0db385 100644 --- a/qualification/issue-184/tool-acquisition/README.md +++ b/qualification/issue-184/tool-acquisition/README.md @@ -29,10 +29,14 @@ their original identities. Whole-source PASS does not qualify or replace them. - `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-v3 closes the two later findings6660476483/6662253657: full root-tree - cleanup and partial-allocation custody, plus unambiguous authority schemas. - Frozen guardian-v2 remains byte-identical as a predecessor countercontrol. Its - earlier owner-exit-only PASS does not clear these later findings or this packet. +- Guardian-v4 proposes the additional completion correction6663237183. Missing + root completion requires every tracked directory/archive descriptor to have + the exact identity/type/owner and zero links. A moved child or a directory + created before its identity can be read remains unproved custody. +- Frozen guardian-v2/v3 remain byte-identical predecessor countercontrols. + V3 closed findings6660476483/6662253657 (partial allocation and authority + parsing), but its later moved-child HOLD supersedes whole-packet PASS6662795537. + Exact successor review is pending; no earlier PASS carries to this packet. Normal preservation requires owner exit0, independent full input readback, guardian exit and a second outer inventory comparison. Result is collected for @@ -89,7 +93,7 @@ admit this residual policy; it is not permission supplied by these sources. | 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 |53| actual v2 counterexamples, authority grammar/types, whole-tree removal, allocation failure and unknown-member retention | +| Guardian successor |62| retained v2 controls, actual v3 moved-child/partial-identity false-completion counterexamples, fully-unlinked positive, authority grammar/types and whole-tree custody | | Role protocol |31| real local roles, allocation rollback/custody, EOF/deadline, guardian loss, survivors, setup refusal, unknown owner, late nonpass and integrated fake HTTP | | Launcher bootstrap |27| exact reads, authority schemas, startup refusals, resource affinity and custody wrapper | diff --git a/qualification/issue-184/tool-acquisition/authority-template.json b/qualification/issue-184/tool-acquisition/authority-template.json index 239c62a..5f60fa3 100644 --- a/qualification/issue-184/tool-acquisition/authority-template.json +++ b/qualification/issue-184/tool-acquisition/authority-template.json @@ -11,7 +11,7 @@ "inputs": { "bootstrap": "6ca5da88054b4c3ea27b2614dfd987f88ecb3db981271a995bb9941d06398fed", "ca": "2ef52ad56b4803e264045c915e55c236cbfa045b84b81efefc218a19eb44bd2b", - "guardian": "79ddc82b0095872885aceb943cbf9daa1eb3a05048bc08f9ee9cd093511de187", + "guardian": "d52bb389127cb81b5b0303b3a593ee1af74f8c4835793d67f08d0bf182e2101c", "interpreter": "52e0a13e60a981d8c4b6478be2ba5176f69da07948a056bf49cf6f077e30cb41", "library": "982cb3768d1d8b00126255ee2785bccf8360c68b197fb3dec95858ab56195f87", "owner": "234e099b5208aaafca379d3e5535cf44b3158a327764eec0ef4e12afcfc45ffb", @@ -19,7 +19,7 @@ "terminal": "7e75b0929e612ec8605d095956fd446c2ad779a8f8b7e8403563f2764b1e70bf" }, "io_seconds": 270, - "launcher_sha256": "3ef6df413565d36f63b2e4722c7ee0bd9eb5c62a29a1d79b450acfa0b5ae8d3d", + "launcher_sha256": "4ed6698d3c6f93ea5040312c7c17f067aa8b1642376e00bb07f9b4302d51266f", "pool_parent": "/home/lab4/fleet/tmp", "retention_deadline_utc": null, "retries": 0, diff --git a/qualification/issue-184/tool-acquisition/review-packet.json b/qualification/issue-184/tool-acquisition/review-packet.json index 036909d..7ce37f2 100644 --- a/qualification/issue-184/tool-acquisition/review-packet.json +++ b/qualification/issue-184/tool-acquisition/review-packet.json @@ -7,13 +7,21 @@ "connected_retry_admission": null, "cpu_affinity": null, "downloaded_wheel_imports_or_installs": 0, + "frozen_predecessor_control_result": { + "cases": 53, + "passed": 53, + "sha256": "30c7d4815ea68410ac839351bab09f10ff9591e9395151d26ce1a917d293b65d" + }, "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-6662253657", + "https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-6663237183" ], "predecessor_v2_sha256": "afc5f198de12d4b26520957bc563ff0122f8535838a1cbc64c40272c5f3f7f70", - "proposed_successor": "guardian-v3; whole-packet source review pending" + "predecessor_v3_sha256": "79ddc82b0095872885aceb943cbf9daa1eb3a05048bc08f9ee9cd093511de187", + "proposed_successor": "guardian-v4; tracked descendants must all be unlinked before root-absence completion; exact successor 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", @@ -60,7 +68,7 @@ "runtime_source_inputs": { "bootstrap": "6ca5da88054b4c3ea27b2614dfd987f88ecb3db981271a995bb9941d06398fed", "ca": "2ef52ad56b4803e264045c915e55c236cbfa045b84b81efefc218a19eb44bd2b", - "guardian": "79ddc82b0095872885aceb943cbf9daa1eb3a05048bc08f9ee9cd093511de187", + "guardian": "d52bb389127cb81b5b0303b3a593ee1af74f8c4835793d67f08d0bf182e2101c", "interpreter": "52e0a13e60a981d8c4b6478be2ba5176f69da07948a056bf49cf6f077e30cb41", "library": "982cb3768d1d8b00126255ee2785bccf8360c68b197fb3dec95858ab56195f87", "owner": "234e099b5208aaafca379d3e5535cf44b3158a327764eec0ef4e12afcfc45ffb", @@ -72,22 +80,22 @@ "selected_archive_bytes": 2901292, "selected_archives_acquired": 0, "selected_sdk_source": "14adc40b00b68cc8a3e1ae5647a885a42f273650", - "session_sha256": "3ef6df413565d36f63b2e4722c7ee0bd9eb5c62a29a1d79b450acfa0b5ae8d3d", + "session_sha256": "4ed6698d3c6f93ea5040312c7c17f067aa8b1642376e00bb07f9b4302d51266f", "source_control_results": { "184-acquire-tools-v5-adapter-controls-result.json": { "cases": 16, "passed": 16, - "sha256": "bd63b42f53a018ca93ce9c115dd667c074a7cbace2a7213cf1fe6bbb6d465150" + "sha256": "a33efea734891ebdf0cf196f082b6d0d153b25e7c0bbc7206a3b0ca2d9d3251e" }, "184-acquire-tools-v5-controls-result.json": { "cases": 86, "passed": 86, "sha256": "2a84c6612fcd2fd57235f4844face425420dafb3217be7d6e82ef57cc6fab1cd" }, - "184-acquisition-guardian-v3-controls-result.json": { - "cases": 53, - "passed": 53, - "sha256": "30c7d4815ea68410ac839351bab09f10ff9591e9395151d26ce1a917d293b65d" + "184-acquisition-guardian-v4-controls-result.json": { + "cases": 62, + "passed": 62, + "sha256": "2c7ee85f4b7b814cab4a9c78c1d150723d86f9cb8b78185644031cd775dc7a73" }, "184-acquisition-launcher-bootstrap-controls-result.json": { "cases": 27, @@ -97,7 +105,7 @@ "184-acquisition-session-controls-result.json": { "cases": 31, "passed": 31, - "sha256": "df2cfe26d296769d938d446a202f3dcfab780c30d3b9894dd348fee943e9de81" + "sha256": "211c0e96505d19a6f24a4f2d8c32bcbc1623018f10185413402edabb7ef3cd93" }, "184-acquisition-terminal-controls-result.json": { "cases": 16, diff --git a/qualification/issue-184/tool-acquisition/source-manifest.json b/qualification/issue-184/tool-acquisition/source-manifest.json index d354afd..8a249db 100644 --- a/qualification/issue-184/tool-acquisition/source-manifest.json +++ b/qualification/issue-184/tool-acquisition/source-manifest.json @@ -8,12 +8,12 @@ { "bytes": 1898, "file": "184-acquire-tools-v5-adapter-controls-result.json", - "sha256": "bd63b42f53a018ca93ce9c115dd667c074a7cbace2a7213cf1fe6bbb6d465150" + "sha256": "a33efea734891ebdf0cf196f082b6d0d153b25e7c0bbc7206a3b0ca2d9d3251e" }, { "bytes": 10492, "file": "184-acquire-tools-v5-adapter-controls.py", - "sha256": "0f4f521580b491016251f0fb9904ee86a33a5274d2acd64e6d1b2a10b760a8af" + "sha256": "d6f18f1ad8cb1a561a96a6528925ed55f8e551eb27e5ca8ef6d64b92ee1ae017" }, { "bytes": 2875, @@ -45,6 +45,11 @@ "file": "184-acquisition-custody-guardian-v3-proposal.py", "sha256": "79ddc82b0095872885aceb943cbf9daa1eb3a05048bc08f9ee9cd093511de187" }, + { + "bytes": 20980, + "file": "184-acquisition-custody-guardian-v4-proposal.py", + "sha256": "d52bb389127cb81b5b0303b3a593ee1af74f8c4835793d67f08d0bf182e2101c" + }, { "bytes": 4928, "file": "184-acquisition-guardian-v3-controls-result.json", @@ -55,6 +60,16 @@ "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": 1010, "file": "184-acquisition-interpreter-authority-proposal.json", @@ -78,17 +93,17 @@ { "bytes": 3466, "file": "184-acquisition-session-controls-result.json", - "sha256": "df2cfe26d296769d938d446a202f3dcfab780c30d3b9894dd348fee943e9de81" + "sha256": "211c0e96505d19a6f24a4f2d8c32bcbc1623018f10185413402edabb7ef3cd93" }, { "bytes": 25506, "file": "184-acquisition-session-controls.py", - "sha256": "93a30d336c2a702b5bd06ea7e5c3226eb2143a8e995efc51858b80f4e301eac2" + "sha256": "f389d59341e68d709c566530a65ec849f9da99fb70b2ce5d15df7548479622bc" }, { - "bytes": 41271, + "bytes": 41327, "file": "184-acquisition-session-proposal.py", - "sha256": "3ef6df413565d36f63b2e4722c7ee0bd9eb5c62a29a1d79b450acfa0b5ae8d3d" + "sha256": "4ed6698d3c6f93ea5040312c7c17f067aa8b1642376e00bb07f9b4302d51266f" }, { "bytes": 1826, @@ -111,14 +126,14 @@ "sha256": "e9a7d639325b866569cdab83b81a2949437209a6bca91c881c2e365fdc573a1c" }, { - "bytes": 7865, + "bytes": 8202, "file": "README.md", - "sha256": "75c1b6b3ffa1289d10567ce0545c0e9d55e2f7ff7e0c811342cc5a43fb1ddf27" + "sha256": "1acf83f7aa9daa9c3c1705b72b9caa216d3b6e54da26bc6ce8a5b11e78f8eae9" }, { "bytes": 1209, "file": "authority-template.json", - "sha256": "d42c600f69d79e745854d76ef8c7c89de2bdb6b737178deb46810c66c69cd6fb" + "sha256": "74910b72df111bb7bc327486d08b82b5938cee027e43a0931eae0fca1022da82" }, { "bytes": 1170, @@ -186,18 +201,21 @@ "sha256": "e9a7d639325b866569cdab83b81a2949437209a6bca91c881c2e365fdc573a1c" }, { - "bytes": 5857, + "bytes": 6397, "file": "review-packet.json", - "sha256": "3f3ac204bb359d86512965704f1fdf0077acb994b40edf1789ecafc31f10ceaa" + "sha256": "2d124b2c38175808f71034830971ca2596a28ca8a3d3e93eaa10c4e5536612d9" } ], "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-6662253657", + "https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-6663237183" ], "predecessor_v2_sha256": "afc5f198de12d4b26520957bc563ff0122f8535838a1cbc64c40272c5f3f7f70", - "proposed_successor": "guardian-v3; whole-packet source review pending" + "predecessor_v3_sha256": "79ddc82b0095872885aceb943cbf9daa1eb3a05048bc08f9ee9cd093511de187", + "proposed_successor": "guardian-v4; tracked descendants must all be unlinked before root-absence completion; exact successor 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, @@ -226,17 +244,17 @@ "184-acquire-tools-v5-adapter-controls-result.json": { "cases": 16, "passed": 16, - "sha256": "bd63b42f53a018ca93ce9c115dd667c074a7cbace2a7213cf1fe6bbb6d465150" + "sha256": "a33efea734891ebdf0cf196f082b6d0d153b25e7c0bbc7206a3b0ca2d9d3251e" }, "184-acquire-tools-v5-controls-result.json": { "cases": 86, "passed": 86, "sha256": "2a84c6612fcd2fd57235f4844face425420dafb3217be7d6e82ef57cc6fab1cd" }, - "184-acquisition-guardian-v3-controls-result.json": { - "cases": 53, - "passed": 53, - "sha256": "30c7d4815ea68410ac839351bab09f10ff9591e9395151d26ce1a917d293b65d" + "184-acquisition-guardian-v4-controls-result.json": { + "cases": 62, + "passed": 62, + "sha256": "2c7ee85f4b7b814cab4a9c78c1d150723d86f9cb8b78185644031cd775dc7a73" }, "184-acquisition-launcher-bootstrap-controls-result.json": { "cases": 27, @@ -246,7 +264,7 @@ "184-acquisition-session-controls-result.json": { "cases": 31, "passed": 31, - "sha256": "df2cfe26d296769d938d446a202f3dcfab780c30d3b9894dd348fee943e9de81" + "sha256": "211c0e96505d19a6f24a4f2d8c32bcbc1623018f10185413402edabb7ef3cd93" }, "184-acquisition-terminal-controls-result.json": { "cases": 16, @@ -254,5 +272,5 @@ "sha256": "32a30a3767d760befae96f77f906431f234dd2733c28b17c3deadb71921f3be0" } }, - "stage": "UNEXECUTED successor source/input review packet; proposed corrections for6660476483/6662253657; no operational admission" + "stage": "UNEXECUTED guardian-v4 successor for6663237183; prior49dd source PASS superseded; exact review pending; no operational admission" } From 4c96e9dcf995fbf4148377c03eb2880cec7b9751 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Thu, 5 Nov 2026 16:41:30 +0000 Subject: [PATCH 6/8] Track raw archive custody before identity reads in guarded acquisition source --- ...uire-tools-v5-adapter-controls-result.json | 2 +- .../184-acquire-tools-v5-adapter-controls.py | 8 +- ...cquisition-custody-guardian-v5-proposal.py | 392 ++++++++++++++++++ ...quisition-guardian-v5-controls-result.json | 277 +++++++++++++ .../184-acquisition-guardian-v5-controls.py | 307 ++++++++++++++ ...4-acquisition-session-controls-result.json | 2 +- .../184-acquisition-session-controls.py | 2 +- .../184-acquisition-session-proposal.py | 4 +- .../issue-184/tool-acquisition/README.md | 18 +- .../tool-acquisition/authority-template.json | 4 +- .../tool-acquisition/review-packet.json | 27 +- .../tool-acquisition/source-manifest.json | 55 ++- 12 files changed, 1052 insertions(+), 46 deletions(-) create mode 100644 qualification/issue-184/tool-acquisition/184-acquisition-custody-guardian-v5-proposal.py create mode 100644 qualification/issue-184/tool-acquisition/184-acquisition-guardian-v5-controls-result.json create mode 100644 qualification/issue-184/tool-acquisition/184-acquisition-guardian-v5-controls.py 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 index 26b82af..2a716a9 100644 --- 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 @@ -2,7 +2,7 @@ "cases": 16, "downloaded_wheel_imports_or_installs": 0, "failed": 0, - "guardian_sha256": "d52bb389127cb81b5b0303b3a593ee1af74f8c4835793d67f08d0bf182e2101c", + "guardian_sha256": "4c92d889d89dbef2502ef5e9bc923d00c0314821b42aa77d4c0ee92080aba41e", "owner_sha256": "234e099b5208aaafca379d3e5535cf44b3158a327764eec0ef4e12afcfc45ffb", "passed": 16, "real_network_requests": 0, 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 index 3fc8292..64f6b15 100644 --- 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 @@ -26,9 +26,9 @@ def load(name,path): owner=load('trusted_v5_owner_adapter',BASE/'184-acquire-tools-v5.py') -guard=load('trusted_reviewed_guard_custody',BASE/'184-acquisition-custody-guardian-v4-proposal.py') -assert hashlib.sha256((BASE/'184-acquisition-custody-guardian-v4-proposal.py').read_bytes()).hexdigest()== \ - 'd52bb389127cb81b5b0303b3a593ee1af74f8c4835793d67f08d0bf182e2101c' +guard=load('trusted_reviewed_guard_custody',BASE/'184-acquisition-custody-guardian-v5-proposal.py') +assert hashlib.sha256((BASE/'184-acquisition-custody-guardian-v5-proposal.py').read_bytes()).hexdigest()== \ + '4c92d889d89dbef2502ef5e9bc923d00c0314821b42aa77d4c0ee92080aba41e' results=[] @@ -187,7 +187,7 @@ def 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-v4-proposal.py').read_bytes()).hexdigest(), + 'guardian_sha256':hashlib.sha256((BASE/'184-acquisition-custody-guardian-v5-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, 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-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-session-controls-result.json b/qualification/issue-184/tool-acquisition/184-acquisition-session-controls-result.json index 809ab81..5c2374b 100644 --- a/qualification/issue-184/tool-acquisition/184-acquisition-session-controls-result.json +++ b/qualification/issue-184/tool-acquisition/184-acquisition-session-controls-result.json @@ -134,5 +134,5 @@ ], "scope": "synthetic role-protocol controls; start/admitted-data API never called with a decision", "selected_archives_acquired": 0, - "source_sha256": "4ed6698d3c6f93ea5040312c7c17f067aa8b1642376e00bb07f9b4302d51266f" + "source_sha256": "2f30889987dfe5c1d7030f3ce9146c327cfc512f241ca2ddefe3cb2d10e017ef" } diff --git a/qualification/issue-184/tool-acquisition/184-acquisition-session-controls.py b/qualification/issue-184/tool-acquisition/184-acquisition-session-controls.py index 2ae7d22..db007e7 100644 --- a/qualification/issue-184/tool-acquisition/184-acquisition-session-controls.py +++ b/qualification/issue-184/tool-acquisition/184-acquisition-session-controls.py @@ -31,7 +31,7 @@ def load(name,path): s=load('trusted_session_protocol_control',BASE/'184-acquisition-session-proposal.py') -g=load('trusted_guard_protocol_control',BASE/'184-acquisition-custody-guardian-v4-proposal.py') +g=load('trusted_guard_protocol_control',BASE/'184-acquisition-custody-guardian-v5-proposal.py') 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()) diff --git a/qualification/issue-184/tool-acquisition/184-acquisition-session-proposal.py b/qualification/issue-184/tool-acquisition/184-acquisition-session-proposal.py index 5e3b2f3..3622f32 100644 --- a/qualification/issue-184/tool-acquisition/184-acquisition-session-proposal.py +++ b/qualification/issue-184/tool-acquisition/184-acquisition-session-proposal.py @@ -31,7 +31,7 @@ INTERPRETER_SHA='52e0a13e60a981d8c4b6478be2ba5176f69da07948a056bf49cf6f077e30cb41' LIBRARY='/usr/lib/x86_64-linux-gnu/libpython3.14.so.1.0' LIBRARY_SHA='982cb3768d1d8b00126255ee2785bccf8360c68b197fb3dec95858ab56195f87' -PINNED={'owner': '234e099b5208aaafca379d3e5535cf44b3158a327764eec0ef4e12afcfc45ffb', 'bootstrap': '6ca5da88054b4c3ea27b2614dfd987f88ecb3db981271a995bb9941d06398fed', 'guardian': 'd52bb389127cb81b5b0303b3a593ee1af74f8c4835793d67f08d0bf182e2101c', 'terminal': '7e75b0929e612ec8605d095956fd446c2ad779a8f8b7e8403563f2764b1e70bf', 'selection': 'e9a7d639325b866569cdab83b81a2949437209a6bca91c881c2e365fdc573a1c', 'interpreter': '52e0a13e60a981d8c4b6478be2ba5176f69da07948a056bf49cf6f077e30cb41', 'library': '982cb3768d1d8b00126255ee2785bccf8360c68b197fb3dec95858ab56195f87', 'ca': '2ef52ad56b4803e264045c915e55c236cbfa045b84b81efefc218a19eb44bd2b'} +PINNED={'owner': '234e099b5208aaafca379d3e5535cf44b3158a327764eec0ef4e12afcfc45ffb', 'bootstrap': '6ca5da88054b4c3ea27b2614dfd987f88ecb3db981271a995bb9941d06398fed', 'guardian': '4c92d889d89dbef2502ef5e9bc923d00c0314821b42aa77d4c0ee92080aba41e', 'terminal': '7e75b0929e612ec8605d095956fd446c2ad779a8f8b7e8403563f2764b1e70bf', 'selection': 'e9a7d639325b866569cdab83b81a2949437209a6bca91c881c2e365fdc573a1c', 'interpreter': '52e0a13e60a981d8c4b6478be2ba5176f69da07948a056bf49cf6f077e30cb41', 'library': '982cb3768d1d8b00126255ee2785bccf8360c68b197fb3dec95858ab56195f87', 'ca': '2ef52ad56b4803e264045c915e55c236cbfa045b84b81efefc218a19eb44bd2b'} FRAME_CAP=65536 POOL_CAP=4*1024**2 @@ -655,7 +655,7 @@ def start(authority_path,expected_sha,expected_decision,expected_session_sha,sta 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-v4-proposal.py',PINNED['guardian']) + guard=module('reviewed_guardian','184-acquisition-custody-guardian-v5-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) diff --git a/qualification/issue-184/tool-acquisition/README.md b/qualification/issue-184/tool-acquisition/README.md index b0db385..e6e832e 100644 --- a/qualification/issue-184/tool-acquisition/README.md +++ b/qualification/issue-184/tool-acquisition/README.md @@ -29,11 +29,17 @@ their original identities. Whole-source PASS does not qualify or replace them. - `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-v4 proposes the additional completion correction6663237183. Missing - root completion requires every tracked directory/archive descriptor to have - the exact identity/type/owner and zero links. A moved child or a directory - created before its identity can be read remains unproved custody. -- Frozen guardian-v2/v3 remain byte-identical predecessor countercontrols. +- Guardian-v5 additionally proposes the archive-allocation correction6663491930. + 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 remain byte-identical predecessor countercontrols. V3 closed findings6660476483/6662253657 (partial allocation and authority parsing), but its later moved-child HOLD supersedes whole-packet PASS6662795537. Exact successor review is pending; no earlier PASS carries to this packet. @@ -93,7 +99,7 @@ admit this residual policy; it is not permission supplied by these sources. | 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 |62| retained v2 controls, actual v3 moved-child/partial-identity false-completion counterexamples, fully-unlinked positive, authority grammar/types and whole-tree custody | +| Guardian successor |66| retained62, actual v4 pre-archive-identity false completion/current refusal, and partial-archive fully-unlinked positives with root present/absent | | Role protocol |31| real local roles, allocation rollback/custody, EOF/deadline, guardian loss, survivors, setup refusal, unknown owner, late nonpass and integrated fake HTTP | | Launcher bootstrap |27| exact reads, authority schemas, startup refusals, resource affinity and custody wrapper | diff --git a/qualification/issue-184/tool-acquisition/authority-template.json b/qualification/issue-184/tool-acquisition/authority-template.json index 5f60fa3..9ab4d64 100644 --- a/qualification/issue-184/tool-acquisition/authority-template.json +++ b/qualification/issue-184/tool-acquisition/authority-template.json @@ -11,7 +11,7 @@ "inputs": { "bootstrap": "6ca5da88054b4c3ea27b2614dfd987f88ecb3db981271a995bb9941d06398fed", "ca": "2ef52ad56b4803e264045c915e55c236cbfa045b84b81efefc218a19eb44bd2b", - "guardian": "d52bb389127cb81b5b0303b3a593ee1af74f8c4835793d67f08d0bf182e2101c", + "guardian": "4c92d889d89dbef2502ef5e9bc923d00c0314821b42aa77d4c0ee92080aba41e", "interpreter": "52e0a13e60a981d8c4b6478be2ba5176f69da07948a056bf49cf6f077e30cb41", "library": "982cb3768d1d8b00126255ee2785bccf8360c68b197fb3dec95858ab56195f87", "owner": "234e099b5208aaafca379d3e5535cf44b3158a327764eec0ef4e12afcfc45ffb", @@ -19,7 +19,7 @@ "terminal": "7e75b0929e612ec8605d095956fd446c2ad779a8f8b7e8403563f2764b1e70bf" }, "io_seconds": 270, - "launcher_sha256": "4ed6698d3c6f93ea5040312c7c17f067aa8b1642376e00bb07f9b4302d51266f", + "launcher_sha256": "2f30889987dfe5c1d7030f3ce9146c327cfc512f241ca2ddefe3cb2d10e017ef", "pool_parent": "/home/lab4/fleet/tmp", "retention_deadline_utc": null, "retries": 0, diff --git a/qualification/issue-184/tool-acquisition/review-packet.json b/qualification/issue-184/tool-acquisition/review-packet.json index 7ce37f2..f31c313 100644 --- a/qualification/issue-184/tool-acquisition/review-packet.json +++ b/qualification/issue-184/tool-acquisition/review-packet.json @@ -12,15 +12,22 @@ "passed": 53, "sha256": "30c7d4815ea68410ac839351bab09f10ff9591e9395151d26ce1a917d293b65d" }, + "frozen_v4_control_result": { + "cases": 62, + "passed": 62, + "sha256": "2c7ee85f4b7b814cab4a9c78c1d150723d86f9cb8b78185644031cd775dc7a73" + }, "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-6663237183", + "https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-6663491930" ], "predecessor_v2_sha256": "afc5f198de12d4b26520957bc563ff0122f8535838a1cbc64c40272c5f3f7f70", "predecessor_v3_sha256": "79ddc82b0095872885aceb943cbf9daa1eb3a05048bc08f9ee9cd093511de187", - "proposed_successor": "guardian-v4; tracked descendants must all be unlinked before root-absence completion; exact successor review pending", + "predecessor_v4_sha256": "d52bb389127cb81b5b0303b3a593ee1af74f8c4835793d67f08d0bf182e2101c", + "proposed_successor": "guardian-v5; raw archive fd/name custody before identity reads; partial raw archive unlink proof; exact successor 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", @@ -68,7 +75,7 @@ "runtime_source_inputs": { "bootstrap": "6ca5da88054b4c3ea27b2614dfd987f88ecb3db981271a995bb9941d06398fed", "ca": "2ef52ad56b4803e264045c915e55c236cbfa045b84b81efefc218a19eb44bd2b", - "guardian": "d52bb389127cb81b5b0303b3a593ee1af74f8c4835793d67f08d0bf182e2101c", + "guardian": "4c92d889d89dbef2502ef5e9bc923d00c0314821b42aa77d4c0ee92080aba41e", "interpreter": "52e0a13e60a981d8c4b6478be2ba5176f69da07948a056bf49cf6f077e30cb41", "library": "982cb3768d1d8b00126255ee2785bccf8360c68b197fb3dec95858ab56195f87", "owner": "234e099b5208aaafca379d3e5535cf44b3158a327764eec0ef4e12afcfc45ffb", @@ -80,22 +87,22 @@ "selected_archive_bytes": 2901292, "selected_archives_acquired": 0, "selected_sdk_source": "14adc40b00b68cc8a3e1ae5647a885a42f273650", - "session_sha256": "4ed6698d3c6f93ea5040312c7c17f067aa8b1642376e00bb07f9b4302d51266f", + "session_sha256": "2f30889987dfe5c1d7030f3ce9146c327cfc512f241ca2ddefe3cb2d10e017ef", "source_control_results": { "184-acquire-tools-v5-adapter-controls-result.json": { "cases": 16, "passed": 16, - "sha256": "a33efea734891ebdf0cf196f082b6d0d153b25e7c0bbc7206a3b0ca2d9d3251e" + "sha256": "56cb21ef101edc34f8b9a6527b6ccfc85160b9509c1a1c24b3c81a09d6066420" }, "184-acquire-tools-v5-controls-result.json": { "cases": 86, "passed": 86, "sha256": "2a84c6612fcd2fd57235f4844face425420dafb3217be7d6e82ef57cc6fab1cd" }, - "184-acquisition-guardian-v4-controls-result.json": { - "cases": 62, - "passed": 62, - "sha256": "2c7ee85f4b7b814cab4a9c78c1d150723d86f9cb8b78185644031cd775dc7a73" + "184-acquisition-guardian-v5-controls-result.json": { + "cases": 66, + "passed": 66, + "sha256": "3ec6b52657fd172c3db493c8f16df677c473a688f44fcfc5af579b3c9f149394" }, "184-acquisition-launcher-bootstrap-controls-result.json": { "cases": 27, @@ -105,7 +112,7 @@ "184-acquisition-session-controls-result.json": { "cases": 31, "passed": 31, - "sha256": "211c0e96505d19a6f24a4f2d8c32bcbc1623018f10185413402edabb7ef3cd93" + "sha256": "3729dce8f4c78beb940d68e75db87fdaa0ff4036f69ea8c57a02c6f80bc5996d" }, "184-acquisition-terminal-controls-result.json": { "cases": 16, diff --git a/qualification/issue-184/tool-acquisition/source-manifest.json b/qualification/issue-184/tool-acquisition/source-manifest.json index 8a249db..754648d 100644 --- a/qualification/issue-184/tool-acquisition/source-manifest.json +++ b/qualification/issue-184/tool-acquisition/source-manifest.json @@ -8,12 +8,12 @@ { "bytes": 1898, "file": "184-acquire-tools-v5-adapter-controls-result.json", - "sha256": "a33efea734891ebdf0cf196f082b6d0d153b25e7c0bbc7206a3b0ca2d9d3251e" + "sha256": "56cb21ef101edc34f8b9a6527b6ccfc85160b9509c1a1c24b3c81a09d6066420" }, { "bytes": 10492, "file": "184-acquire-tools-v5-adapter-controls.py", - "sha256": "d6f18f1ad8cb1a561a96a6528925ed55f8e551eb27e5ca8ef6d64b92ee1ae017" + "sha256": "bb63482acfbc8f583baebeefee70d3cc29dc41ba3d09eef670e257ee5d17d822" }, { "bytes": 2875, @@ -50,6 +50,11 @@ "file": "184-acquisition-custody-guardian-v4-proposal.py", "sha256": "d52bb389127cb81b5b0303b3a593ee1af74f8c4835793d67f08d0bf182e2101c" }, + { + "bytes": 21371, + "file": "184-acquisition-custody-guardian-v5-proposal.py", + "sha256": "4c92d889d89dbef2502ef5e9bc923d00c0314821b42aa77d4c0ee92080aba41e" + }, { "bytes": 4928, "file": "184-acquisition-guardian-v3-controls-result.json", @@ -70,6 +75,16 @@ "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": 1010, "file": "184-acquisition-interpreter-authority-proposal.json", @@ -93,17 +108,17 @@ { "bytes": 3466, "file": "184-acquisition-session-controls-result.json", - "sha256": "211c0e96505d19a6f24a4f2d8c32bcbc1623018f10185413402edabb7ef3cd93" + "sha256": "3729dce8f4c78beb940d68e75db87fdaa0ff4036f69ea8c57a02c6f80bc5996d" }, { "bytes": 25506, "file": "184-acquisition-session-controls.py", - "sha256": "f389d59341e68d709c566530a65ec849f9da99fb70b2ce5d15df7548479622bc" + "sha256": "13ecf522dc9589e410c8ec41fbf73af161033e88c0186d8ac678c538bde908ba" }, { "bytes": 41327, "file": "184-acquisition-session-proposal.py", - "sha256": "4ed6698d3c6f93ea5040312c7c17f067aa8b1642376e00bb07f9b4302d51266f" + "sha256": "2f30889987dfe5c1d7030f3ce9146c327cfc512f241ca2ddefe3cb2d10e017ef" }, { "bytes": 1826, @@ -126,14 +141,14 @@ "sha256": "e9a7d639325b866569cdab83b81a2949437209a6bca91c881c2e365fdc573a1c" }, { - "bytes": 8202, + "bytes": 8638, "file": "README.md", - "sha256": "1acf83f7aa9daa9c3c1705b72b9caa216d3b6e54da26bc6ce8a5b11e78f8eae9" + "sha256": "f4b1900b6e6751059895815a0f9e0b1a5f0224426212d3ffdbf3750a75696119" }, { "bytes": 1209, "file": "authority-template.json", - "sha256": "74910b72df111bb7bc327486d08b82b5938cee027e43a0931eae0fca1022da82" + "sha256": "8215d40d9125d9830fc828e2aeed56b63be546afbf82a5531349d7689365664b" }, { "bytes": 1170, @@ -201,20 +216,22 @@ "sha256": "e9a7d639325b866569cdab83b81a2949437209a6bca91c881c2e365fdc573a1c" }, { - "bytes": 6397, + "bytes": 6750, "file": "review-packet.json", - "sha256": "2d124b2c38175808f71034830971ca2596a28ca8a3d3e93eaa10c4e5536612d9" + "sha256": "194a13803582ab2fd378383347dcf2edc8031afe26cef7cabc73fd7bc55822ea" } ], "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-6663237183", + "https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-6663491930" ], "predecessor_v2_sha256": "afc5f198de12d4b26520957bc563ff0122f8535838a1cbc64c40272c5f3f7f70", "predecessor_v3_sha256": "79ddc82b0095872885aceb943cbf9daa1eb3a05048bc08f9ee9cd093511de187", - "proposed_successor": "guardian-v4; tracked descendants must all be unlinked before root-absence completion; exact successor review pending", + "predecessor_v4_sha256": "d52bb389127cb81b5b0303b3a593ee1af74f8c4835793d67f08d0bf182e2101c", + "proposed_successor": "guardian-v5; raw archive fd/name custody before identity reads; partial raw archive unlink proof; exact successor review pending", "superseded_whole_source_PASS": "https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-6662795537" }, "manifest_excludes_self": true, @@ -244,17 +261,17 @@ "184-acquire-tools-v5-adapter-controls-result.json": { "cases": 16, "passed": 16, - "sha256": "a33efea734891ebdf0cf196f082b6d0d153b25e7c0bbc7206a3b0ca2d9d3251e" + "sha256": "56cb21ef101edc34f8b9a6527b6ccfc85160b9509c1a1c24b3c81a09d6066420" }, "184-acquire-tools-v5-controls-result.json": { "cases": 86, "passed": 86, "sha256": "2a84c6612fcd2fd57235f4844face425420dafb3217be7d6e82ef57cc6fab1cd" }, - "184-acquisition-guardian-v4-controls-result.json": { - "cases": 62, - "passed": 62, - "sha256": "2c7ee85f4b7b814cab4a9c78c1d150723d86f9cb8b78185644031cd775dc7a73" + "184-acquisition-guardian-v5-controls-result.json": { + "cases": 66, + "passed": 66, + "sha256": "3ec6b52657fd172c3db493c8f16df677c473a688f44fcfc5af579b3c9f149394" }, "184-acquisition-launcher-bootstrap-controls-result.json": { "cases": 27, @@ -264,7 +281,7 @@ "184-acquisition-session-controls-result.json": { "cases": 31, "passed": 31, - "sha256": "211c0e96505d19a6f24a4f2d8c32bcbc1623018f10185413402edabb7ef3cd93" + "sha256": "3729dce8f4c78beb940d68e75db87fdaa0ff4036f69ea8c57a02c6f80bc5996d" }, "184-acquisition-terminal-controls-result.json": { "cases": 16, @@ -272,5 +289,5 @@ "sha256": "32a30a3767d760befae96f77f906431f234dd2733c28b17c3deadb71921f3be0" } }, - "stage": "UNEXECUTED guardian-v4 successor for6663237183; prior49dd source PASS superseded; exact review pending; no operational admission" + "stage": "UNEXECUTED guardian-v5 successor for6663491930; v4 retains HOLD; exact review pending; no operational admission" } From d42e510952f945e9de68cdd8a18b4f5cf8969987 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Thu, 5 Nov 2026 16:55:07 +0000 Subject: [PATCH 7/8] Close tracked acquisition descriptors independently of callback registration --- ...uire-tools-v5-adapter-controls-result.json | 2 +- .../184-acquire-tools-v5-adapter-controls.py | 8 +- ...cquisition-custody-guardian-v6-proposal.py | 418 ++++++++++++++++++ ...quisition-guardian-v6-controls-result.json | 305 +++++++++++++ .../184-acquisition-guardian-v6-controls.py | 363 +++++++++++++++ ...4-acquisition-session-controls-result.json | 14 +- .../184-acquisition-session-controls.py | 29 +- .../184-acquisition-session-proposal.py | 8 +- .../issue-184/tool-acquisition/README.md | 15 +- .../tool-acquisition/authority-template.json | 4 +- .../tool-acquisition/review-packet.json | 31 +- .../tool-acquisition/source-manifest.json | 65 ++- 12 files changed, 1207 insertions(+), 55 deletions(-) create mode 100644 qualification/issue-184/tool-acquisition/184-acquisition-custody-guardian-v6-proposal.py create mode 100644 qualification/issue-184/tool-acquisition/184-acquisition-guardian-v6-controls-result.json create mode 100644 qualification/issue-184/tool-acquisition/184-acquisition-guardian-v6-controls.py 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 index 2a716a9..e4b8fad 100644 --- 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 @@ -2,7 +2,7 @@ "cases": 16, "downloaded_wheel_imports_or_installs": 0, "failed": 0, - "guardian_sha256": "4c92d889d89dbef2502ef5e9bc923d00c0314821b42aa77d4c0ee92080aba41e", + "guardian_sha256": "cbb1672855a0dec48c7d952f02be49ab3c0935d9bd7b1cf467638d73fc543e4c", "owner_sha256": "234e099b5208aaafca379d3e5535cf44b3158a327764eec0ef4e12afcfc45ffb", "passed": 16, "real_network_requests": 0, 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 index 64f6b15..6b400cf 100644 --- 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 @@ -26,9 +26,9 @@ def load(name,path): owner=load('trusted_v5_owner_adapter',BASE/'184-acquire-tools-v5.py') -guard=load('trusted_reviewed_guard_custody',BASE/'184-acquisition-custody-guardian-v5-proposal.py') -assert hashlib.sha256((BASE/'184-acquisition-custody-guardian-v5-proposal.py').read_bytes()).hexdigest()== \ - '4c92d889d89dbef2502ef5e9bc923d00c0314821b42aa77d4c0ee92080aba41e' +guard=load('trusted_reviewed_guard_custody',BASE/'184-acquisition-custody-guardian-v6-proposal.py') +assert hashlib.sha256((BASE/'184-acquisition-custody-guardian-v6-proposal.py').read_bytes()).hexdigest()== \ + 'cbb1672855a0dec48c7d952f02be49ab3c0935d9bd7b1cf467638d73fc543e4c' results=[] @@ -187,7 +187,7 @@ def 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-v5-proposal.py').read_bytes()).hexdigest(), + 'guardian_sha256':hashlib.sha256((BASE/'184-acquisition-custody-guardian-v6-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, 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-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-session-controls-result.json b/qualification/issue-184/tool-acquisition/184-acquisition-session-controls-result.json index 5c2374b..a10236f 100644 --- a/qualification/issue-184/tool-acquisition/184-acquisition-session-controls-result.json +++ b/qualification/issue-184/tool-acquisition/184-acquisition-session-controls-result.json @@ -1,12 +1,20 @@ { "acquisition_accepted": false, - "cases": 31, + "cases": 33, "downloaded_wheel_imports_or_installs": 0, "failed": 0, "new_general_decision": null, - "passed": 31, + "passed": 33, "real_network_requests": 0, "results": [ + { + "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" @@ -134,5 +142,5 @@ ], "scope": "synthetic role-protocol controls; start/admitted-data API never called with a decision", "selected_archives_acquired": 0, - "source_sha256": "2f30889987dfe5c1d7030f3ce9146c327cfc512f241ca2ddefe3cb2d10e017ef" + "source_sha256": "e721bba90c8cf7fae9eede4b3a2df2b7abb25b59f84b62f8ee5be5b9ee381e89" } diff --git a/qualification/issue-184/tool-acquisition/184-acquisition-session-controls.py b/qualification/issue-184/tool-acquisition/184-acquisition-session-controls.py index db007e7..5e851bb 100644 --- a/qualification/issue-184/tool-acquisition/184-acquisition-session-controls.py +++ b/qualification/issue-184/tool-acquisition/184-acquisition-session-controls.py @@ -31,7 +31,7 @@ def load(name,path): s=load('trusted_session_protocol_control',BASE/'184-acquisition-session-proposal.py') -g=load('trusted_guard_protocol_control',BASE/'184-acquisition-custody-guardian-v5-proposal.py') +g=load('trusted_guard_protocol_control',BASE/'184-acquisition-custody-guardian-v6-proposal.py') 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()) @@ -355,6 +355,31 @@ def retention_boundary(seconds,accepted): 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 check(name,operation): try:operation() except BaseException as error:results.append({'id':name,'outcome':'fail','type':type(error).__name__,'detail':str(error)[:600]}) @@ -362,6 +387,8 @@ def check(name,operation): with patch.object(socket,'create_connection',side_effect=AssertionError('NO_NETWORK_IN_SESSION_CONTROLS')): + for directory in ('metadata','events'): + check(directory+'-callback-failure-closes-recorded-directory-fd',lambda directory=directory:preparation_callback_failure_closes(directory)) for seconds,accepted in [(0,False),(300,False),(301,True),(86400,True),(86401,False)]: check('retention-deadline-boundary-'+str(seconds),lambda seconds=seconds,accepted=accepted:retention_boundary(seconds,accepted)) for mode in ('alloc-after-root','alloc-after-wheelhouse','alloc-after-metadata','alloc-unknown'): diff --git a/qualification/issue-184/tool-acquisition/184-acquisition-session-proposal.py b/qualification/issue-184/tool-acquisition/184-acquisition-session-proposal.py index 3622f32..001381d 100644 --- a/qualification/issue-184/tool-acquisition/184-acquisition-session-proposal.py +++ b/qualification/issue-184/tool-acquisition/184-acquisition-session-proposal.py @@ -31,7 +31,7 @@ INTERPRETER_SHA='52e0a13e60a981d8c4b6478be2ba5176f69da07948a056bf49cf6f077e30cb41' LIBRARY='/usr/lib/x86_64-linux-gnu/libpython3.14.so.1.0' LIBRARY_SHA='982cb3768d1d8b00126255ee2785bccf8360c68b197fb3dec95858ab56195f87' -PINNED={'owner': '234e099b5208aaafca379d3e5535cf44b3158a327764eec0ef4e12afcfc45ffb', 'bootstrap': '6ca5da88054b4c3ea27b2614dfd987f88ecb3db981271a995bb9941d06398fed', 'guardian': '4c92d889d89dbef2502ef5e9bc923d00c0314821b42aa77d4c0ee92080aba41e', 'terminal': '7e75b0929e612ec8605d095956fd446c2ad779a8f8b7e8403563f2764b1e70bf', 'selection': 'e9a7d639325b866569cdab83b81a2949437209a6bca91c881c2e365fdc573a1c', 'interpreter': '52e0a13e60a981d8c4b6478be2ba5176f69da07948a056bf49cf6f077e30cb41', 'library': '982cb3768d1d8b00126255ee2785bccf8360c68b197fb3dec95858ab56195f87', 'ca': '2ef52ad56b4803e264045c915e55c236cbfa045b84b81efefc218a19eb44bd2b'} +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 @@ -132,10 +132,10 @@ def prepare_tree(guard,parent,leaf,filenames): 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.stack.callback(os.close,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') - custody.directory_fds[name]=fd return custody except BaseException as error: # No owner exists. Roll back only identity-verified, empty extra dirs. @@ -655,7 +655,7 @@ def start(authority_path,expected_sha,expected_decision,expected_session_sha,sta 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-v5-proposal.py',PINNED['guardian']) + 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) diff --git a/qualification/issue-184/tool-acquisition/README.md b/qualification/issue-184/tool-acquisition/README.md index e6e832e..7834696 100644 --- a/qualification/issue-184/tool-acquisition/README.md +++ b/qualification/issue-184/tool-acquisition/README.md @@ -29,7 +29,14 @@ their original identities. Whole-source PASS does not qualify or replace them. - `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-v5 additionally proposes the archive-allocation correction6663491930. +- 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, @@ -39,7 +46,7 @@ their original identities. Whole-source PASS does not qualify or replace them. 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 remain byte-identical predecessor countercontrols. +- Frozen guardian-v2/v3/v4/v5 remain byte-identical predecessor countercontrols. V3 closed findings6660476483/6662253657 (partial allocation and authority parsing), but its later moved-child HOLD supersedes whole-packet PASS6662795537. Exact successor review is pending; no earlier PASS carries to this packet. @@ -99,8 +106,8 @@ admit this residual policy; it is not permission supplied by these sources. | 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 |66| retained62, actual v4 pre-archive-identity false completion/current refusal, and partial-archive fully-unlinked positives with root present/absent | -| Role protocol |31| real local roles, allocation rollback/custody, EOF/deadline, guardian loss, survivors, setup refusal, unknown owner, late nonpass and integrated fake HTTP | +| Guardian successor |73| retained66 plus actual v5 callback leak/current idempotent close, descriptor reuse, and parent/root/wheelhouse callback failures | +| Role protocol |33| retained31 plus metadata/events callback failure and independent tracked-directory closure | | 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. diff --git a/qualification/issue-184/tool-acquisition/authority-template.json b/qualification/issue-184/tool-acquisition/authority-template.json index 9ab4d64..654a0d4 100644 --- a/qualification/issue-184/tool-acquisition/authority-template.json +++ b/qualification/issue-184/tool-acquisition/authority-template.json @@ -11,7 +11,7 @@ "inputs": { "bootstrap": "6ca5da88054b4c3ea27b2614dfd987f88ecb3db981271a995bb9941d06398fed", "ca": "2ef52ad56b4803e264045c915e55c236cbfa045b84b81efefc218a19eb44bd2b", - "guardian": "4c92d889d89dbef2502ef5e9bc923d00c0314821b42aa77d4c0ee92080aba41e", + "guardian": "cbb1672855a0dec48c7d952f02be49ab3c0935d9bd7b1cf467638d73fc543e4c", "interpreter": "52e0a13e60a981d8c4b6478be2ba5176f69da07948a056bf49cf6f077e30cb41", "library": "982cb3768d1d8b00126255ee2785bccf8360c68b197fb3dec95858ab56195f87", "owner": "234e099b5208aaafca379d3e5535cf44b3158a327764eec0ef4e12afcfc45ffb", @@ -19,7 +19,7 @@ "terminal": "7e75b0929e612ec8605d095956fd446c2ad779a8f8b7e8403563f2764b1e70bf" }, "io_seconds": 270, - "launcher_sha256": "2f30889987dfe5c1d7030f3ce9146c327cfc512f241ca2ddefe3cb2d10e017ef", + "launcher_sha256": "e721bba90c8cf7fae9eede4b3a2df2b7abb25b59f84b62f8ee5be5b9ee381e89", "pool_parent": "/home/lab4/fleet/tmp", "retention_deadline_utc": null, "retries": 0, diff --git a/qualification/issue-184/tool-acquisition/review-packet.json b/qualification/issue-184/tool-acquisition/review-packet.json index f31c313..b4879fe 100644 --- a/qualification/issue-184/tool-acquisition/review-packet.json +++ b/qualification/issue-184/tool-acquisition/review-packet.json @@ -17,17 +17,24 @@ "passed": 62, "sha256": "2c7ee85f4b7b814cab4a9c78c1d150723d86f9cb8b78185644031cd775dc7a73" }, + "frozen_v5_control_result": { + "cases": 66, + "passed": 66, + "sha256": "3ec6b52657fd172c3db493c8f16df677c473a688f44fcfc5af579b3c9f149394" + }, "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-6663491930", + "https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-6663692366" ], "predecessor_v2_sha256": "afc5f198de12d4b26520957bc563ff0122f8535838a1cbc64c40272c5f3f7f70", "predecessor_v3_sha256": "79ddc82b0095872885aceb943cbf9daa1eb3a05048bc08f9ee9cd093511de187", "predecessor_v4_sha256": "d52bb389127cb81b5b0303b3a593ee1af74f8c4835793d67f08d0bf182e2101c", - "proposed_successor": "guardian-v5; raw archive fd/name custody before identity reads; partial raw archive unlink proof; exact successor review pending", + "predecessor_v5_sha256": "4c92d889d89dbef2502ef5e9bc923d00c0314821b42aa77d4c0ee92080aba41e", + "proposed_successor": "guardian-v6; explicit idempotent descriptor close independent of callback registration; exact successor 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", @@ -75,7 +82,7 @@ "runtime_source_inputs": { "bootstrap": "6ca5da88054b4c3ea27b2614dfd987f88ecb3db981271a995bb9941d06398fed", "ca": "2ef52ad56b4803e264045c915e55c236cbfa045b84b81efefc218a19eb44bd2b", - "guardian": "4c92d889d89dbef2502ef5e9bc923d00c0314821b42aa77d4c0ee92080aba41e", + "guardian": "cbb1672855a0dec48c7d952f02be49ab3c0935d9bd7b1cf467638d73fc543e4c", "interpreter": "52e0a13e60a981d8c4b6478be2ba5176f69da07948a056bf49cf6f077e30cb41", "library": "982cb3768d1d8b00126255ee2785bccf8360c68b197fb3dec95858ab56195f87", "owner": "234e099b5208aaafca379d3e5535cf44b3158a327764eec0ef4e12afcfc45ffb", @@ -87,22 +94,22 @@ "selected_archive_bytes": 2901292, "selected_archives_acquired": 0, "selected_sdk_source": "14adc40b00b68cc8a3e1ae5647a885a42f273650", - "session_sha256": "2f30889987dfe5c1d7030f3ce9146c327cfc512f241ca2ddefe3cb2d10e017ef", + "session_sha256": "e721bba90c8cf7fae9eede4b3a2df2b7abb25b59f84b62f8ee5be5b9ee381e89", "source_control_results": { "184-acquire-tools-v5-adapter-controls-result.json": { "cases": 16, "passed": 16, - "sha256": "56cb21ef101edc34f8b9a6527b6ccfc85160b9509c1a1c24b3c81a09d6066420" + "sha256": "b49570665353a9202701af61b98d117eee45023f851616d4a460d5ac29c923ea" }, "184-acquire-tools-v5-controls-result.json": { "cases": 86, "passed": 86, "sha256": "2a84c6612fcd2fd57235f4844face425420dafb3217be7d6e82ef57cc6fab1cd" }, - "184-acquisition-guardian-v5-controls-result.json": { - "cases": 66, - "passed": 66, - "sha256": "3ec6b52657fd172c3db493c8f16df677c473a688f44fcfc5af579b3c9f149394" + "184-acquisition-guardian-v6-controls-result.json": { + "cases": 73, + "passed": 73, + "sha256": "f1b504d920d26468dff0f882b6f2e0041529bf3c0f0c4ecb9f88842bd239f4e4" }, "184-acquisition-launcher-bootstrap-controls-result.json": { "cases": 27, @@ -110,9 +117,9 @@ "sha256": "874b1b868975690e56818147c72b1eb7db197c51a961b42da0f1ae00312452b1" }, "184-acquisition-session-controls-result.json": { - "cases": 31, - "passed": 31, - "sha256": "3729dce8f4c78beb940d68e75db87fdaa0ff4036f69ea8c57a02c6f80bc5996d" + "cases": 33, + "passed": 33, + "sha256": "2ac6e635fa7f8ba76091e85548e98db6d9f4cd1fd0313400702edfdf429c5974" }, "184-acquisition-terminal-controls-result.json": { "cases": 16, diff --git a/qualification/issue-184/tool-acquisition/source-manifest.json b/qualification/issue-184/tool-acquisition/source-manifest.json index 754648d..5799a23 100644 --- a/qualification/issue-184/tool-acquisition/source-manifest.json +++ b/qualification/issue-184/tool-acquisition/source-manifest.json @@ -8,12 +8,12 @@ { "bytes": 1898, "file": "184-acquire-tools-v5-adapter-controls-result.json", - "sha256": "56cb21ef101edc34f8b9a6527b6ccfc85160b9509c1a1c24b3c81a09d6066420" + "sha256": "b49570665353a9202701af61b98d117eee45023f851616d4a460d5ac29c923ea" }, { "bytes": 10492, "file": "184-acquire-tools-v5-adapter-controls.py", - "sha256": "bb63482acfbc8f583baebeefee70d3cc29dc41ba3d09eef670e257ee5d17d822" + "sha256": "294d5dab6b21d66071a4bfca224ebe9ad954ea512610d7f8fc3dc49330a66ccc" }, { "bytes": 2875, @@ -55,6 +55,11 @@ "file": "184-acquisition-custody-guardian-v5-proposal.py", "sha256": "4c92d889d89dbef2502ef5e9bc923d00c0314821b42aa77d4c0ee92080aba41e" }, + { + "bytes": 22524, + "file": "184-acquisition-custody-guardian-v6-proposal.py", + "sha256": "cbb1672855a0dec48c7d952f02be49ab3c0935d9bd7b1cf467638d73fc543e4c" + }, { "bytes": 4928, "file": "184-acquisition-guardian-v3-controls-result.json", @@ -85,6 +90,16 @@ "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": 1010, "file": "184-acquisition-interpreter-authority-proposal.json", @@ -106,19 +121,19 @@ "sha256": "0cd9d5309b1dd3314e5a7f288f7462c784e2bcf4cea9618eeff4aa401f516aaf" }, { - "bytes": 3466, + "bytes": 3678, "file": "184-acquisition-session-controls-result.json", - "sha256": "3729dce8f4c78beb940d68e75db87fdaa0ff4036f69ea8c57a02c6f80bc5996d" + "sha256": "2ac6e635fa7f8ba76091e85548e98db6d9f4cd1fd0313400702edfdf429c5974" }, { - "bytes": 25506, + "bytes": 27176, "file": "184-acquisition-session-controls.py", - "sha256": "13ecf522dc9589e410c8ec41fbf73af161033e88c0186d8ac678c538bde908ba" + "sha256": "7f23fe46b0880e557d333efb61f5bc6f41af1044ff190bc17cd31157a6e8eb56" }, { - "bytes": 41327, + "bytes": 41341, "file": "184-acquisition-session-proposal.py", - "sha256": "2f30889987dfe5c1d7030f3ce9146c327cfc512f241ca2ddefe3cb2d10e017ef" + "sha256": "e721bba90c8cf7fae9eede4b3a2df2b7abb25b59f84b62f8ee5be5b9ee381e89" }, { "bytes": 1826, @@ -141,14 +156,14 @@ "sha256": "e9a7d639325b866569cdab83b81a2949437209a6bca91c881c2e365fdc573a1c" }, { - "bytes": 8638, + "bytes": 9049, "file": "README.md", - "sha256": "f4b1900b6e6751059895815a0f9e0b1a5f0224426212d3ffdbf3750a75696119" + "sha256": "37849d545c76dd3ecd5fedcb91bf33650e30cca3241e152fb812bd5655d953e4" }, { "bytes": 1209, "file": "authority-template.json", - "sha256": "8215d40d9125d9830fc828e2aeed56b63be546afbf82a5531349d7689365664b" + "sha256": "239d025c890988971963c6429bad148789498e4d813bb84816c5c0826dae7b2e" }, { "bytes": 1170, @@ -216,9 +231,9 @@ "sha256": "e9a7d639325b866569cdab83b81a2949437209a6bca91c881c2e365fdc573a1c" }, { - "bytes": 6750, + "bytes": 7081, "file": "review-packet.json", - "sha256": "194a13803582ab2fd378383347dcf2edc8031afe26cef7cabc73fd7bc55822ea" + "sha256": "9947c69df3533b3165beadb3062d13696889aa17d0aca10993fffae4cca58060" } ], "guardian_later_findings": { @@ -226,12 +241,14 @@ "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-6663491930", + "https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-6663692366" ], "predecessor_v2_sha256": "afc5f198de12d4b26520957bc563ff0122f8535838a1cbc64c40272c5f3f7f70", "predecessor_v3_sha256": "79ddc82b0095872885aceb943cbf9daa1eb3a05048bc08f9ee9cd093511de187", "predecessor_v4_sha256": "d52bb389127cb81b5b0303b3a593ee1af74f8c4835793d67f08d0bf182e2101c", - "proposed_successor": "guardian-v5; raw archive fd/name custody before identity reads; partial raw archive unlink proof; exact successor review pending", + "predecessor_v5_sha256": "4c92d889d89dbef2502ef5e9bc923d00c0314821b42aa77d4c0ee92080aba41e", + "proposed_successor": "guardian-v6; explicit idempotent descriptor close independent of callback registration; exact successor review pending", "superseded_whole_source_PASS": "https://github.com/durable-workflow/sdk-python/issues/184#issuecomment-6662795537" }, "manifest_excludes_self": true, @@ -261,17 +278,17 @@ "184-acquire-tools-v5-adapter-controls-result.json": { "cases": 16, "passed": 16, - "sha256": "56cb21ef101edc34f8b9a6527b6ccfc85160b9509c1a1c24b3c81a09d6066420" + "sha256": "b49570665353a9202701af61b98d117eee45023f851616d4a460d5ac29c923ea" }, "184-acquire-tools-v5-controls-result.json": { "cases": 86, "passed": 86, "sha256": "2a84c6612fcd2fd57235f4844face425420dafb3217be7d6e82ef57cc6fab1cd" }, - "184-acquisition-guardian-v5-controls-result.json": { - "cases": 66, - "passed": 66, - "sha256": "3ec6b52657fd172c3db493c8f16df677c473a688f44fcfc5af579b3c9f149394" + "184-acquisition-guardian-v6-controls-result.json": { + "cases": 73, + "passed": 73, + "sha256": "f1b504d920d26468dff0f882b6f2e0041529bf3c0f0c4ecb9f88842bd239f4e4" }, "184-acquisition-launcher-bootstrap-controls-result.json": { "cases": 27, @@ -279,9 +296,9 @@ "sha256": "874b1b868975690e56818147c72b1eb7db197c51a961b42da0f1ae00312452b1" }, "184-acquisition-session-controls-result.json": { - "cases": 31, - "passed": 31, - "sha256": "3729dce8f4c78beb940d68e75db87fdaa0ff4036f69ea8c57a02c6f80bc5996d" + "cases": 33, + "passed": 33, + "sha256": "2ac6e635fa7f8ba76091e85548e98db6d9f4cd1fd0313400702edfdf429c5974" }, "184-acquisition-terminal-controls-result.json": { "cases": 16, @@ -289,5 +306,5 @@ "sha256": "32a30a3767d760befae96f77f906431f234dd2733c28b17c3deadb71921f3be0" } }, - "stage": "UNEXECUTED guardian-v5 successor for6663491930; v4 retains HOLD; exact review pending; no operational admission" + "stage": "UNEXECUTED guardian-v6 successor for6663692366; v5 holds recorded; exact review pending; no operational admission" } From cf7c044062f973902d8417ece112dd4fb6cae850 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Thu, 5 Nov 2026 19:03:07 +0000 Subject: [PATCH 8/8] Close guardian setup and pre-adoption custody gaps for #184 --- ...uire-tools-v5-adapter-controls-result.json | 2 +- .../184-acquire-tools-v5-adapter-controls.py | 8 +- ...cquisition-custody-guardian-v7-proposal.py | 439 +++++++++++ ...quisition-guardian-v7-controls-result.json | 337 +++++++++ .../184-acquisition-guardian-v7-controls.py | 417 +++++++++++ ...4-acquisition-session-controls-result.json | 106 ++- .../184-acquisition-session-controls.py | 233 +++++- .../184-acquisition-session-proposal.py | 270 ++++++- .../issue-184/tool-acquisition/README.md | 30 +- .../tool-acquisition/authority-template.json | 4 +- .../historical-source/manifest.json | 6 + .../historical-source/session-v6.source | 699 ++++++++++++++++++ .../tool-acquisition/review-packet.json | 45 +- .../tool-acquisition/source-manifest.json | 76 +- 14 files changed, 2572 insertions(+), 100 deletions(-) create mode 100644 qualification/issue-184/tool-acquisition/184-acquisition-custody-guardian-v7-proposal.py create mode 100644 qualification/issue-184/tool-acquisition/184-acquisition-guardian-v7-controls-result.json create mode 100644 qualification/issue-184/tool-acquisition/184-acquisition-guardian-v7-controls.py create mode 100644 qualification/issue-184/tool-acquisition/control-inputs/historical-source/session-v6.source 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 index e4b8fad..40d1056 100644 --- 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 @@ -2,7 +2,7 @@ "cases": 16, "downloaded_wheel_imports_or_installs": 0, "failed": 0, - "guardian_sha256": "cbb1672855a0dec48c7d952f02be49ab3c0935d9bd7b1cf467638d73fc543e4c", + "guardian_sha256": "032a2da74a26ea8ed036428d5470420dd49f099b7fe61cfcf4d7ab216d720066", "owner_sha256": "234e099b5208aaafca379d3e5535cf44b3158a327764eec0ef4e12afcfc45ffb", "passed": 16, "real_network_requests": 0, 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 index 6b400cf..9ed39fa 100644 --- 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 @@ -26,9 +26,9 @@ def load(name,path): owner=load('trusted_v5_owner_adapter',BASE/'184-acquire-tools-v5.py') -guard=load('trusted_reviewed_guard_custody',BASE/'184-acquisition-custody-guardian-v6-proposal.py') -assert hashlib.sha256((BASE/'184-acquisition-custody-guardian-v6-proposal.py').read_bytes()).hexdigest()== \ - 'cbb1672855a0dec48c7d952f02be49ab3c0935d9bd7b1cf467638d73fc543e4c' +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=[] @@ -187,7 +187,7 @@ def 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-v6-proposal.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, 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-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-session-controls-result.json b/qualification/issue-184/tool-acquisition/184-acquisition-session-controls-result.json index a10236f..a023bf9 100644 --- a/qualification/issue-184/tool-acquisition/184-acquisition-session-controls-result.json +++ b/qualification/issue-184/tool-acquisition/184-acquisition-session-controls-result.json @@ -1,12 +1,104 @@ { "acquisition_accepted": false, - "cases": 33, + "cases": 58, "downloaded_wheel_imports_or_installs": 0, "failed": 0, "new_general_decision": null, - "passed": 33, + "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" @@ -138,9 +230,17 @@ { "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": "e721bba90c8cf7fae9eede4b3a2df2b7abb25b59f84b62f8ee5be5b9ee381e89" + "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 index 5e851bb..8704cd5 100644 --- a/qualification/issue-184/tool-acquisition/184-acquisition-session-controls.py +++ b/qualification/issue-184/tool-acquisition/184-acquisition-session-controls.py @@ -2,6 +2,7 @@ from __future__ import annotations import datetime as dt +import ast import hashlib import importlib.util import io @@ -15,9 +16,10 @@ import sys import tempfile import time +import traceback import urllib.request import zipfile -from types import SimpleNamespace +from types import ModuleType,SimpleNamespace from unittest.mock import patch sys.dont_write_bytecode=True @@ -31,7 +33,11 @@ def load(name,path): s=load('trusted_session_protocol_control',BASE/'184-acquisition-session-proposal.py') -g=load('trusted_guard_protocol_control',BASE/'184-acquisition-custody-guardian-v6-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()) @@ -119,7 +125,7 @@ def cleanup_directory(path): directory.chmod(0o700) -def run_protocol(mode,disconnect=None): +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 @@ -132,7 +138,25 @@ def run_protocol(mode,disconnect=None): if child==0: try: outer.close();os.environ.clear();os.environ.update(s.MINIMAL_ENV) - s.POOL_PARENT=parent + 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): @@ -157,7 +181,7 @@ def stop_with_one_survivor(role,ceiling): 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=s.guardian_job(inner,authority_raw,authority,started,dt.datetime.now(dt.timezone.utc).isoformat(), + 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) @@ -177,8 +201,12 @@ def stop_with_one_survivor(role,ceiling): _,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] - assert not list((expected_pool/'wheelhouse').iterdir()) - assert (expected_pool/'guardian-result.json').exists() + 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': @@ -198,13 +226,30 @@ def stop_with_one_survivor(role,ceiling): else:message=session.wait() reaped=session.guardian_reaped result=message['result'];assert not result['acquisition_accepted'] - if mode=='alloc-unknown': + 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']=='ALLOCATION_REFUSED' and result['terminal_cleanup_complete'] + 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': @@ -234,7 +279,8 @@ def stop_with_one_survivor(role,ceiling): 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' - assert select.select([pidfd],[],[],0)[0] if not session.finished and mode!='alloc-unknown' else True + 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) @@ -380,13 +426,174 @@ def callback(stack,function,*args,**kwargs): 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=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