Skip to content

Commit f142edc

Browse files
Merge branch 'feat/issue57-connected-generations' into feat/issue57-retrieval-generations
# Conflicts: # SHA256SUMS # references/connected-generation-transfer-v1.md # tests/test_connected_generation_controller.py
2 parents 64fb52c + 4bb78db commit f142edc

7 files changed

Lines changed: 202 additions & 9 deletions

‎SHA256SUMS‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@ c8c53629ab5a059972e2aa2d32c3fe8882648376baaa7550ea1cc9b12287d5e5 jev_integratio
400400
31e7b9bcd68377f1c8cf54f0531f8d33c4ebb2cf3360dac141094fd903455f7f jev_integration_evaluator/template_connected_binding.py
401401
b630cd38cd8600d8decce9f5176e193b25f8ffbecba8d09316e3367fd6cc66a1 jev_integration_evaluator/template_connected_composite_binding.py
402402
cca7693deb80c91c2e6dd2e1f7bed4aec0a3d864659dd5adeeaa8cb8babdb4ff jev_integration_evaluator/template_connected_delivery.py
403-
d83b82b65be4de1d0afdc9031f09ad1ad02a87f55205a3c1df085c556ecea795 jev_integration_evaluator/template_connected_generation.py
403+
22784e53139d404c9a27248bf7ec87cc6a211d0148fc4ebc0c58eada465b393b jev_integration_evaluator/template_connected_generation.py
404404
f4643f535e8c3d778f209cd86dce3559b953119d428fe82f2ab396a5943d372f jev_integration_evaluator/template_delivery.py
405405
7be56a2ac9a95ad71eefc899347d6e79e88e1442e7c155c7124ca79da27b6e2a jev_integration_evaluator/template_delivery_journey.py
406406
f3ff7a0c96e0bd39b1131324704db557264df88cc94af983fa2519a9921f7a42 jev_integration_evaluator/template_installation.py
@@ -424,7 +424,7 @@ f3ff7a0c96e0bd39b1131324704db557264df88cc94af983fa2519a9921f7a42 jev_integratio
424424
a0094293d0f6072b704981ebe6bc372832018c38656893ae92149c1f8fd8702f jev_integration_evaluator/windows_template_plan.py
425425
607f6da51e7798fee43d0f34b93af47231345e2799f6b4d990fa8c5df204fc58 jev_integration_evaluator/windows_template_preflight.py
426426
fda511c1f617ccd1eb215430a8ccb11be6b3341c5c5e0573bf6e419be76f8ad8 jev_integration_evaluator/windows_template_run.py
427-
2cb17bc387b070b6fef08c6003f817e41f8b8b3e83f1d8a0efdb88b26d9bba04 jev_integration_evaluator/windows_template_session.py
427+
8a1a61dc2667b81635cda91878aebc50074ec83ecbdfb46685f4de5d05c88c8d jev_integration_evaluator/windows_template_session.py
428428
60cbd6c2dcda05901508a7e84f03bca928a8b4b6da78a6bc89397cdb9f3a7d4b jev_integration_evaluator/windows_template_tree.py
429429
39f73cbb735867193f2459ed813a588808b18e9e9ab4c615c0d49d76b72bad54 package-lock.json
430430
908466d7d6ad932102f9b6f2ad2441a44048ce2ff7ae6c7d9741f75290ffcbd5 package.json
@@ -439,7 +439,7 @@ cbbd2a92eeb685c0e859746fd8ebc40fd8bfaa9fb2901d6cf79a31f183ed4517 references/arc
439439
fdd82ced94ade58abbff02589b21a318670711dabb276b544e12726b70554614 references/confidence-and-calibration.md
440440
9fb76e904994c7f8c9fa96e45c37d4c8b4fc67ae9e5996c98ea8e140e850724f references/connected-composite-binding-v1.md
441441
04c035b2a13d500835d6ba366eabb22a57bc156b343c68a4dedaa2c6bdbc31c3 references/connected-delivery-v1.md
442-
f6948f52ac3e82e09e78476d9339fa22ac1fede8afda9398a037db4528913586 references/connected-generation-transfer-v1.md
442+
0434235e45293d7ca60a4ad0e60e96d5859869d80f380618e42ab9d2e74e24db references/connected-generation-transfer-v1.md
443443
6d0d3a9f92c643123cf3c577a13a94bffe9a011515688db40ddb9f5067a835b5 references/connected-installed-binding-v1.md
444444
313892a92e30412b83466577dcc05e4437e26fd53e735787de3e3c8dcedcefad references/data-contracts.md
445445
20c354b1b84ad6e079a49ce8098f261411cd469ce7176afa0297762a1d5b2b09 references/executable-implementation-prompt.md
@@ -483,7 +483,7 @@ b5b0ef31ba447bba9a9815838bd0c0f90aeb68049e0f7fbae73a0c39f4512fc7 references/tem
483483
38bd959ff5ecdca22e6f10ca2fa85316f7583a3281a512c44efdb1e2aa9a54ba references/template-python-entrypoint-v1.md
484484
dd802dcaa1423a4fe6ffec9902d0ccf9ea47bb211a4f32a273b5e3a81eb7cac0 references/use-case-template-matrix-v1.md
485485
a1631fbda0ce82cc7a2981762e1927af12044289f5ec5a316baa63aeed41dbd1 references/windows-source-mutation-v1.md
486-
f3ef9e5ca7db8391db385d142cbbbd02536bcb02a3b762233a6ef323df245958 references/windows-template-connected-v1.md
486+
750c2cf6e5647d9826b59a3b2ef922b1028321a387a56e020c255cb4bb22aea4 references/windows-template-connected-v1.md
487487
d461878417cebbda60849c096065a24f3559455da62e56fe534c34560ff0e61c references/windows-template-native-delivery-v1.md
488488
b815ddf80c0f8f51a5df7da6948e7cbdbb7861172614933111a2cf11d6488d25 references/windows-template-preparation-v1.md
489489
a83cc2b9b0bcde277c3c46d7db16bcd59d55e6e72663085ca9cc272d0cc57049 reports/README.md
@@ -847,7 +847,7 @@ f15e9b55163998541e8d762c36f383cba0395bf818d45871da19c26856e1c8e0 tests/test_cod
847847
0a050b134c0904e9d2c38333dd60a50ee3c5116164c7900755f1b88df1a573fa tests/test_composite_console_delivery.py
848848
4845bc21ff445511473bcd900c600f9e747b2a5ab885feb08bf5aaab341c265c tests/test_composite_transaction.py
849849
b93acf12f8ac02c948c571145b116e22c4e6d00cb8153e9fef944b710ae3939f tests/test_connected_composite_binding.py
850-
02c9c454102093bc3529281021166ed496d633039cee3820a6a6c9d857ddbb67 tests/test_connected_generation_controller.py
850+
e97afe18f6e3371b49afa8b7a87c541cb868338335578fabf51aa94555f1f02f tests/test_connected_generation_controller.py
851851
b3a657fabcb1e42b1113b7961aec72085f6c32b56e3f27114fa6d5c7a5d5b7ad tests/test_connected_generation_installed.py
852852
33287bc85719ffb56171ce07597feb9f7351a214739a281fdbe7481815a4e2f2 tests/test_connected_generation_ledger.py
853853
38110602249b8248c33cc357076f083717bd95326764f513ad5190f3a5281fd5 tests/test_connected_generation_public_key_anchor.py
@@ -972,7 +972,7 @@ b9f61fb2194a29268d009676c6c945526d5b0086685f1ca3fed8b1febde8d3e0 tests/test_win
972972
1aa9aad36184e19517d5cc5b0404688cb54555b2b7a2c79c1e95349832f96b26 tests/test_windows_template_package_inputs.py
973973
0f92e9c458fcbb73fabe35d8a5fb4a774ff5623054612d4c6f1f7bbf8dfd6a10 tests/test_windows_template_preflight.py
974974
8234c141d7b7001f1593f6e9e1561286e53c682c1f67d15dc77b2ae34bbd3113 tests/test_windows_template_run.py
975-
5b360774d92d10809b9b140825f85b2dadb722cbd69affd1eb71ecdc66e9b918 tests/test_windows_template_session.py
975+
b7c20ee8ab31fc878198acfe5aa7be7c8b333292594422a0bc6eb8e7522a2351 tests/test_windows_template_session.py
976976
a6b1a7a7ecff4702035ac40f57f00351870bdb05f0dd318e3f88f9b759a411db tests/test_work_queue_installed.py
977977
44547431d46b71d4b2df2447b96e777a6682b638e252ade1d62bc0c36e6c6460 tests/test_work_queue_oracle.py
978978
bd01f12283775caa54cbbb7ccdd95bc2058e5d7396af412cbcffb4a935331428 tests/windows_install_death_child.py

‎jev_integration_evaluator/template_connected_generation.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,12 @@ def plan_connected_generation_transfer(old_session: str | Path, new_plan: dict,
258258
or parse_utc(expires_at) > min(parse_utc(cutoff),
259259
parse_utc(new['egress_expires_at']))):
260260
raise ConnectedGenerationError('connected_generation_scope_invalid')
261+
# Opening a ledger creates one when both files are absent. A transfer
262+
# plan must derive from the stopped session's existing durable history;
263+
# a fresh empty ledger would silently reset spend, tasks and effects.
264+
if (not old['ledger'].is_file()
265+
or not Path(str(old['ledger']) + '.sqlite').is_file()):
266+
raise ConnectedGenerationError('connected_generation_existing_ledger_required')
261267
ledger = RuntimeLedger(old['ledger'], identity=old['identity'], **limits)
262268
try:
263269
history = digest(ledger.generation_snapshot())

‎jev_integration_evaluator/windows_template_session.py‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -216,8 +216,18 @@ def _owned_process(identity: dict, *, terminate: bool = False) -> tuple[bool, ob
216216
return False, None, None
217217
if wait != 0x102:
218218
raise InputError('windows_session_process_state_unavailable')
219-
if _identity(process, kernel) != (identity['created_filetime'],
220-
identity['image'].casefold()):
219+
try:
220+
current_identity = _identity(process, kernel)
221+
except InputError:
222+
# During process teardown, an image query can return access
223+
# denied while a zero-time wait still reports live. Wait only a
224+
# bounded 100 ms on this same retained handle. Only a signaled
225+
# handle proves exit; every other result remains uncertain.
226+
if kernel.WaitForSingleObject(process, 100) == 0:
227+
return False, None, None
228+
raise
229+
if current_identity != (identity['created_filetime'],
230+
identity['image'].casefold()):
221231
return False, None, None
222232
job = kernel.OpenJobObjectW(0x000c if terminate else 0x0004,
223233
False, identity['job'])

‎references/connected-generation-transfer-v1.md‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ protocol. Package and install steps
88
remain off mode. Transfer never
99
launches a console, creates provider authority, resets spend or changes the
1010
run ID. A retained environment can be selected by a separately signed reverse
11-
transfer. This controller accepts the legacy Alpha and finite D profiles only;
11+
transfer. Planning requires the stopped session's existing ledger marker and
12+
database; a missing ledger fails closed as
13+
`connected_generation_existing_ledger_required` instead of being recreated
14+
empty. This controller accepts the legacy Alpha and finite D profiles only;
1215
composite placement and Windows generation transfer have no path here.
1316

1417
The host installs both generations through existing offline receipts, derives

‎references/windows-template-connected-v1.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,21 @@ restore authority. Stop targets the exact retained Job and verifies process
3030
death. A launch intent makes replay a refusal, including after an interrupted
3131
attempt. Recovery of an unknown pending launch requires independent review.
3232

33+
The merged #118 Windows Server 3.13 run failed once while polling a synthetic
34+
child: its first Win32 wait reported live, then `QueryFullProcessImageNameW`
35+
could not return the image. The hosted Win32 error value was not recorded.
36+
A local Windows 3.13 installed run with a zero-time recheck repeated the same
37+
failure after 3,391 seconds; its failed JUnit receipt is retained separately.
38+
A local real-process/Job probe reproduced `ERROR_ACCESS_DENIED` (5) while the
39+
same handle remained unsignaled, then observed exit within 100 ms in 40 of 40
40+
short-child trials. This supports process teardown as the cause but does not
41+
identify the hosted error value. After an identity-query failure, observation
42+
waits at most 100 ms on the same retained handle. Only a signaled handle is
43+
classified exited; a live or unknown result retains the original identity
44+
error. The real Win32 regression injects an image-query failure after an
45+
owned process has exited; separate controlled wait states prove that a
46+
still-live or uncertain process is never treated as exited.
47+
3348
The child receives a credential reference resolved only at launch, public
3449
key and reference hashes, and a bounded environment. It never receives the
3550
issuer private key. Windows CNG verifies signed grants using a pinned

‎tests/test_connected_generation_controller.py‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from __future__ import annotations
33

44
import base64
5+
from datetime import datetime, timedelta, timezone
56
import hashlib
67
from pathlib import Path
78
import subprocess
@@ -11,6 +12,8 @@
1112

1213
from jev_integration_evaluator.template_connected_generation import _verify, _OPENSSL_ENV
1314
from jev_integration_evaluator import template_connected_generation as generation
15+
from jev_integration_evaluator.integrations.runtime_ledger import RuntimeLedger
16+
from jev_integration_evaluator.io import digest
1417

1518

1619
@pytest.mark.skipif(sys.platform != 'linux', reason='fixed OpenSSL verifier is Linux only')
@@ -83,3 +86,51 @@ def swap_before_verify(args, **kwargs):
8386
assert not _verify(trusted, 'generation_transfer', exact, signature,
8487
expected_public_sha256=expected_public_sha256)
8588
assert swapped == [True]
89+
90+
91+
LIMITS = dict(max_calls_per_task=2, max_cost_per_task=1,
92+
max_total_calls=3, max_total_cost=2, max_in_flight=2, max_tasks=4)
93+
94+
95+
def _planned(monkeypatch, tmp_path):
96+
"""Bypass installed-source checks so only the ledger precondition is exercised."""
97+
ledger = tmp_path / 'runtime-ledger'
98+
far = (datetime.now(timezone.utc) + timedelta(days=1)).isoformat().replace('+00:00', 'Z')
99+
monkeypatch.setattr(generation.delivery.offline, '_linux_profile', lambda: None)
100+
monkeypatch.setattr(generation, '_stopped',
101+
lambda directory, trusted_head: ({}, {'plan_sha256': 'a' * 64}))
102+
monkeypatch.setattr(generation.delivery, '_check_plan', lambda plan: None)
103+
installed = iter(({'identity': digest('old reviewed runtime'), 'placement': 'seam',
104+
'ledger': ledger, 'binding_sha256': 'b' * 64, 'public': 'c' * 64,
105+
'egress_expires_at': far},
106+
{'identity': digest('new reviewed runtime'), 'placement': 'seam',
107+
'ledger': ledger, 'binding_sha256': 'd' * 64, 'public': 'c' * 64,
108+
'egress_expires_at': far}))
109+
monkeypatch.setattr(generation, '_installed',
110+
lambda plan, dependency_plan, limits: next(installed))
111+
now = datetime.now(timezone.utc)
112+
stamp = lambda delta: (now + delta).isoformat().replace('+00:00', 'Z')
113+
return ledger, dict(trusted_old_head='e' * 64, old_dependency_plan={},
114+
new_dependency_plan={}, limits=LIMITS, action='upgrade',
115+
issued_at=stamp(timedelta(minutes=-1)),
116+
expires_at=stamp(timedelta(minutes=5)))
117+
118+
119+
def test_transfer_plan_requires_existing_durable_ledger(tmp_path, monkeypatch):
120+
ledger, arguments = _planned(monkeypatch, tmp_path)
121+
with pytest.raises(generation.ConnectedGenerationError,
122+
match='^connected_generation_existing_ledger_required$'):
123+
generation.plan_connected_generation_transfer(tmp_path / 'old', {'plan_sha256': 'f' * 64},
124+
**arguments)
125+
# Planning must not manufacture an empty ledger in place of the missing history.
126+
assert not ledger.exists() and not Path(str(ledger) + '.sqlite').exists()
127+
128+
129+
def test_transfer_plan_derives_from_existing_ledger_history(tmp_path, monkeypatch):
130+
ledger, arguments = _planned(monkeypatch, tmp_path)
131+
RuntimeLedger(ledger, identity=digest('old reviewed runtime'), **LIMITS).release()
132+
grant = generation.plan_connected_generation_transfer(tmp_path / 'old',
133+
{'plan_sha256': 'f' * 64}, **arguments)
134+
assert grant['kind'] == 'connected-generation-transfer-v1'
135+
assert grant['old_identity'] == digest('old reviewed runtime')
136+
assert grant['new_identity'] == digest('new reviewed runtime')

‎tests/test_windows_template_session.py‎

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,114 @@
2626
)
2727

2828

29+
@pytest.mark.parametrize('recheck,exited', ((0, True), (0x102, False),
30+
(0xffffffff, False)))
31+
@pytest.mark.parametrize('terminate', (False, True))
32+
def test_identity_query_race_requires_same_handle_exit(monkeypatch, recheck, exited,
33+
terminate):
34+
class Kernel:
35+
def __init__(self):
36+
self.waits = [0x102, recheck]
37+
self.closed = []
38+
39+
def OpenProcess(self, access, inherit, pid):
40+
assert access == 0x101000 and not inherit and pid == 17
41+
return 123
42+
43+
def WaitForSingleObject(self, handle, timeout):
44+
assert handle == 123 and timeout == (0 if len(self.waits) == 2 else 100)
45+
return self.waits.pop(0)
46+
47+
def CloseHandle(self, handle):
48+
self.closed.append(handle)
49+
50+
def OpenJobObjectW(self, *args):
51+
pytest.fail('uncertain identity must not reach job ownership')
52+
53+
kernel = Kernel()
54+
monkeypatch.setattr(native_session, '_kernel', lambda: kernel)
55+
56+
def unavailable(handle, actual_kernel):
57+
assert handle == 123 and actual_kernel is kernel
58+
raise InputError('windows_session_process_identity_unavailable')
59+
60+
monkeypatch.setattr(native_session, '_identity', unavailable)
61+
identity = {'pid': 17, 'created_filetime': 1, 'image': 'python.exe',
62+
'job': 'Local\\owned'}
63+
if exited:
64+
assert native_session._owned_process(identity, terminate=terminate) == (False, None, None)
65+
else:
66+
with pytest.raises(InputError, match='windows_session_process_identity_unavailable'):
67+
native_session._owned_process(identity, terminate=terminate)
68+
assert kernel.waits == []
69+
assert kernel.closed == [123]
70+
71+
72+
@pytest.mark.skipif(os.name != 'nt', reason='native Windows process handles only')
73+
def test_real_process_exit_during_image_query_is_rechecked_on_retained_handle(monkeypatch):
74+
target = subprocess.Popen([sys._base_executable, '-I', '-c', 'import time;time.sleep(30)'],
75+
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
76+
stderr=subprocess.DEVNULL,
77+
creationflags=subprocess.CREATE_NO_WINDOW)
78+
kernel = _kernel()
79+
unrelated = None
80+
job = None
81+
try:
82+
unrelated = subprocess.Popen([sys._base_executable, '-I', '-c', 'import time;time.sleep(30)'],
83+
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
84+
stderr=subprocess.DEVNULL,
85+
creationflags=subprocess.CREATE_NO_WINDOW)
86+
job_name = 'Local\\jev-template-race-' + str(uuid.uuid4())
87+
job = kernel.CreateJobObjectW(None, job_name)
88+
assert job and kernel.AssignProcessToJobObject(job, int(target._handle))
89+
created, image = _identity(int(target._handle), kernel)
90+
identity = {'pid': target.pid, 'created_filetime': created, 'image': image,
91+
'job': 'unused-before-identity-recheck'}
92+
93+
class ExitDuringQuery:
94+
def __init__(self):
95+
self.query_attempted = False
96+
self.opened = None
97+
self.closed = []
98+
99+
def __getattr__(self, name):
100+
return getattr(kernel, name)
101+
102+
def OpenProcess(self, *args):
103+
self.opened = kernel.OpenProcess(*args)
104+
return self.opened
105+
106+
def QueryFullProcessImageNameW(self, *args):
107+
target.terminate()
108+
target.wait(timeout=5)
109+
# Invoke the actual API on the retained Win32 handle after
110+
# owned process exit. Force a query failure regardless of
111+
# OS-specific post-exit behavior so the recheck is deterministic.
112+
kernel.QueryFullProcessImageNameW(*args)
113+
self.query_attempted = True
114+
ctypes.set_last_error(5)
115+
return 0
116+
117+
def CloseHandle(self, handle):
118+
self.closed.append(handle)
119+
return kernel.CloseHandle(handle)
120+
121+
raced = ExitDuringQuery()
122+
monkeypatch.setattr(native_session, '_kernel', lambda: raced)
123+
assert native_session._owned_process(identity) == (False, None, None)
124+
assert raced.query_attempted and raced.opened in raced.closed
125+
assert unrelated.poll() is None
126+
finally:
127+
if job:
128+
kernel.CloseHandle(job)
129+
if target.poll() is None:
130+
target.terminate()
131+
target.wait(timeout=5)
132+
if unrelated is not None and unrelated.poll() is None:
133+
unrelated.terminate()
134+
unrelated.wait(timeout=5)
135+
136+
29137
@pytest.mark.skipif(os.name != 'nt', reason='native Windows Job Object only')
30138
def test_owned_job_stop_does_not_touch_unrelated_process(tmp_path, monkeypatch):
31139
owned = create_private_directory(tmp_path / 'session')

0 commit comments

Comments
 (0)