diff --git a/CHANGELOG.md b/CHANGELOG.md index fa0fa29..4ec775c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,15 @@ Format: [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), [Semantic Vers ## [Unreleased] +## [0.2.1] - 2026-10-11 + +### Geändert / Changed + +- `system-agent`: `sha256` bezieht sich auf schema+id+blueprint und ist für Konsumenten nachrechenbar / + sha256 scope: schema+id+blueprint, verifiable by consumers (canonical JSON, sorted keys, compact + separators, UTF-8, `ensure_ascii=False`; excludes version, capability, permissions, `_meta`). + The hash value of the unchanged v0.2.0 blueprint changes from the old whole-file hash. + ## [0.2.0] - 2026-10-11 ### Hinzugefügt / Added diff --git a/README.md b/README.md index 04ce54e..bb72d60 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ pull-request summaries, read-only health/traffic/star reports, a preview/apply c write, a plugin SPI and signed exact-SHA review receipts. Pure Python standard library, no runtime dependencies. -> Status: alpha (0.2.0). The CLI, the JSON result envelope and SPI version 1 are the contract; +> Status: alpha (0.2.1). The CLI, the JSON result envelope and SPI version 1 are the contract; > everything else may still change. ## Features @@ -89,8 +89,8 @@ The Python API is the package itself: `githubbot_core.triage.IssueTriage`, ### System agent template `githubbot-core --json system-agent` prints the neutral agent template shipped with the package -(schema `githubbot-core.system-agent/v1`): `version` (package version), `sha256` of the canonical -template, `capability`, the effective `permissions` and a `blueprint` with the field names of the +(schema `githubbot-core.system-agent/v1`): `version` (package version), `sha256` over the canonical JSON of +`{schema, id, blueprint}` (sorted keys, compact separators, UTF-8; verifiable from the printed output), `capability`, the effective `permissions` and a `blueprint` with the field names of the agent-studio blueprint payload (persona, role, contractus, governance, skills). The template exists only where the package is installed. Consumers bind an agent to `version` + `sha256` and must not overwrite an existing agent configuration when a newer template appears. diff --git a/README_de.md b/README_de.md index 2edb27f..8af0115 100644 --- a/README_de.md +++ b/README_de.md @@ -7,7 +7,7 @@ Duplikaterkennung, Pull-Request-Zusammenfassungen, lesende Berichte zu Gesundhei Sternen, ein Vorschau-/Ausführungsvertrag für jede Schreibaktion, eine Plugin-Schnittstelle (SPI) und signierte Exact-SHA-Review-Belege. Reine Python-Standardbibliothek, keine Laufzeitabhängigkeiten. -> Status: Alpha (0.2.0). Verbindlich sind CLI, JSON-Ergebnishülle und SPI-Version 1; alles andere +> Status: Alpha (0.2.1). Verbindlich sind CLI, JSON-Ergebnishülle und SPI-Version 1; alles andere > kann sich noch ändern. ## Funktionen @@ -90,7 +90,7 @@ Die Python-API ist das Paket selbst: `githubbot_core.triage.IssueTriage`, ### Systemagent-Vorlage `githubbot-core --json system-agent` gibt die neutrale, mit dem Paket ausgelieferte Agentenvorlage aus -(Schema `githubbot-core.system-agent/v1`): `version` (Paketversion), `sha256` der kanonischen Vorlage, +(Schema `githubbot-core.system-agent/v1`): `version` (Paketversion), `sha256` über das kanonische JSON von `{schema, id, blueprint}` (nachrechenbar aus der Ausgabe), `capability`, die wirksamen `permissions` und ein `blueprint` mit den Feldnamen des Agent-Studio-Blueprint-Payloads (Persona, Rolle, Contractus, Governance, Skills). Die Vorlage existiert nur dort, wo das Paket installiert ist. Konsumenten binden einen Agenten an `version` + diff --git a/src/githubbot_core/__init__.py b/src/githubbot_core/__init__.py index 923bd0c..a08acfd 100644 --- a/src/githubbot_core/__init__.py +++ b/src/githubbot_core/__init__.py @@ -5,4 +5,4 @@ a plugin SPI and signed exact-SHA review receipts. Standard library only. """ -__version__ = "0.2.0" +__version__ = "0.2.1" diff --git a/src/githubbot_core/cli.py b/src/githubbot_core/cli.py index 9413de2..699f2f0 100644 --- a/src/githubbot_core/cli.py +++ b/src/githubbot_core/cli.py @@ -115,11 +115,20 @@ def cmd_plugins(args, config, transport) -> int: return EXIT_OK +def template_sha256(schema: str, template_id: str, blueprint: dict) -> str: + """sha256 over canonical JSON of exactly {schema, id, blueprint}: sort_keys, separators (",", ":"), + ensure_ascii=False, UTF-8. Excludes version, capability, permissions and _meta, so a consumer can + recompute it from the printed `data` alone.""" + material = {"schema": schema, "id": template_id, "blueprint": blueprint} + canonical = json.dumps(material, sort_keys=True, separators=(",", ":"), ensure_ascii=False) + return hashlib.sha256(canonical.encode("utf-8")).hexdigest() + + def cmd_system_agent(args, config, transport) -> int: template = json.loads(resources.files("githubbot_core").joinpath("system_agent.json").read_text(encoding="utf-8")) - canonical = json.dumps(template, sort_keys=True, separators=(",", ":"), ensure_ascii=False) - data = {"schema": template["_meta"]["schema"], "id": template["id"], "version": __version__, - "sha256": hashlib.sha256(canonical.encode("utf-8")).hexdigest(), + schema = template["_meta"]["schema"] + data = {"schema": schema, "id": template["id"], "version": __version__, + "sha256": template_sha256(schema, template["id"], template["blueprint"]), "capability": {"package": "githubbot-core", "version": __version__, "spi_version": plugins.SPI_VERSION}, "blueprint": template["blueprint"], "permissions": config.get("permissions", {})} text = f"system agent {data['id']} {data['version']} sha256 {data['sha256']}" diff --git a/tests/test_system_agent.py b/tests/test_system_agent.py index 835006f..5a94c6f 100644 --- a/tests/test_system_agent.py +++ b/tests/test_system_agent.py @@ -5,6 +5,7 @@ import json import re from importlib import resources +from types import SimpleNamespace from githubbot_core import __version__, cli @@ -15,6 +16,12 @@ EXECUTABLE_MODUS = {"casualis", "usus"} +def _recompute(d: dict) -> str: + material = {"schema": d["schema"], "id": d["id"], "blueprint": d["blueprint"]} + return hashlib.sha256(json.dumps(material, sort_keys=True, separators=(",", ":"), + ensure_ascii=False).encode("utf-8")).hexdigest() + + def _system_agent(capsys, *extra: str) -> dict: assert cli.main([*extra, "--json", "system-agent"]) == 0 envelope = json.loads(capsys.readouterr().out) @@ -25,10 +32,12 @@ def _system_agent(capsys, *extra: str) -> dict: def test_template_identity_is_version_plus_canonical_hash(capsys): data = _system_agent(capsys) template = json.loads(resources.files("githubbot_core").joinpath("system_agent.json").read_text(encoding="utf-8")) - canonical = json.dumps(template, sort_keys=True, separators=(",", ":"), ensure_ascii=False) assert data["schema"] == "githubbot-core.system-agent/v1" assert data["id"] == "githubbot" and data["version"] == __version__ - assert data["sha256"] == hashlib.sha256(canonical.encode("utf-8")).hexdigest() + assert data["sha256"] == _recompute(data) # consumer view: printed output alone suffices + assert data["sha256"] == _recompute({"schema": template["_meta"]["schema"], "id": template["id"], + "blueprint": template["blueprint"]}) + assert data["sha256"].startswith("2e72c000") # pinned: v0.2.0 blueprint content, BACH contract assert data["capability"] == {"package": "githubbot-core", "version": __version__, "spi_version": 1} @@ -48,3 +57,22 @@ def test_permissions_are_the_effective_narrowed_configuration(capsys, tmp_path): layer.write_text(json.dumps({"permissions": {"enabled": []}}), encoding="utf-8") assert _system_agent(capsys)["permissions"]["enabled"] == ["issue.add_labels", "issue.comment"] assert _system_agent(capsys, "--config", str(layer))["permissions"]["enabled"] == [] + + +def test_hash_scope_is_schema_id_blueprint_only(): + h = cli.template_sha256("s", "i", {"a": 1}) + assert h == _recompute({"schema": "s", "id": "i", "blueprint": {"a": 1}}) + assert cli.template_sha256("s2", "i", {"a": 1}) != h + assert cli.template_sha256("s", "i2", {"a": 1}) != h + assert cli.template_sha256("s", "i", {"a": 2}) != h + + +def test_hash_ignores_version_and_meta(capsys, monkeypatch, tmp_path): + before = _system_agent(capsys)["sha256"] + template = json.loads(resources.files("githubbot_core").joinpath("system_agent.json").read_text(encoding="utf-8")) + template["_meta"]["description"] += " (changed)" + (tmp_path / "system_agent.json").write_text(json.dumps(template), encoding="utf-8") + monkeypatch.setattr(cli, "resources", SimpleNamespace(files=lambda _package: tmp_path)) + monkeypatch.setattr(cli, "__version__", "9.9.9") + data = _system_agent(capsys) + assert data["version"] == "9.9.9" and data["sha256"] == before