Skip to content
1 change: 1 addition & 0 deletions .apm/architecture/owners/install-deployment.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"owner": "deployment_ledger.py (DeploymentLedgerCodec and installation-bound DeploymentMaterializationSession); receipt claim keys route through install/deployed_paths.py (deployed_path_entry) and retain the admitted key through postcommit binding; authenticated same-writer target handoffs require a guarded actual copy, never adopted-receipt rebinding; live install and uninstall survivor sessions persist actual final-rendered skill writes, with final-lock-success binding; cleanup retires confirmed outcomes once per phase before survivor copies; read-only index: skill_support.build_skill_ownership_maps, exposed by SkillIntegrator._build_ownership_maps with integrate-phase snapshot lifetime",
"selectors": [
"src/apm_cli/core/deployment_ledger.py",
"src/apm_cli/utils/filesystem_identity.py",
"src/apm_cli/integration/skill_support.py",
"src/apm_cli/install/pipeline.py"
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1277,8 +1277,25 @@ def _materialization_evidence_findings(provider: FactsProvider, rule_id: str) ->
requirements = {
_DEPLOYMENT_LEDGER: (
"class DeploymentMaterializationSession:",
'data["installation"] != installation',
'data["root_identity"] != self._root_identity()',
'data["installation"] != self._installation',
"not self._matches_root_identity(data)",
'return data["root_identity"] == [metadata.st_dev, metadata.st_ino]',
'return data["root_identity"] == self._root_identity()',
'"deployment-receipts-v2"',
'"volume": darwin_volume_uuid(self.root)',
"self.baseline(path, [target]) != record.content_hash",
"compute_file_hash(path) != record.content_hash",
"if snapshot[key] == self._legacy_snapshot.get(key):",
"for key in self._legacy_snapshot.keys() - snapshot.keys():",
"self._retire_legacy_deletions(snapshot)",
"legacy_changed = self._retire_legacy_members(removed)",
"committed._refresh_legacy_writes(witnessed)",
"for current_key in self._records.keys() - snapshot.keys():",
'data["stable_root_identity"] != self._root_identity()',
"committed._publish_legacy_compatibility()",
'data["legacy_snapshot"] = self._snapshot(self._legacy_projection(data))',
"self.baseline(Path(self._destinations[key]), [target]) != record.content_hash",
"new_file_mode=0o600,\n create_only=True,",
"record.active_owner not in self._valid_owners",
"self._bindings.get(key) != [record.active_owner, self._claim_signature(value)]",
"value = deployed_path_entry(path, self.root, targets)",
Expand Down Expand Up @@ -1330,7 +1347,7 @@ def _materialization_evidence_findings(provider: FactsProvider, rule_id: str) ->
),
"src/apm_cli/integration/skill_support.py": ("removed_paths.append(skill_subdir)",),
"src/apm_cli/commands/uninstall/engine.py": (
"DeploymentMaterializationSession(project_root, _rebuild_scope, lockfile)",
"DeploymentMaterializationSession(project_root, _rebuild_scope, lockfile, targets=_targets)",
'with _integrators["skills"].ownership_snapshot(materialization_session=materialization_session):',
"materialization_session=materialization_session,",
"removed_paths=removed_skill_paths",
Expand Down
2 changes: 1 addition & 1 deletion src/apm_cli/commands/uninstall/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -1388,7 +1388,7 @@ def _sync_integrations_after_uninstall(
_allow_executables = getattr(apm_package, "allow_executables", None)
reintegration_diagnostics = DiagnosticCollector()
materialization_session = (
DeploymentMaterializationSession(project_root, _rebuild_scope, lockfile)
DeploymentMaterializationSession(project_root, _rebuild_scope, lockfile, targets=_targets)
if target_survivor_plan
else None
)
Expand Down
465 changes: 416 additions & 49 deletions src/apm_cli/core/deployment_ledger.py

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion src/apm_cli/install/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,9 @@ def _run_integration_phase(ctx: InstallContext) -> DeploymentMaterializationSess
session = (
None
if ctx.lockfile_only or ctx.dry_run
else DeploymentMaterializationSession(ctx.project_root, ctx.scope, ctx.existing_lockfile)
else DeploymentMaterializationSession(
ctx.project_root, ctx.scope, ctx.existing_lockfile, targets=ctx.targets
)
)
with (
contextlib.nullcontext()
Expand Down
47 changes: 47 additions & 0 deletions src/apm_cli/utils/filesystem_identity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""Native persistent volume identity, without invoking an external command."""

from __future__ import annotations

import ctypes
import os
import uuid
from pathlib import Path


class _AttrList(ctypes.Structure):
_fields_ = [
("bitmapcount", ctypes.c_uint16),
("reserved", ctypes.c_uint16),
("commonattr", ctypes.c_uint32),
("volattr", ctypes.c_uint32),
("dirattr", ctypes.c_uint32),
("fileattr", ctypes.c_uint32),
("forkattr", ctypes.c_uint32),
]


def darwin_volume_uuid(path: Path) -> str:
"""Read ATTR_VOL_UUID; fail closed if a stable identity is unavailable."""
libc = ctypes.CDLL("/usr/lib/libSystem.B.dylib", use_errno=True)
getattrlist = libc.getattrlist
getattrlist.argtypes = [
ctypes.c_char_p,
ctypes.POINTER(_AttrList),
ctypes.c_void_p,
ctypes.c_size_t,
ctypes.c_ulong,
]
getattrlist.restype = ctypes.c_int
# ATTR_VOL_INFO | ATTR_VOL_UUID; the packed result is a 32-bit length
# followed by the 16-byte UUID. getattrlist accepts a path within a volume.
attrs = _AttrList(5, 0, 0, 0x80040000, 0, 0, 0)
result = ctypes.create_string_buffer(20)
if getattrlist(os.fsencode(path), ctypes.byref(attrs), result, len(result), 0):
error = ctypes.get_errno()
raise OSError(error, os.strerror(error), str(path))
if int.from_bytes(result.raw[:4], "little") != 20:
raise ValueError("Invalid persistent volume identity response")
volume = uuid.UUID(bytes=result.raw[4:20])
if volume.int == 0:
raise ValueError("Persistent volume identity unavailable")
return str(volume)
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,26 @@ def test_receipt_guard_accepts_current_owner_routing() -> None:
@pytest.mark.parametrize(
("path", "before", "after"),
[
(
"core/deployment_ledger.py",
"committed._refresh_legacy_writes(witnessed)",
"pass",
),
(
"core/deployment_ledger.py",
"for current_key in self._records.keys() - snapshot.keys():",
"for current_key in ():",
),
(
"core/deployment_ledger.py",
"legacy_changed = self._retire_legacy_members(removed)",
"legacy_changed = False",
),
(
"core/deployment_ledger.py",
"for key in self._legacy_snapshot.keys() - snapshot.keys():",
"for key in ():",
),
(
"commands/uninstall/cli.py",
"else:\n if integration_cleanup.materialization_session is not None:",
Expand Down Expand Up @@ -115,9 +135,74 @@ def test_receipt_guard_accepts_current_owner_routing() -> None:
),
(
"core/deployment_ledger.py",
'data["root_identity"] != self._root_identity()',
"not self._matches_root_identity(data)",
"False",
),
(
"core/deployment_ledger.py",
'data["installation"] != self._installation',
"False",
),
(
"core/deployment_ledger.py",
'return data["root_identity"] == [metadata.st_dev, metadata.st_ino]',
"return True",
),
(
"core/deployment_ledger.py",
'return data["root_identity"] == self._root_identity()',
"return True",
),
(
"core/deployment_ledger.py",
'"deployment-receipts-v2"',
'"deployment-receipts"',
),
(
"core/deployment_ledger.py",
'"volume": darwin_volume_uuid(self.root)',
'"volume": str(metadata.st_dev)',
),
(
"core/deployment_ledger.py",
"self.baseline(path, [target]) != record.content_hash",
"False",
),
(
"core/deployment_ledger.py",
"compute_file_hash(path) != record.content_hash",
"False",
),
(
"core/deployment_ledger.py",
"if snapshot[key] == self._legacy_snapshot.get(key):",
"if False:",
),
(
"core/deployment_ledger.py",
'data["stable_root_identity"] != self._root_identity()',
"False",
),
(
"core/deployment_ledger.py",
"committed._publish_legacy_compatibility()",
"pass",
),
(
"core/deployment_ledger.py",
'data["legacy_snapshot"] = self._snapshot(self._legacy_projection(data))',
'data["legacy_snapshot"] = {}',
),
(
"core/deployment_ledger.py",
"self.baseline(Path(self._destinations[key]), [target]) != record.content_hash",
"False",
),
(
"core/deployment_ledger.py",
"new_file_mode=0o600,\n create_only=True,",
"new_file_mode=0o600,\n create_only=False,",
),
(
"core/deployment_ledger.py",
"record.active_owner not in self._valid_owners",
Expand Down
18 changes: 12 additions & 6 deletions tests/integration/test_generated_lifecycle_state_machine.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,15 +252,17 @@ def exact_paths(self) -> dict[str, frozenset[str]]:
if not self.global_scope:
home = self.scenario.isolated.home
with patch("apm_cli.core.deployment_ledger.Path.home", return_value=home):
receipt = DeploymentMaterializationSession(
session = DeploymentMaterializationSession(
self.project.root, InstallScope.PROJECT, None
).state_path
)
receipts = (session.state_path, session.legacy_state_path)
return {
**_INSTALL_EXACT_PATHS,
"user": _USER_WRITE_PATHS
| {
receipt.parent.relative_to(home).as_posix(),
receipt.relative_to(home).as_posix(),
path.relative_to(home).as_posix()
for receipt in receipts
for path in (receipt.parent, receipt)
},
}
return {
Expand Down Expand Up @@ -919,6 +921,7 @@ def test_generated_lifecycle_mandatory_replay(
[
*itertools.product(("identity-sibling", "wrong-root"), _VARIANTS),
("receipt-sibling", "project"),
("legacy-receipt-sibling", "project"),
],
)
def test_generated_identity_write_scope_is_exact(
Expand All @@ -945,8 +948,11 @@ def test_generated_identity_write_scope_is_exact(
before = model._capture()
if extra_write == "identity-sibling":
unexpected = identity.parent / "unrelated.txt"
elif extra_write == "receipt-sibling":
unexpected = home / ".apm/deployment-receipts/unrelated.json"
elif extra_write in ("receipt-sibling", "legacy-receipt-sibling"):
namespace = (
"deployment-receipts-v2" if extra_write == "receipt-sibling" else "deployment-receipts"
)
unexpected = home / ".apm" / namespace / "unrelated.json"
else:
wrong_root = project if model.fixture.global_scope else home
unexpected = wrong_root / ".apm-local/installation"
Expand Down
Loading