From 69c87fec67eac40c2b29921f608c3fe12aeb8b1e Mon Sep 17 00:00:00 2001 From: Ivan D Vasin Date: Fri, 23 Oct 2026 00:36:53 -0400 Subject: [PATCH] fix(versionkeeping): retain guarded HTTPS publication on current main --- .../SKILL.md | 18 +++-- .../references/publication-execution.md | 36 ++++++---- .../scripts/git_publication/adapter.py | 6 +- .../scripts/git_publication/execution.py | 14 ++++ .../plugin-content-locks/versionkeeping.json | 8 +-- release/provingkit/definition-v1.json | 2 +- .../test_git_publication_adapter.py | 37 ++++++++++ .../test_git_publication_execution.py | 71 +++++++++++++++++++ 8 files changed, 163 insertions(+), 29 deletions(-) diff --git a/plugins/versionkeeping/skills/checkpointing-and-publishing-git-work/SKILL.md b/plugins/versionkeeping/skills/checkpointing-and-publishing-git-work/SKILL.md index 537d51dd..3990577d 100644 --- a/plugins/versionkeeping/skills/checkpointing-and-publishing-git-work/SKILL.md +++ b/plugins/versionkeeping/skills/checkpointing-and-publishing-git-work/SKILL.md @@ -109,13 +109,17 @@ Before planning/executing, read for script routes, effects, trusted handoff, transport, push, and verification. Run and review the planner first; then bind publication authority as above to the reviewed `ready` plan's push before invoking the executor. -The adapter uses the host-compatible Git configuration profile by default and -supports an explicit `VERSIONKEEPING_GIT_CONFIG_PROFILE=hardened` profile for -closed-environment publication. Both profiles reject command-valued local/worktree -settings (including signing programs) and unsafe includes without exposing their -values. The hardened profile additionally disables implicit commit/tag/push signing -and closes configuration across all scopes; it permits only HTTPS, SSH, or -ancestry-guarded local endpoints. +For the guarded authenticated HTTPS contract—no ambient executable Git +configuration, one trusted provider, and a fail-closed provider boundary—set +`VERSIONKEEPING_GIT_CONFIG_PROFILE=hardened` before planning and keep it selected +through execution. The profile is reviewed plan identity; selecting another profile +requires a fresh plan and review. The adapter otherwise uses `host-compatible` by +default. That compatibility profile may inherit effective host configuration and +providers, so it is outside the guarded contract. Both profiles reject command-valued +local/worktree settings (including signing programs) and unsafe includes without +exposing their values. The hardened profile additionally disables implicit +commit/tag/push signing and closes configuration across all scopes; it permits only +HTTPS, SSH, or ancestry-guarded local endpoints. Follow its typed gates exactly. Ordinary publication never deletes a remote ref; the separate terminal route requires verified merge and explicit diff --git a/plugins/versionkeeping/skills/checkpointing-and-publishing-git-work/references/publication-execution.md b/plugins/versionkeeping/skills/checkpointing-and-publishing-git-work/references/publication-execution.md index 75f3796a..e7eb7252 100644 --- a/plugins/versionkeeping/skills/checkpointing-and-publishing-git-work/references/publication-execution.md +++ b/plugins/versionkeeping/skills/checkpointing-and-publishing-git-work/references/publication-execution.md @@ -85,14 +85,18 @@ remote name is discovery metadata, not a publication actuator. Ordinary publication uses the `host-compatible` Git configuration profile by default. It inherits the effective host Git configuration and credential helpers visible to the harness, while retaining endpoint, ref, lease, prompt, -and post-push verification gates. Set -`VERSIONKEEPING_GIT_CONFIG_PROFILE=hardened` for the explicit closed profile. -That profile clears global and system configuration and permits only the -validated platform provider described below. The selected profile is included -in the reviewed destination configuration digest; changing it requires a fresh -plan and review. - -The ordinary publication executor supports noninteractive HTTPS credentials on +and post-push verification gates. This weaker, harness-trusting profile is +outside the guarded authenticated HTTPS contract; that distinction is a route +boundary, not an assertion that it provides hardened isolation. When the +publication needs no ambient executable Git configuration, one trusted +provider, and a fail-closed provider boundary, set +`VERSIONKEEPING_GIT_CONFIG_PROFILE=hardened` before planning and keep it +selected through execution. That profile clears global and system +configuration and permits only the validated platform provider described +below. The selected profile is included in the reviewed destination +configuration digest; changing it requires a fresh plan and review. + +The hardened publication profile supports noninteractive HTTPS credentials on modern macOS, Linux, and Windows through closed platform-provider sets: - macOS uses `git-credential-osxkeychain` from the trusted system Git exec @@ -140,17 +144,21 @@ variables are also removed. This prevents a narrower Git configuration or process environment from weakening TLS beneath the authenticated endpoint. Rejected setting values are never read into diagnostics. -Credential bytes travel only between Git and the system helper through Git's -credential protocol: they are never read by the planner or executor and never -enter arguments, diagnostics, plans, receipts, or repository configuration. -SSH and ancestry-guarded local endpoints do not enable a credential helper and -retain their existing contracts. +Credential-provider response bytes remain between Git and the system helper. +The executor sends credential-preflight stdout and stderr to the null device +and decides availability only from the return code; it never receives or +inspects provider response bytes. Credential bytes never enter arguments, +diagnostics, plans, receipts, or repository configuration. SSH and +ancestry-guarded local endpoints do not enable a credential helper and retain +their existing contracts. ## Terminal Remote-Ref Deletion Remote-ref deletion is a separate planner and executor surface owned by this skill. It is not an ordinary publication update and must not be supplied to -`execute_git_publication.py`. Before planning, the deletion request requires a +`execute_git_publication.py`. It is outside this guarded authenticated HTTPS +claim and must establish any credential/profile claim under its own contract. +Before planning, the deletion request requires a verified merge outcome plus explicit repository and operator authorization. Each authorization repeats and exactly binds the remote, full `refs/heads/...` ref, and `expected_target_sha`; the verified merged source SHA must equal that diff --git a/plugins/versionkeeping/skills/checkpointing-and-publishing-git-work/scripts/git_publication/adapter.py b/plugins/versionkeeping/skills/checkpointing-and-publishing-git-work/scripts/git_publication/adapter.py index d0e13f45..3995c698 100644 --- a/plugins/versionkeeping/skills/checkpointing-and-publishing-git-work/scripts/git_publication/adapter.py +++ b/plugins/versionkeeping/skills/checkpointing-and-publishing-git-work/scripts/git_publication/adapter.py @@ -1224,8 +1224,8 @@ def ensure_https_credentials(self, endpoint: str) -> None: env=self.env, input=query, text=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, timeout=self.timeout_seconds, check=False, ) @@ -1234,7 +1234,7 @@ def ensure_https_credentials(self, endpoint: str) -> None: "HTTPS_CREDENTIALS_UNAVAILABLE", profile=self.config_profile, ) from error - if result.returncode != 0 or not result.stdout.strip(): + if result.returncode != 0: raise PolicyGate( "HTTPS_CREDENTIALS_UNAVAILABLE", profile=self.config_profile, diff --git a/plugins/versionkeeping/skills/checkpointing-and-publishing-git-work/scripts/git_publication/execution.py b/plugins/versionkeeping/skills/checkpointing-and-publishing-git-work/scripts/git_publication/execution.py index f49dc15d..873be0f9 100644 --- a/plugins/versionkeeping/skills/checkpointing-and-publishing-git-work/scripts/git_publication/execution.py +++ b/plugins/versionkeeping/skills/checkpointing-and-publishing-git-work/scripts/git_publication/execution.py @@ -305,6 +305,20 @@ def execute_repository( expected_fingerprint=destination["endpoint_fingerprint"], observed_fingerprint=endpoint_fingerprint, ) + if repo.config_profile != destination["config_profile"]: + early_selection = dict(selection) + early_selection["default_branch_ref"] = destination[ + "default_branch_ref" + ] + early_selection["config_profile"] = repo.config_profile + raise PolicyGate( + "REVIEWED_CONFIG_CHANGED", + expected_digest=destination["config_digest"], + observed_digest=_config_digest( + early_selection, + endpoint_fingerprint, + ), + ) repo.enable_https_credentials(endpoint) default_branch_ref = _probe_default_branch(repo, endpoint, object_format) if default_branch_ref != destination["default_branch_ref"]: diff --git a/release/plugin-content-locks/versionkeeping.json b/release/plugin-content-locks/versionkeeping.json index f6b52060..b25e2530 100644 --- a/release/plugin-content-locks/versionkeeping.json +++ b/release/plugin-content-locks/versionkeeping.json @@ -19,7 +19,7 @@ "mode": 420 }, "skills/checkpointing-and-publishing-git-work/SKILL.md": { - "sha256": "12c8e6cff200774dce7d371448e96e4d46109c5b1989378af16f32383494670f", + "sha256": "01ca4151554adad2f8ff4d734f9877c66cce0d5f3f97b2844b454053dd6da846", "mode": 420 }, "skills/checkpointing-and-publishing-git-work/agents/openai.yaml": { @@ -35,7 +35,7 @@ "mode": 420 }, "skills/checkpointing-and-publishing-git-work/references/publication-execution.md": { - "sha256": "3eac694a5de44fff942a6ee5d11defd867670abf6c2230fbf2839172f02b8f47", + "sha256": "e2dedd152502037a961e3204ea30cef916d3080dbab0e0b84cdae6995e72e6ae", "mode": 420 }, "skills/checkpointing-and-publishing-git-work/references/terminal-cleanup.md": { @@ -59,7 +59,7 @@ "mode": 420 }, "skills/checkpointing-and-publishing-git-work/scripts/git_publication/adapter.py": { - "sha256": "9b6f56af71a59cdcacb8ede86fb00960c3c54fd6ffb76ae66a5bb6cff24d1640", + "sha256": "14f233a5b166d61f71e02dca9e97a71bfeff2120591ad03daadbbbe2db1f7ed2", "mode": 420 }, "skills/checkpointing-and-publishing-git-work/scripts/git_publication/core.py": { @@ -71,7 +71,7 @@ "mode": 420 }, "skills/checkpointing-and-publishing-git-work/scripts/git_publication/execution.py": { - "sha256": "2fcd07d058460a4bf1dc110b569128dfa22f21b59e6c6e8c2c81a0ca77dced99", + "sha256": "e448b37ffa9252d257aeb8204978154cf1e4404e8c3c0c4b1a1f7c3deed5bdfa", "mode": 420 }, "skills/checkpointing-and-publishing-git-work/scripts/plan_git_publication.py": { diff --git a/release/provingkit/definition-v1.json b/release/provingkit/definition-v1.json index 1ea4c913..2d5526f4 100644 --- a/release/provingkit/definition-v1.json +++ b/release/provingkit/definition-v1.json @@ -52,7 +52,7 @@ "content_identity": { "kind": "plugin-content-lock", "path": "release/plugin-content-locks/versionkeeping.json", - "sha256": "sha256:7cb688670ebc3a2b1ef7cc43c0fac7772903916c1fae17997692492113127143" + "sha256": "sha256:8e226e2ce5098868c76b72eeaa994710d620da2a2c80966bd69352941897b0f9" }, "display_name": "Versionkeeping", "distribution_kind": "agent-plugin", diff --git a/tests/plugins/versionkeeping/checkpointing-and-publishing-git-work/test_git_publication_adapter.py b/tests/plugins/versionkeeping/checkpointing-and-publishing-git-work/test_git_publication_adapter.py index e887fe7e..a21bc0b4 100644 --- a/tests/plugins/versionkeeping/checkpointing-and-publishing-git-work/test_git_publication_adapter.py +++ b/tests/plugins/versionkeeping/checkpointing-and-publishing-git-work/test_git_publication_adapter.py @@ -266,6 +266,43 @@ def test_https_credential_preflight_includes_repository_path(self): "protocol=https\nhost=github.com\npath=nisavid/provingkit.git\n\n", ) + def test_https_credential_preflight_uses_only_return_code(self): + repository = object.__new__(adapter.GitRepository) + repository.git_executable = "git" + repository.path = Path("/controlled/repository") + repository.env = {} + repository.timeout_seconds = 1 + repository.config_profile = "hardened" + repository.enable_https_credentials = mock.Mock() + completed = ( + subprocess.CompletedProcess( + ["git", "credential", "fill"], 0, "", "" + ), + subprocess.CompletedProcess( + ["git", "credential", "fill"], 1, None, None + ), + ) + with mock.patch.object( + adapter.subprocess, "run", side_effect=completed + ) as run: + try: + repository.ensure_https_credentials( + "https://github.com/nisavid/provingkit.git" + ) + except adapter.PolicyGate as gate: + self.fail(f"zero return code was rejected: {gate.code}") + + with self.assertRaises(adapter.PolicyGate) as raised: + repository.ensure_https_credentials( + "https://github.com/nisavid/provingkit.git" + ) + + self.assertEqual(raised.exception.code, "HTTPS_CREDENTIALS_UNAVAILABLE") + for call in run.call_args_list: + self.assertIs(call.kwargs["check"], False) + self.assertIs(call.kwargs["stdout"], subprocess.DEVNULL) + self.assertIs(call.kwargs["stderr"], subprocess.DEVNULL) + def test_cached_https_credentials_recheck_host_configuration(self): repository = object.__new__(adapter.GitRepository) repository.path = Path("/controlled/repository") diff --git a/tests/plugins/versionkeeping/checkpointing-and-publishing-git-work/test_git_publication_execution.py b/tests/plugins/versionkeeping/checkpointing-and-publishing-git-work/test_git_publication_execution.py index 35f17c32..04e4be57 100644 --- a/tests/plugins/versionkeeping/checkpointing-and-publishing-git-work/test_git_publication_execution.py +++ b/tests/plugins/versionkeeping/checkpointing-and-publishing-git-work/test_git_publication_execution.py @@ -1936,6 +1936,77 @@ def test_config_change_after_review_blocks_before_push(self) -> None: self.assertFalse(result["push_attempted"]) self.assertEqual(result["reasons"][0]["code"], "REVIEWED_CONFIG_CHANGED") + def test_reviewed_profile_change_blocks_before_external_boundaries(self) -> None: + events = [] + endpoint = "https://example.invalid/repository.git" + destination = self.plan["destination"] + host_home = self.root / "host-home" + host_config_home = self.root / "host-config" + host_home.mkdir() + host_config_home.mkdir() + + def record_credential_activation(_repository, _endpoint): + events.append("credential_activation") + + def record_default_branch_probe(*_args, **_kwargs): + events.append("endpoint_probe") + return destination["default_branch_ref"] + + def record_target_probe(*_args, **_kwargs): + events.append("endpoint_probe") + return self.start + + def record_push(*_args, **_kwargs): + events.append("push") + return subprocess.CompletedProcess(["git", "push"], 0, "", "") + + with mock.patch.dict( + os.environ, + { + adapter.GIT_CONFIG_PROFILE_ENV: "host-compatible", + "HOME": str(host_home), + "XDG_CONFIG_HOME": str(host_config_home), + "PATH": "/usr/bin:/bin", + }, + clear=False, + ): + with mock.patch.object( + execution, + "_endpoint", + return_value=(endpoint, destination["endpoint_fingerprint"]), + ): + with mock.patch.object( + execution.GitRepository, + "enable_https_credentials", + autospec=True, + side_effect=record_credential_activation, + ): + with mock.patch.object( + execution, + "_probe_default_branch", + side_effect=record_default_branch_probe, + ): + with mock.patch.object( + execution, + "_probe_ref", + side_effect=record_target_probe, + ): + with mock.patch.object( + execution, + "_run_endpoint", + side_effect=record_push, + ): + result = execute(self.repo, self.plan) + + self.assertEqual(result["status"], "blocked") + self.assertFalse(result["push_attempted"]) + self.assertEqual(result["reasons"][0]["code"], "REVIEWED_CONFIG_CHANGED") + self.assertEqual(events, []) + self.assertEqual( + git(self.remote, "rev-parse", "refs/heads/topic"), + self.start, + ) + def test_target_lease_change_after_review_blocks_before_push(self) -> None: remote_only = commit(self.repo, "remote-only") git(self.repo, "push", "publish", f"{remote_only}:refs/heads/topic")