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
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:
@@ -0,0 +1,133 @@
|
||||
"""Unit: git-gate.signing manifest parsing + validation.
|
||||
|
||||
PRD prd-new (signed commits & audit attribution): a bottle opts into
|
||||
per-activation commit signing with `git-gate.signing.enabled: true`.
|
||||
The block is bottle-only (rejected at the agent level, like
|
||||
git-gate.repos) and defaults to off.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
|
||||
from bot_bottle.manifest import ManifestError, ManifestGitSigning, ManifestIndex
|
||||
|
||||
|
||||
def _bottle(git_gate: dict) -> dict: # type: ignore
|
||||
return {
|
||||
"bottles": {"dev": {"git-gate": git_gate}},
|
||||
"agents": {"demo": {"skills": [], "prompt": "", "bottle": "dev"}},
|
||||
}
|
||||
|
||||
|
||||
class TestSigningParsing(unittest.TestCase):
|
||||
def test_default_is_disabled(self):
|
||||
"""A bottle with no git-gate block signs nothing."""
|
||||
m = ManifestIndex.from_json_obj({
|
||||
"bottles": {"dev": {}},
|
||||
"agents": {"demo": {"skills": [], "prompt": "", "bottle": "dev"}},
|
||||
})
|
||||
self.assertEqual(ManifestGitSigning(), m.bottles["dev"].git_signing)
|
||||
self.assertFalse(m.bottles["dev"].git_signing.enabled)
|
||||
|
||||
def test_git_gate_without_signing_is_disabled(self):
|
||||
"""git-gate present (e.g. user only) but no signing → off."""
|
||||
m = ManifestIndex.from_json_obj(_bottle({
|
||||
"user": {"name": "claude", "email": "eric+claude@dideric.is"},
|
||||
}))
|
||||
self.assertFalse(m.bottles["dev"].git_signing.enabled)
|
||||
|
||||
def test_enabled_true(self):
|
||||
m = ManifestIndex.from_json_obj(_bottle({"signing": {"enabled": True}}))
|
||||
self.assertTrue(m.bottles["dev"].git_signing.enabled)
|
||||
|
||||
def test_enabled_false(self):
|
||||
m = ManifestIndex.from_json_obj(_bottle({"signing": {"enabled": False}}))
|
||||
self.assertFalse(m.bottles["dev"].git_signing.enabled)
|
||||
|
||||
def test_signing_coexists_with_user_and_repos(self):
|
||||
m = ManifestIndex.from_json_obj(_bottle({
|
||||
"user": {"name": "claude", "email": "eric+claude@dideric.is"},
|
||||
"signing": {"enabled": True},
|
||||
"repos": {
|
||||
"bot-bottle": {
|
||||
"url": "ssh://git@gitea.dideric.is:30009/didericis/bot-bottle.git",
|
||||
"key": {"provider": "static", "path": "/dev/null"},
|
||||
},
|
||||
},
|
||||
}))
|
||||
b = m.bottles["dev"]
|
||||
self.assertTrue(b.git_signing.enabled)
|
||||
self.assertEqual("claude", b.git_user.name)
|
||||
self.assertEqual(1, len(b.git))
|
||||
|
||||
|
||||
class TestSigningValidation(unittest.TestCase):
|
||||
def test_unknown_key_under_signing_dies(self):
|
||||
with self.assertRaises(ManifestError) as cm:
|
||||
ManifestIndex.from_json_obj(_bottle({
|
||||
"signing": {"enabled": True, "enforce": ["author"]},
|
||||
}))
|
||||
msg = str(cm.exception)
|
||||
self.assertIn("git-gate.signing", msg)
|
||||
self.assertIn("enforce", msg)
|
||||
|
||||
def test_non_bool_enabled_dies(self):
|
||||
with self.assertRaises(ManifestError) as cm:
|
||||
ManifestIndex.from_json_obj(_bottle({"signing": {"enabled": "yes"}}))
|
||||
self.assertIn("git-gate.signing.enabled must be a boolean", str(cm.exception))
|
||||
|
||||
def test_signing_not_a_mapping_dies(self):
|
||||
with self.assertRaises(ManifestError):
|
||||
ManifestIndex.from_json_obj(_bottle({"signing": ["enabled"]}))
|
||||
|
||||
def test_unknown_git_gate_key_lists_signing(self):
|
||||
"""The git-gate allowed-key error names signing as valid."""
|
||||
with self.assertRaises(ManifestError) as cm:
|
||||
ManifestIndex.from_json_obj(_bottle({"bogus": {}}))
|
||||
self.assertIn("allowed: user, repos, signing", str(cm.exception))
|
||||
|
||||
|
||||
class TestSigningIsBottleOnly(unittest.TestCase):
|
||||
def test_agent_level_signing_rejected(self):
|
||||
"""git-gate.signing on an agent dies — it is bottle-only."""
|
||||
with self.assertRaises(ManifestError) as cm:
|
||||
ManifestIndex.from_json_obj({
|
||||
"bottles": {"dev": {}},
|
||||
"agents": {
|
||||
"demo": {
|
||||
"skills": [],
|
||||
"prompt": "",
|
||||
"bottle": "dev",
|
||||
"git-gate": {"signing": {"enabled": True}},
|
||||
},
|
||||
},
|
||||
})
|
||||
msg = str(cm.exception)
|
||||
self.assertIn("git-gate.signing", msg)
|
||||
self.assertIn("not allowed at the agent level", msg)
|
||||
|
||||
|
||||
class TestSigningExtendsOverlay(unittest.TestCase):
|
||||
def test_child_inherits_parent_signing(self):
|
||||
"""A child that omits signing inherits the parent's enabled flag."""
|
||||
m = ManifestIndex.from_json_obj({
|
||||
"bottles": {
|
||||
"base": {"git-gate": {"signing": {"enabled": True}}},
|
||||
"dev": {"extends": "base"},
|
||||
},
|
||||
"agents": {"demo": {"skills": [], "prompt": "", "bottle": "dev"}},
|
||||
})
|
||||
self.assertTrue(m.bottles["dev"].git_signing.enabled)
|
||||
|
||||
def test_child_enables_over_disabled_parent(self):
|
||||
m = ManifestIndex.from_json_obj({
|
||||
"bottles": {
|
||||
"base": {},
|
||||
"dev": {"extends": "base", "git-gate": {"signing": {"enabled": True}}},
|
||||
},
|
||||
"agents": {"demo": {"skills": [], "prompt": "", "bottle": "dev"}},
|
||||
})
|
||||
self.assertTrue(m.bottles["dev"].git_signing.enabled)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user