Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/product-version-operations/repeat-update-fence-2.7.24.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"baseline_version": "2.7.23",
"classification_rationale": "installed-updater-repeatability-remediation",
"component": "product",
"event_lineage": "installed-updater-repeatability-remediation",
"expected_head": "6cd60f983d0cb8a9d68ad377d34fbe4c7a8a7a44",
"operation_id": "repeat-update-fence-2.7.24",
"policy_revision": "forge-bootstrap-release-cadence-v2",
"product": "forge",
"projection_paths": "product-version.json",
"release_class": "PATCH",
"requested_bump": "patch",
"requested_version": null,
"schema_version": "1",
"target_version": "2.7.24"
}
20 changes: 12 additions & 8 deletions docs/operations/FORGE_INSTALLED_UPDATE_RUNBOOK.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Forge installed update controller

Status: bounded product-owned maintenance provisioner for the selected Forge
2.7.21 to 2.7.22 schema-37-to-38 transition and the selected 2.7.22 to 2.7.23
same-schema corrective transition.
2.7.21 to 2.7.22 schema-37-to-38 transition and the selected 2.7.22/2.7.23 to
2.7.23/2.7.24 same-schema corrective transitions.

This controller closes one concrete product provisioning gap. It is not the
universal Forge Platform installer, an installer UI, a new service supervisor,
Expand All @@ -19,7 +19,7 @@ cannot be called by the installed runtime as a second self-installer.
## Supported operation

The controller accepts only one explicitly bound existing installation and an
exact `forge-autonomy` wheel for one of those two transitions. Every invocation
exact `forge-autonomy` wheel for one of those bounded transitions. Every invocation
binds:

- operation, runtime, installation, and peer-configuration identities;
Expand All @@ -40,9 +40,10 @@ interpreter, version, and bytes.
1. Read the wheel once through a no-follow descriptor; validate its digest,
canonical RECORD, purelib tag, package metadata, member allowlist, and the
exact terminal release receipt without importing it. The historical
2.7.22 reconciliation receipt retains its dedicated validation; 2.7.23 must
have the normal protected release-complete publication/readback/cleanup
shape and cannot be presented to the older transition.
2.7.22 reconciliation receipt retains its dedicated validation; 2.7.23 and
2.7.24 must have the normal protected release-complete
publication/readback/cleanup shape and cannot be presented to an
unsupported transition.
2. Create an isolated versioned runtime slot outside the source checkout with
the explicit Python interpreter. Extract only the already validated bytes
into a pip-free virtual environment, then verify every installed file and
Expand All @@ -66,7 +67,9 @@ interpreter, version, and bytes.
reset-table additions and a fresh idle control row. The 38-to-38 route
permits no table additions and requires the complete reset state and every
domain/security/configuration row to remain byte-logically unchanged.
8. Point the stable resolver at a maintenance fence. Take an exclusive SQLite
8. Normalize the product-owned maintenance launcher to operation-independent
canonical bytes, accepting only the exact older operation-labelled shape,
then point the stable resolver at that fence. Take an exclusive SQLite
writer boundary, prove the live database is still byte-logically identical
to the backed-up snapshot, and atomically install the already Forge-migrated
database copy. The replacement remains read-only until activation and final
Expand Down Expand Up @@ -112,7 +115,8 @@ separate authority and compatibility proof.
writer rejection, both bounded schema transitions, preservation
of Missions, allocations, reviews, execution receipts, governance grants,
configuration and identity, concurrent-operation exclusion, resolver adoption,
canonical receipt shape, path safety, exact slot contents, exclusive atomic
prior-operation fence normalization and tamper rejection, canonical receipt
shape, path safety, exact slot contents, exclusive atomic
database replacement, late-writer rejection, and interruption before
migration, after migration, and during activation. An opt-in test runs the
entire route and replay against the exact published wheel and terminal release
Expand Down
2 changes: 1 addition & 1 deletion product-version.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"product": "forge",
"schema_version": 1,
"version": "2.7.23"
"version": "2.7.24"
}
57 changes: 45 additions & 12 deletions scripts/update_installed_forge.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,14 @@
SUPPORTED_TRANSITIONS = {
("2.7.21", "2.7.22"): (37, 38),
("2.7.22", "2.7.23"): (38, 38),
("2.7.22", "2.7.24"): (38, 38),
("2.7.23", "2.7.24"): (38, 38),
}
NORMAL_RELEASE_TRANSITIONS = frozenset({
("2.7.22", "2.7.23"),
("2.7.22", "2.7.24"),
("2.7.23", "2.7.24"),
})
PHASE_ORDER = {
phase: index for index, phase in enumerate((
"PREPARED", "STAGED", "ADOPTED", "BACKED_UP", "MIGRATION_QUALIFIED",
Expand Down Expand Up @@ -183,6 +190,24 @@ def _atomic_json(path: Path, value: object) -> None:
temporary.unlink()


def _atomic_regular_file(path: Path, payload: bytes, *, mode: int) -> None:
"""Replace one managed regular file without exposing partial bytes."""
_safe_directory(path.parent, create=True)
descriptor, name = tempfile.mkstemp(prefix=f".{path.name}.tmp-", dir=path.parent)
temporary = Path(name)
try:
os.fchmod(descriptor, mode)
with os.fdopen(descriptor, "wb") as handle:
handle.write(payload)
handle.flush()
os.fsync(handle.fileno())
os.replace(temporary, path)
os.chmod(path, mode)
finally:
if temporary.exists():
temporary.unlink()


def _read_json(path: Path) -> dict[str, Any]:
try:
value = json.loads(_read_regular_bytes(path))
Expand Down Expand Up @@ -380,7 +405,7 @@ def _normal_release_evidence(
request: UpdateRequest, receipt: Mapping[str, Any], manifest: Mapping[str, str],
receipt_path: Path,
) -> dict[str, Any]:
"""Validate the normal 2.7.23 release route without weakening 2.7.22 recovery."""
"""Validate current normal-release transitions without weakening 2.7.22 recovery."""
expected_name = f"forge_autonomy-{request.version}-py3-none-any.whl"
sdist_name = f"forge_autonomy-{request.version}.tar.gz"
qualification = receipt.get("qualification")
Expand All @@ -399,7 +424,7 @@ def _normal_release_evidence(
}
exact_observed = {expected_name: request.wheel_sha256, sdist_name: sdist_digest}
if (
(request.existing_version, request.version) != ("2.7.22", "2.7.23")
(request.existing_version, request.version) not in NORMAL_RELEASE_TRANSITIONS
or set(receipt) != expected_top
or receipt.get("state") != "RELEASE_COMPLETE"
or receipt.get("product") != "forge"
Expand Down Expand Up @@ -459,7 +484,7 @@ def _qualified_artifact(request: UpdateRequest) -> tuple[dict[str, Any], bytes,
raise InstalledForgeUpdateError("qualification receipt is malformed") from error
if not isinstance(receipt, dict):
raise InstalledForgeUpdateError("qualification receipt is malformed")
if (request.existing_version, request.version) == ("2.7.22", "2.7.23"):
if (request.existing_version, request.version) in NORMAL_RELEASE_TRANSITIONS:
return (
_normal_release_evidence(request, receipt, manifest, receipt_path),
wheel_bytes,
Expand Down Expand Up @@ -1113,17 +1138,25 @@ def _adopt_resolver(self, state: dict[str, Any]) -> dict[str, Any]:
raise InstalledForgeUpdateError("retained legacy entrypoint does not match the pinned resolver bytes")
elif file_digest(self.legacy_entrypoint) != self.request.resolver_sha256:
raise InstalledForgeUpdateError("retained legacy entrypoint changed")
fence = (
"#!/bin/sh\n"
f"echo 'Forge installation maintenance is active: {self.request.operation_id}' >&2\n"
"exit 75\n"
# The fence is shared by every update operation. Its bytes therefore
# must be operation-independent. Releases before 2.7.24 embedded the
# first operation id and made every later update fail closed while
# trying to reuse the same managed launcher. Accept only that exact
# legacy shape and atomically normalize it; arbitrary launcher changes
# remain a hard failure.
fence = b"#!/bin/sh\necho 'Forge installation maintenance is active' >&2\nexit 75\n"
legacy_fence = re.compile(
rb"\A#!/bin/sh\necho 'Forge installation maintenance is active: "
rb"[A-Za-z0-9][A-Za-z0-9._-]{0,127}' >&2\nexit 75\n\Z"
)
if not self.fenced_resolver.exists():
descriptor = os.open(self.fenced_resolver, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o755)
with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
handle.write(fence)
elif self.fenced_resolver.read_text(encoding="utf-8") != fence:
raise InstalledForgeUpdateError("maintenance fence launcher changed")
_atomic_regular_file(self.fenced_resolver, fence, mode=0o755)
else:
observed_fence = _read_regular_bytes(self.fenced_resolver)
if observed_fence != fence:
if legacy_fence.fullmatch(observed_fence) is None:
raise InstalledForgeUpdateError("maintenance fence launcher changed")
_atomic_regular_file(self.fenced_resolver, fence, mode=0o755)
if not self.current.exists() and not self.current.is_symlink():
_replace_symlink(self.current, os.path.relpath(self.legacy_entrypoint, self.runtime_root))
if not self.stable_resolver.exists() and not self.stable_resolver.is_symlink():
Expand Down
41 changes: 38 additions & 3 deletions tests/test_installed_forge_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,8 +230,8 @@ def test_exact_release_complete_artifact_is_accepted_and_mismatch_rejected(self)
with self.assertRaisesRegex(update.InstalledForgeUpdateError, "wheel digest"):
update.validate_qualified_artifact(self.request)

def test_normal_2723_release_receipt_is_accepted_only_for_the_new_transition(self) -> None:
version = "2.7.23"
def test_normal_release_receipt_is_accepted_only_for_supported_normal_transitions(self) -> None:
version = "2.7.24"
wheel = self.root / f"forge_autonomy-{version}-py3-none-any.whl"
dist_info = f"forge_autonomy-{version}.dist-info"
members = {
Expand Down Expand Up @@ -290,7 +290,7 @@ def test_normal_2723_release_receipt_is_accepted_only_for_the_new_transition(sel
}, sort_keys=True), encoding="utf-8")
request = update.UpdateRequest(**{
**self.request.__dict__,
"operation_id": "forge-update-2723-test-002",
"operation_id": "forge-update-2724-test-002",
"version": version,
"product_source": source,
"wheel": str(wheel),
Expand All @@ -303,6 +303,10 @@ def test_normal_2723_release_receipt_is_accepted_only_for_the_new_transition(sel
evidence = update.validate_qualified_artifact(request)

self.assertEqual(evidence["release_route"], "NORMAL")
from_2723 = update.UpdateRequest(**{
**request.__dict__, "existing_version": "2.7.23",
})
self.assertEqual(update.validate_qualified_artifact(from_2723)["release_route"], "NORMAL")
wrong_transition = update.UpdateRequest(**{
**request.__dict__, "existing_version": "2.7.21",
})
Expand Down Expand Up @@ -446,6 +450,37 @@ def test_resolver_adoption_fences_without_editing_the_legacy_environment(self) -
self.assertEqual(self.resolver.resolve(), controller.fenced_resolver.resolve())
self.assertEqual(controller.legacy_entrypoint.read_bytes(), legacy_bytes)

def test_resolver_adoption_normalizes_a_prior_operation_fence(self) -> None:
controller = self._controller()
controller.fenced_resolver.parent.mkdir(parents=True)
controller.fenced_resolver.write_text(
"#!/bin/sh\n"
"echo 'Forge installation maintenance is active: forge-update-older-001' >&2\n"
"exit 75\n",
encoding="utf-8",
)
controller.fenced_resolver.chmod(0o755)

with patch.object(update, "installed_identity", return_value={"version": "2.7.21"}):
controller._adopt_resolver(controller._state())

self.assertEqual(
controller.fenced_resolver.read_bytes(),
b"#!/bin/sh\necho 'Forge installation maintenance is active' >&2\nexit 75\n",
)
self.assertEqual(controller.fenced_resolver.stat().st_mode & 0o777, 0o755)

def test_resolver_adoption_rejects_an_unrecognized_existing_fence(self) -> None:
controller = self._controller()
controller.fenced_resolver.parent.mkdir(parents=True)
controller.fenced_resolver.write_text("#!/bin/sh\necho compromised\n", encoding="utf-8")

with (
patch.object(update, "installed_identity", return_value={"version": "2.7.21"}),
self.assertRaisesRegex(update.InstalledForgeUpdateError, "fence launcher changed"),
):
controller._adopt_resolver(controller._state())

def test_crash_before_migration_restores_the_legacy_route(self) -> None:
controller = self._controller()
state = controller._state()
Expand Down
Loading