diff --git a/docs/krea-v12-native-studies.md b/docs/krea-v12-native-studies.md index 280af52..2f75843 100644 --- a/docs/krea-v12-native-studies.md +++ b/docs/krea-v12-native-studies.md @@ -148,7 +148,12 @@ The supported source reference is the surviving The original repository URL currently returns404. The integration verifies the installed `ComfyUI-Krea2T-Enhancer` class source Git blob `c46e1c3ed244caaef231c580bb601ea935b3d1c7` and the pinned native text-fusion source -before attachment. It delegates the original helper's two text-fusion paths and +before attachment. The separately reviewed source blob +`31e24388a8d495a524f46371659d3087e2302664` is also admitted: its basic enhancer +functions, class and numeric constants are identical, with only additional +imports/registrations for independent utilities. Loaded helper code and constants +must match the admitted source; a matching file alone is insufficient. +It delegates the original helper's two text-fusion paths and bounded token delta; no upstream numeric implementation is bundled here. The separate package retains its own MIT notice. Model and task-weight terms remain separate from plugin code; this source compatibility check makes no commercial @@ -229,5 +234,27 @@ guider, evaluation, batch and grounded-wrapper control flow with loading/device boundaries and neural/VAE work explicitly substituted. Failure/short-schedule cleanup evidence is source evidence, not installed-runtime or model acceptance. +Two exact native source pairs are admitted. The original pair binds sampler blob +`a7cc3419e325e7d55aac8c7d5ce6d65f620fc511` to model-base SHA256 +`e7d08a4450287c76994b109437fbd03d721cf373fc8214bb9c30c478db060ffe`. +The reviewed earlier API pair binds sampler blob +`17f7bb8babdeec605586cc08df2ab1c4f1c92fd4` to model-base SHA256 +`5722b46418c6b7475041733d659bbd71f11f1dacb4d0aaff2b21aeb2c82d5542`. +Mixed pairs reject. The earlier sampler omits the writable-model scope; Krea's +inherited writable-model request in the newer source returns None. The consumer +rejects instance writable-model overrides and additional multigpu models, keeping +this equivalence limited to the exact Krea path. Changes to other model families +do not establish their compatibility. Helper and numerical-sampling pins remain +unchanged. + +At active binding, the consumer compiles the admitted source without executing +its module body and compares the observed, unwrapped loaded call-path code with +it, including nested code and exception regions. It also checks loaded enhancer +constants and native fusion methods. This detects stale or replaced function +bodies despite matching files; it is not a whole-runtime or model-state proof. +Device behavior, actual tokenizer/tensors, effect and task quality still require +installed-host validation. Node labels, caller revisions and inference-handle +limitations are unchanged. + To roll back this source addition, retain the previous checkout/package; no old saved graph is rewritten and no model/recipe is migrated by these nodes. diff --git a/kg_krea_v12/compatibility.py b/kg_krea_v12/compatibility.py new file mode 100644 index 0000000..c4dca83 --- /dev/null +++ b/kg_krea_v12/compatibility.py @@ -0,0 +1,52 @@ +"""Compare loaded Python function bodies with admitted source without executing it. + +This observes the imported callable's code, rather than assuming a file hash +proves which code was loaded. Dependency/device/model state needs separate host +verification. No upstream implementation is copied or executed by compilation. +""" +import inspect +import types + + +def _constant(value): + if isinstance(value, types.CodeType): + return ('code', _code_key(value)) + if isinstance(value, tuple): + return tuple(_constant(item) for item in value) + if isinstance(value, frozenset): + return frozenset(_constant(item) for item in value) + return value + + +def _code_key(code): + # Filename/line tables are debugging coordinates. Preserve executable byte + # code, constants, closure layout and exception regions, including nesting. + return (code.co_argcount, code.co_posonlyargcount, code.co_kwonlyargcount, + code.co_nlocals, code.co_stacksize, code.co_flags, code.co_code, + tuple(_constant(item) for item in code.co_consts), code.co_names, + code.co_varnames, code.co_freevars, code.co_cellvars, + getattr(code, 'co_exceptiontable', b'')) + + +def verify_loaded_functions(module, source, names): + compiled = compile(source, '', 'exec', dont_inherit=True) + codes = {} + def collect(code, prefix=()): + for value in code.co_consts: + if isinstance(value, types.CodeType): + path = (*prefix, value.co_name) + codes['.'.join(path)] = value + collect(value, path) + collect(compiled) + for name in names: + value = module + for part in name.split('.'): + value = getattr(value, part, None) + if isinstance(value, (classmethod, staticmethod)): + value = value.__func__ + value = inspect.unwrap(value) + actual = getattr(value, '__code__', None) + expected = codes.get(name) + if (not isinstance(actual, types.CodeType) or expected is None + or _code_key(actual) != _code_key(expected)): + raise RuntimeError('V12 loaded function differs from admitted source: ' + name) diff --git a/kg_krea_v12/consumer.py b/kg_krea_v12/consumer.py index 35232f4..f26e896 100644 --- a/kg_krea_v12/consumer.py +++ b/kg_krea_v12/consumer.py @@ -14,11 +14,20 @@ import uuid from .native import GROUNDING_BLOB, _signature from .studies import NativeStudies +from .compatibility import verify_loaded_functions SAMPLERS_BLOB = 'a7cc3419e325e7d55aac8c7d5ce6d65f620fc511' EULER_SHA256 = 'cfc4221ed0bc1f84a6f5689d3ec440cad3008c2541ab8f5e2599d1a73fac74ce' HELPERS_SHA256 = '53c82235bfea0c4edf3cd5219f080c44c45ac1c9a6c3171f2aae151e18b56b91' MODEL_BASE_SHA256 = 'e7d08a4450287c76994b109437fbd03d721cf373fc8214bb9c30c478db060ffe' +# Exact paired API profiles. The second omits writable_models around sampling; +# Krea's inherited writable-model request in the first is a no-op. Other model +# families and arbitrary cross-pairs are outside this native Krea boundary. +CONSUMER_SOURCE_PROFILES = { + (SAMPLERS_BLOB, MODEL_BASE_SHA256): 'native-krea-writable-scope-v1', + ('17f7bb8babdeec605586cc08df2ab1c4f1c92fd4', + '5722b46418c6b7475041733d659bbd71f11f1dacb4d0aaff2b21aeb2c82d5542'): 'native-krea-pre-writable-scope-v1', +} _lock = threading.Lock() _busy = set() _MISSING = object() @@ -32,6 +41,13 @@ def _source_bytes(obj): return Path(inspect.getfile(obj)).read_bytes().replace(b'\r\n', b'\n') +def _consumer_source_profile(samplers_blob, model_base_sha256): + profile = CONSUMER_SOURCE_PROFILES.get((samplers_blob, model_base_sha256)) + if profile is None: + raise RuntimeError('V12 native guider/model source pair is unverified') + return profile + + def load_consumer_host(): import comfy.samplers as samplers import comfy.sampler_helpers as helpers @@ -39,14 +55,40 @@ def load_consumer_host(): import comfy.k_diffusion.sampling as euler data = _source_bytes(samplers) blob = hashlib.sha1(b'blob ' + str(len(data)).encode() + b'\0' + data).hexdigest() - if blob != SAMPLERS_BLOB: - raise RuntimeError('V12 native guider/sampler source revision is unverified') - for module, expected in ((euler, EULER_SHA256), (helpers, HELPERS_SHA256), (model_base, MODEL_BASE_SHA256)): + model_data = _source_bytes(model_base) + profile = _consumer_source_profile(blob, hashlib.sha256(model_data).hexdigest()) + for module, expected in ((euler, EULER_SHA256), (helpers, HELPERS_SHA256)): if hashlib.sha256(_source_bytes(module)).hexdigest() != expected: raise RuntimeError('V12 native evaluation dependency source revision is unverified') + verify_loaded_functions(samplers, data, ( + 'CFGGuider.sample', 'CFGGuider.outer_sample', 'CFGGuider.inner_sample', + 'CFGGuider.predict_noise', 'CFGGuider.inner_set_conds', 'CFGGuider.set_conds', + 'KSAMPLER.sample', 'KSamplerX0Inpaint.__call__', 'process_conds', + 'calc_cond_batch', '_calc_cond_batch', 'sampling_function', 'cfg_function')) + verify_loaded_functions(model_base, model_data, ( + 'Krea2.__init__', 'Krea2.extra_conds', 'BaseModel.apply_model', + 'BaseModel._apply_model', 'BaseModel.extra_conds', + 'BaseModel.process_latent_in', 'BaseModel.process_latent_out')) + if profile == 'native-krea-writable-scope-v1': + verify_loaded_functions(model_base, model_data, ('BaseModel.get_writable_model',)) + elif hasattr(model_base.BaseModel, 'get_writable_model'): + raise RuntimeError('V12 loaded model has an unverified writable-model hook') + verify_loaded_functions(helpers, _source_bytes(helpers), ('convert_cond',)) + verify_loaded_functions(euler, _source_bytes(euler), ( + 'sample_euler', 'sample_euler_ancestral', 'sample_euler_ancestral_RF', + 'sample_er_sde', 'offset_first_sigma_for_snr', 'sigma_to_half_log_snr')) return samplers, euler, model_base +def _validate_writable_scope(patcher): + # Both admitted source pairs have the same Krea path only when the primary + # class has no instance writable-model override and no additional GPU model + # can enter the newer writable-model scope. Do not infer other model support. + if ('get_writable_model' in vars(patcher.model) + or patcher.get_additional_models_with_key('multigpu')): + raise RuntimeError('V12 native source profiles do not support extra writable/multigpu models') + + def _scalar(value): if not isinstance(value, Real): if not callable(getattr(value, 'numel', None)) or value.numel() != 1: @@ -167,6 +209,7 @@ class NativePhaseGuider: samplers, euler, model_base = self.host if type(guider) is not samplers.CFGGuider or type(guider.model_patcher.model) is not model_base.Krea2: raise RuntimeError('V12 consumer requires the pinned native Krea2 CFGGuider') + _validate_writable_scope(guider.model_patcher) allowed_samplers = (euler.sample_euler, euler.sample_euler_ancestral, getattr(euler, 'sample_er_sde', None)) if (type(sampler) is not samplers.KSAMPLER diff --git a/kg_krea_v12/enhancer.py b/kg_krea_v12/enhancer.py index 535044b..5d18059 100644 --- a/kg_krea_v12/enhancer.py +++ b/kg_krea_v12/enhancer.py @@ -14,8 +14,13 @@ import threading from .ledger import TAP_WIDTH, TAPS from .native import _signature +from .compatibility import verify_loaded_functions ENHANCER_BLOB = 'c46e1c3ed244caaef231c580bb601ea935b3d1c7' +ENHANCER_SOURCE_PROFILES = { + ENHANCER_BLOB: 'krea2t-basic-math-v1', + '31e24388a8d495a524f46371659d3087e2302664': 'krea2t-basic-math-extra-registrations-v1', +} CORE_FUSION_BLOB = 'a904e05e0053d38bda0fdbd70625cd8c5e9b70a2' WRAPPER_KEY = 'kg_native_enhancer_v12' _OTHER_KEYS = ('krea2t_prompt_adherence_enhancer', 'krea2t_prompt_adherence_enhancer_advanced') @@ -31,8 +36,9 @@ def load_enhancer_host(): raise RuntimeError('V12 enhancer requires the pinned original Krea2T enhancer package') data = Path(inspect.getfile(cls)).read_bytes().replace(b'\r\n', b'\n') blob = hashlib.sha1(b'blob ' + str(len(data)).encode() + b'\0' + data).hexdigest() - if blob != ENHANCER_BLOB: + if blob not in ENHANCER_SOURCE_PROFILES: raise RuntimeError('V12 enhancer source revision is unverified') + enhancer_blob = blob module = sys.modules[cls.__module__] helper = getattr(module, '_enhanced_txtfusion_forward', None) compatible = getattr(module, '_is_krea2_dm', None) @@ -40,12 +46,23 @@ def load_enhancer_host(): or inspect.getfile(helper) != inspect.getfile(cls) or inspect.getfile(compatible) != inspect.getfile(cls)): raise RuntimeError('V12 enhancer helper does not belong to the pinned source') + verify_loaded_functions(module, data, ( + '_is_krea2_dm', '_chunk_gains', '_run_refiners', + '_run_txtfusion_parts', '_enhanced_txtfusion_forward')) + profile = (1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 2.5, 5.0, 1.1, 4.0, 1.0) + expected_constants = {'KREA2_TAP_LAYERS': TAPS, 'KREA2_TAP_DIM': TAP_WIDTH, + 'KREA2_CHUNK_COUNT': 24, 'KREA2_CHUNK_DIM': 1280, + 'ENHANCER_PROFILE_12': profile, 'ENHANCER_CHUNK_PROFILE': profile + profile, + 'ENHANCER_GLOBAL_MULTIPLIER': 15.0, 'TXTFUSION_TOKEN_REL_CAP': 0.75} + if any(getattr(module, name, None) != value for name, value in expected_constants.items()): + raise RuntimeError('V12 loaded enhancer constants differ from admitted source') import comfy.ldm.krea2.model as core data = Path(inspect.getfile(core.TextFusionTransformer)).read_bytes().replace(b'\r\n', b'\n') blob = hashlib.sha1(b'blob ' + str(len(data)).encode() + b'\0' + data).hexdigest() if blob != CORE_FUSION_BLOB: raise RuntimeError('V12 enhancer native text-fusion source revision is unverified') - return helper, compatible + verify_loaded_functions(core, data, ('TextFusionTransformer.forward', 'TextFusionBlock.forward')) + return helper, compatible, enhancer_blob @contextmanager @@ -81,7 +98,7 @@ def apply_enhancer(model, enabled=False, strength=1.0): raise ValueError('V12 enhancer strength must be finite within [0, 2]') if not enabled or strength == 0: return model, 'Krea native enhancer V12: OFF; original MODEL retained.' - helper, compatible = load_enhancer_host() + helper, compatible, enhancer_blob = load_enhancer_host() dm = model.model.diffusion_model _check_architecture(dm, compatible) original_options_signature = _signature(model.model_options) @@ -90,7 +107,7 @@ def apply_enhancer(model, enabled=False, strength=1.0): raise RuntimeError('V12 enhancer refuses an already enhanced MODEL') if 'krea2_edit' in original_wrappers: raise RuntimeError('V12 enhancer must precede the native edit patch, which bypasses inner wrappers') - config = {'enabled': True, 'strength': float(strength), 'source_blob': ENHANCER_BLOB} + config = {'enabled': True, 'strength': float(strength), 'source_blob': enhancer_blob} def wrapper(executor, *args, **kwargs): options = kwargs.get('transformer_options') diff --git a/tests/test_krea_v12_compatibility.py b/tests/test_krea_v12_compatibility.py new file mode 100644 index 0000000..80e0dce --- /dev/null +++ b/tests/test_krea_v12_compatibility.py @@ -0,0 +1,92 @@ +"""Source-admission and observed loaded-code regressions; no host or model import.""" +import functools +import importlib.util +from pathlib import Path +import sys +from types import ModuleType, SimpleNamespace as NS +import unittest + +ROOT = Path(__file__).resolve().parents[1] +SPEC = importlib.util.spec_from_file_location('compatibility_v12_tests', ROOT / 'kg_krea_v12/__init__.py', + submodule_search_locations=[str(ROOT / 'kg_krea_v12')]) +PACKAGE = importlib.util.module_from_spec(SPEC) +sys.modules[SPEC.name] = PACKAGE +SPEC.loader.exec_module(PACKAGE) +from compatibility_v12_tests import compatibility, consumer, enhancer + + +class CompatibilityTests(unittest.TestCase): + def test_exact_core_pairs_admit_but_cross_pairs_and_unknown_reject(self): + pairs = list(consumer.CONSUMER_SOURCE_PROFILES) + self.assertEqual(len(pairs), 2) + for pair in pairs: + self.assertTrue(consumer._consumer_source_profile(*pair)) + for pair in ((pairs[0][0], pairs[1][1]), (pairs[1][0], pairs[0][1]), ('unknown', pairs[0][1])): + with self.assertRaisesRegex(RuntimeError, 'source pair is unverified'): + consumer._consumer_source_profile(*pair) + + def test_loaded_body_changed_after_disk_read_rejects(self): + source = 'def predict(x):\n return x + 1\n' + module = ModuleType('observed_source_fixture') + exec(compile(source, '', 'exec', dont_inherit=True), module.__dict__) + compatibility.verify_loaded_functions(module, source.encode(), ('predict',)) + changed = ModuleType('changed') + exec('def predict(x):\n return x - 1\n', changed.__dict__) + module.predict = changed.predict + with self.assertRaisesRegex(RuntimeError, 'loaded function differs'): + compatibility.verify_loaded_functions(module, source.encode(), ('predict',)) + + def test_loaded_nested_code_and_exception_cleanup_must_match(self): + source = ('class Guider:\n' + ' def sample(self, x):\n' + ' def evaluate():\n' + ' return x + 1\n' + ' try:\n' + ' return evaluate()\n' + ' finally:\n' + ' self.clean = True\n') + module = ModuleType('nested_source_fixture') + exec(compile(source, '', 'exec', dont_inherit=True), module.__dict__) + compatibility.verify_loaded_functions(module, source.encode(), ('Guider.sample',)) + other = ModuleType('changed_nested') + exec(compile(source.replace('x + 1', 'x + 2'), '', 'exec', dont_inherit=True), other.__dict__) + module.Guider.sample = other.Guider.sample + with self.assertRaisesRegex(RuntimeError, 'loaded function differs'): + compatibility.verify_loaded_functions(module, source.encode(), ('Guider.sample',)) + + def test_standard_decorator_unwrap_matches_original_loaded_body(self): + source = 'def step(x):\n return x * 2\n' + module = ModuleType('decorated_source_fixture') + exec(compile(source, '', 'exec', dont_inherit=True), module.__dict__) + original = module.step + @functools.wraps(original) + def wrapper(*args, **kwargs): + return original(*args, **kwargs) + module.step = wrapper + compatibility.verify_loaded_functions(module, source.encode(), ('step',)) + + def test_source_compilation_never_executes_module_body(self): + source = 'must_never_execute()\ndef step(x):\n return x\n' + module = ModuleType('compile_only_fixture') + exec(compile('def step(x):\n return x\n', '', 'exec', dont_inherit=True), module.__dict__) + compatibility.verify_loaded_functions(module, source.encode(), ('step',)) + + def test_extra_writable_or_multigpu_models_reject(self): + patcher = NS(model=NS(), get_additional_models_with_key=lambda name: []) + consumer._validate_writable_scope(patcher) + patcher.model.get_writable_model = lambda *args: object() + with self.assertRaisesRegex(RuntimeError, 'extra writable/multigpu'): + consumer._validate_writable_scope(patcher) + del patcher.model.get_writable_model + patcher.get_additional_models_with_key = lambda name: [object()] + with self.assertRaisesRegex(RuntimeError, 'extra writable/multigpu'): + consumer._validate_writable_scope(patcher) + + def test_enhancer_profiles_bind_only_two_reviewed_whole_sources(self): + self.assertEqual(set(enhancer.ENHANCER_SOURCE_PROFILES), + {'c46e1c3ed244caaef231c580bb601ea935b3d1c7', + '31e24388a8d495a524f46371659d3087e2302664'}) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_krea_v12_enhancer.py b/tests/test_krea_v12_enhancer.py index 8f37a7e..f65acb9 100644 --- a/tests/test_krea_v12_enhancer.py +++ b/tests/test_krea_v12_enhancer.py @@ -60,7 +60,7 @@ class EnhancerV12Tests(unittest.TestCase): self.calls.append((fusion, x, kwargs)) return NS(shape=(1, 2, 2560), value='delegated-helper'), None - host_patch = patch.object(enhancer, 'load_enhancer_host', return_value=(helper, lambda dm: True)) + host_patch = patch.object(enhancer, 'load_enhancer_host', return_value=(helper, lambda dm: True, enhancer.ENHANCER_BLOB)) host_patch.start() self.addCleanup(host_patch.stop) self.addCleanup(self.assertFalse, enhancer._busy) @@ -175,7 +175,7 @@ class EnhancerV12Tests(unittest.TestCase): self.assertEqual(self.calls, []) def helper(*args, **kwargs): return NS(shape=(1, 3, 2560)), None - with patch.object(enhancer, 'load_enhancer_host', return_value=(helper, lambda dm: True)): + with patch.object(enhancer, 'load_enhancer_host', return_value=(helper, lambda dm: True, enhancer.ENHANCER_BLOB)): _, malformed_options, malformed_wrapper = self.attached() with self.assertRaisesRegex(RuntimeError, 'helper returned'): malformed_wrapper(Executor(dm), transformer_options=malformed_options)