Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions README_de.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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` +
Expand Down
2 changes: 1 addition & 1 deletion src/githubbot_core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
a plugin SPI and signed exact-SHA review receipts. Standard library only.
"""

__version__ = "0.2.0"
__version__ = "0.2.1"
15 changes: 12 additions & 3 deletions src/githubbot_core/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']}"
Expand Down
32 changes: 30 additions & 2 deletions tests/test_system_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import json
import re
from importlib import resources
from types import SimpleNamespace

from githubbot_core import __version__, cli

Expand All @@ -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)
Expand All @@ -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}


Expand All @@ -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
Loading