feat(manifest): git-gate.signing opt-in (PRD chunk 2)
test / integration-docker (pull_request) Successful in 38s
lint / lint (push) Successful in 56s
test / unit (pull_request) Successful in 52s
test / integration-firecracker (pull_request) Successful in 3m48s
test / coverage (pull_request) Successful in 15s
test / publish-infra (pull_request) Has been skipped
tracker-policy-pr / check-pr (pull_request) Failing after 24s

Implements the manifest surface for the signed-commits & audit-attribution
PRD (#423, PR #480): a bottle opts into per-activation commit signing with

    git-gate:
      signing:
        enabled: true

- New ManifestGitSigning value type (frozen, enabled: bool = False) parsed
  under git-gate.signing; unknown sub-keys and non-bool `enabled` die with
  targeted errors. parse_git_gate_config now returns (repos, user, signing).
- ManifestBottle gains git_signing; defaults to off, so bottles that omit
  the block are unchanged.
- Bottle-only: git-gate.signing on an agent is rejected by the existing
  agent-level non-`user` guard (like git-gate.repos).
- extends/runtime merges thread git_signing with an enable-wins overlay
  (a child enabling signing wins; omitting inherits the parent), mirroring
  the git_user non-empty-wins overlay.
- Facade exports ManifestGitSigning.

Tests: tests/unit/test_manifest_git_signing.py (parse, validation,
agent-level rejection, extends overlay). Full unit suite green.

This is the first stacked chunk; signing pipeline, gate signature check,
and control-plane verification + audit tables follow per the PRD.

Issue: #423

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-26 02:52:47 +00:00
parent 010e253d66
commit e16efc86ad
5 changed files with 217 additions and 10 deletions
+46 -4
View File
@@ -283,16 +283,52 @@ class ManifestGitUser:
return not self.name and not self.email
@dataclass(frozen=True)
class ManifestGitSigning:
"""Per-bottle commit-signing switch (PRD prd-new: signed commits &
audit attribution).
When `enabled`, the launcher mints a per-activation Ed25519 signing
key host-side, holds the private half in the sidecar ssh-agent, and
configures the bottle to sign every commit at commit time; the gate
rejects any commit it forwards that is not signed by the activation
key, and the control plane records an audit binding.
Bottle-only: it carries data-plane / audit policy, not an
agent-overlayable identity, so — like `git-gate.repos` — it is
rejected at the agent level. Defaults to off: bottles that omit
`git-gate.signing` behave exactly as before."""
enabled: bool = False
@classmethod
def from_dict(cls, bottle_name: str, raw: object) -> "ManifestGitSigning":
d = as_json_object(raw, f"bottle '{bottle_name}' git-gate.signing")
for k in d:
if k != "enabled":
raise ManifestError(
f"bottle '{bottle_name}' git-gate.signing has unknown key "
f"{k!r}; allowed: enabled"
)
enabled = d.get("enabled", False)
if not isinstance(enabled, bool):
raise ManifestError(
f"bottle '{bottle_name}' git-gate.signing.enabled must be a "
f"boolean (was {type(enabled).__name__})"
)
return cls(enabled=enabled)
def parse_git_gate_config(
bottle_name: str,
raw: object,
) -> tuple[tuple[ManifestGitEntry, ...], ManifestGitUser]:
) -> tuple[tuple[ManifestGitEntry, ...], ManifestGitUser, ManifestGitSigning]:
d = as_json_object(raw, f"bottle '{bottle_name}' git-gate")
for k in d:
if k not in {"user", "repos"}:
if k not in {"user", "repos", "signing"}:
raise ManifestError(
f"bottle '{bottle_name}' git-gate has unknown key {k!r}; "
f"allowed: user, repos"
f"allowed: user, repos, signing"
)
git_user = (
@@ -301,6 +337,12 @@ def parse_git_gate_config(
else ManifestGitUser()
)
git_signing = (
ManifestGitSigning.from_dict(bottle_name, d["signing"])
if "signing" in d
else ManifestGitSigning()
)
git: tuple[ManifestGitEntry, ...] = ()
repos_raw = d.get("repos")
if repos_raw is not None:
@@ -311,4 +353,4 @@ def parse_git_gate_config(
)
validate_unique_git_names(bottle_name, git)
return git, git_user
return git, git_user, git_signing