Import publish-grant runs locally; leave the push to Activity Core (ACTIVITY-WP-0046-T02)
Some checks failed
Governed runtime contract / contract (push) Failing after 11s

The artifact transfer now accepts a repository grant v2. It still imports
exactly one commit, refuses any changed path outside
publish_scope.path_prefixes, never pushes, and records published: false
for Activity Core's publisher (hub decision 1c9817ee).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 269692@bnt-lap001
Assistant-Session: 8077e68a-0985-4b41-898c-2dc602b0d6be
This commit is contained in:
tegwick 2026-10-13 21:43:00 +02:00
parent 4695649cc6
commit 5c36d80756
2 changed files with 60 additions and 2 deletions

View file

@ -119,10 +119,14 @@ class RepositoryArtifactTransfer:
raise RepositoryArtifactError(
"artifact transfer requires the active repository transaction"
)
if grant.min_commits != 1 or grant.max_commits != 1 or grant.publish:
if grant.min_commits != 1 or grant.max_commits != 1:
raise RepositoryArtifactError(
"artifact transfer v1 requires exactly one local commit"
)
# A publish grant (v2) only marks the imported commit for Activity
# Core's publisher; this transfer never pushes.
if grant.publish and grant.publish_scope is None:
raise RepositoryArtifactError("publish grant has no publish_scope")
self.transaction = transaction
self.grant = grant
self.cancel = resolve_cancel(cancel)
@ -284,7 +288,16 @@ class RepositoryArtifactTransfer:
_git(
staging, "merge", "--ff-only", "--no-edit", "--no-stat", self._head
)
inspection.validate_acceptance(self.grant.acceptance_policy())
inspected = inspection.validate_acceptance(
self.grant.acceptance_policy()
)
scope = self.grant.publish_scope
if scope is not None and not all(
scope.covers_path(path) for path in inspected.changed_paths
):
raise RepositoryArtifactError(
"artifact changes paths outside the publish scope"
)
self._check()
_git(
baseline.repo_root,
@ -316,5 +329,7 @@ class RepositoryArtifactTransfer:
"head": accepted.head,
"imported": True,
}
if self.grant.publish:
evidence["published"] = False
self._bundle = None
return evidence

View file

@ -14,6 +14,7 @@ from rein_aharness.execution_cancel import ExecutionCancel, ExecutionCancelled
from rein_aharness.repository_artifact import (
RepositoryArtifactError,
RepositoryArtifactTransfer,
_git as _git_spy,
)
from rein_aharness.repository_grant import RepositoryGrant
from rein_aharness.repository_transaction import (
@ -310,3 +311,45 @@ def test_ignored_sandbox_cache_is_not_imported(tmp_path):
transfer.import_after_teardown(head)
assert not (source / "cache").exists()
assert tx.acceptance.changed_paths == ("result.txt",)
def _publish_grant():
return RepositoryGrant.from_mapping(
{
"version": "2",
"allowed_paths": ["out/"],
"commit_count": {"min": 1, "max": 1},
"publish": True,
"publish_scope": {
"remote": "origin",
"branch": "main",
"path_prefixes": ["out/pub/"],
"fast_forward_only": True,
},
}
)
def test_publish_grant_imports_locally_and_leaves_push_to_publisher(tmp_path):
source, sandbox, _ = fixture(tmp_path)
with RepositoryTransaction(source, state_dir=tmp_path / "state") as tx:
transfer = RepositoryArtifactTransfer(tx, _publish_grant())
head = commit(sandbox, "out/pub/brief.md")
capture(transfer, sandbox, head)
with patch("rein_aharness.repository_artifact._git", wraps=_git_spy) as spy:
result = transfer.import_after_teardown(head)
assert not any("push" in call.args for call in spy.call_args_list)
assert result["imported"] and result["published"] is False
assert git(source, "rev-parse", "HEAD") == head
def test_publish_grant_refuses_paths_outside_publish_scope(tmp_path):
source, sandbox, _ = fixture(tmp_path)
with RepositoryTransaction(source, state_dir=tmp_path / "state") as tx:
baseline = tx.baseline.head
transfer = RepositoryArtifactTransfer(tx, _publish_grant())
head = commit(sandbox, "out/private.md")
capture(transfer, sandbox, head)
with pytest.raises(RepositoryArtifactError, match="publish scope"):
transfer.import_after_teardown(head)
assert git(source, "rev-parse", "HEAD") == baseline