csoai 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
csoai-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,30 @@
1
+ Metadata-Version: 2.4
2
+ Name: csoai
3
+ Version: 0.1.0
4
+ Summary: Council of AI — signed, deterministic AI-governance measurement: CLI + MCP server + agent-callable verifier
5
+ Author: Council of AI (CSOAI LTD, UK 16939677)
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://councilof.ai
8
+ Keywords: ai-governance,eu-ai-act,measurement,signed-evidence,mcp,article-50
9
+ Requires-Python: >=3.9
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: cryptography>=41
12
+ Provides-Extra: mcp
13
+ Requires-Dist: fastmcp>=2.0; extra == "mcp"
14
+
15
+ # csoai
16
+
17
+ Council of AI — signed, deterministic AI-governance measurement.
18
+
19
+ The boring CLI is the primary agent rail; the MCP server wraps it; both emit the
20
+ same signed atom. Public artifacts only (hiQ/Van Buren footing); measurement, not
21
+ certification.
22
+
23
+ ```bash
24
+ pip install csoai
25
+ csoai check --entity gpt2 --pack art50 --json # measure a public artifact -> signed card
26
+ csoai verify --record card.json # verify a signed record offline
27
+ pip install "csoai[mcp]" && python -m csoai.mcp_server # agent-callable MCP tools
28
+ ```
29
+
30
+ CI gate: `csoai check` exits 3 on a missing transparency predicate — fail the build on it.
csoai-0.1.0/README.md ADDED
@@ -0,0 +1,16 @@
1
+ # csoai
2
+
3
+ Council of AI — signed, deterministic AI-governance measurement.
4
+
5
+ The boring CLI is the primary agent rail; the MCP server wraps it; both emit the
6
+ same signed atom. Public artifacts only (hiQ/Van Buren footing); measurement, not
7
+ certification.
8
+
9
+ ```bash
10
+ pip install csoai
11
+ csoai check --entity gpt2 --pack art50 --json # measure a public artifact -> signed card
12
+ csoai verify --record card.json # verify a signed record offline
13
+ pip install "csoai[mcp]" && python -m csoai.mcp_server # agent-callable MCP tools
14
+ ```
15
+
16
+ CI gate: `csoai check` exits 3 on a missing transparency predicate — fail the build on it.
@@ -0,0 +1,26 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "csoai"
7
+ version = "0.1.0"
8
+ description = "Council of AI — signed, deterministic AI-governance measurement: CLI + MCP server + agent-callable verifier"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = {text = "Apache-2.0"}
12
+ authors = [{name = "Council of AI (CSOAI LTD, UK 16939677)"}]
13
+ keywords = ["ai-governance", "eu-ai-act", "measurement", "signed-evidence", "mcp", "article-50"]
14
+ dependencies = ["cryptography>=41"]
15
+
16
+ [project.optional-dependencies]
17
+ mcp = ["fastmcp>=2.0"]
18
+
19
+ [project.urls]
20
+ Homepage = "https://councilof.ai"
21
+
22
+ [project.scripts]
23
+ csoai = "csoai.cli:main"
24
+
25
+ [tool.setuptools.packages.find]
26
+ where = ["src"]
csoai-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,6 @@
1
+ """csoai — Council of AI signed measurement toolkit.
2
+
3
+ The boring CLI is the primary agent rail; the MCP server wraps it; both emit the
4
+ same signed atom. Public-artifacts-only, measurement-not-certification.
5
+ """
6
+ __version__ = "0.1.0"
@@ -0,0 +1,88 @@
1
+ """csoai — the boring CLI. Universal agent rail + CI forcing-function + atom-emitter
2
+ + the first agent-callable signed-record verifier.
3
+
4
+ csoai check --entity <hf-repo> --pack art50 [--sign] [--json]
5
+ csoai verify --record <file.json>
6
+
7
+ Deterministic output + exit codes so an agent (or a CI gate) acts on the result
8
+ without reasoning about the statute from memory. Guardrails: PUBLIC artifacts only
9
+ (hiQ/Van Buren); measurement, not certification; signed only with a real key.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import json
15
+ import sys
16
+ from pathlib import Path
17
+
18
+ from csoai import council_signal
19
+
20
+ PACKS = {
21
+ "art50": {
22
+ "title": "EU AI Act Article 50 transparency (public-artifact signals)",
23
+ "required": ["license_declared", "task_declared", "model_card_present"],
24
+ },
25
+ "transparency": {
26
+ "title": "Baseline transparency",
27
+ "required": ["license_declared", "model_card_present"],
28
+ },
29
+ }
30
+
31
+
32
+ def cmd_check(a) -> int:
33
+ pack = PACKS.get(a.pack)
34
+ if not pack:
35
+ print(f"unknown pack '{a.pack}'; known: {', '.join(PACKS)}", file=sys.stderr)
36
+ return 64
37
+ try:
38
+ rec = council_signal.state_record(a.entity)
39
+ except Exception as e: # noqa: BLE001
40
+ print(f"FETCH FAILED for {a.entity}: {e}. No card emitted (never fabricated).", file=sys.stderr)
41
+ return 2
42
+ rec["pack"] = a.pack
43
+ missing = [k for k in pack["required"] if not rec["predicates"].get(k)]
44
+ rec["pack_result"] = {"pack": a.pack, "required": pack["required"], "missing": missing, "pass": not missing}
45
+ if a.sign:
46
+ out = a.out or f"csoai_check_{a.entity.replace('/', '_')}.json"
47
+ rec = council_signal.sign_record(rec, out)
48
+ if a.json:
49
+ print(json.dumps(rec, indent=2))
50
+ else:
51
+ print(f" {a.entity} pack={a.pack} pass={rec['pack_result']['pass']}"
52
+ f" missing={missing or 'none'} signed={rec.get('signed', bool(a.sign))}")
53
+ return 0 if not missing else 3
54
+
55
+
56
+ def cmd_verify(a) -> int:
57
+ from csoai import sign
58
+ obj = json.loads(Path(a.record).read_text())
59
+ s = obj.get("signature")
60
+ if not s or s.get("kind") != "ed25519":
61
+ print("UNSIGNED — no Ed25519 signature on this record.")
62
+ return 1
63
+ try:
64
+ sign.verify(a.record)
65
+ return 0
66
+ except SystemExit as e:
67
+ return int(e.code) if isinstance(e.code, int) else 1
68
+
69
+
70
+ def main(argv=None) -> int:
71
+ ap = argparse.ArgumentParser(prog="csoai", description=__doc__)
72
+ sub = ap.add_subparsers(dest="cmd", required=True)
73
+ c = sub.add_parser("check", help="measure a public artifact → signed card")
74
+ c.add_argument("--entity", required=True)
75
+ c.add_argument("--pack", default="art50", help=f"one of: {', '.join(PACKS)}")
76
+ c.add_argument("--sign", action="store_true")
77
+ c.add_argument("--json", action="store_true")
78
+ c.add_argument("--out")
79
+ c.set_defaults(fn=cmd_check)
80
+ v = sub.add_parser("verify", help="verify a signed record offline")
81
+ v.add_argument("--record", required=True)
82
+ v.set_defaults(fn=cmd_verify)
83
+ a = ap.parse_args(argv)
84
+ return a.fn(a)
85
+
86
+
87
+ if __name__ == "__main__":
88
+ raise SystemExit(main())
@@ -0,0 +1,247 @@
1
+ """council_signal.py — continuous, LAWFUL compliance signal over PUBLISHED AI artifacts.
2
+
3
+ The play (build-map, compass 237202a7 #A): scan an entity's PUBLIC artifacts, emit a
4
+ compact Ed25519-signed state record, and notify on drift. This is the first brick:
5
+ one public artifact source (a Hugging Face model repo's PUBLIC metadata) → a
6
+ deterministic transparency state → a signed record → a drift diff against the prior.
7
+
8
+ LEGAL GUARDRAILS, baked in (not optional):
9
+ * PUBLIC ARTIFACTS ONLY. This reads the public model-info endpoint (no auth, no
10
+ gated content, no private API). Footing: hiQ v. LinkedIn (9th Cir. 2022) +
11
+ Van Buren (SCOTUS 2021) — scanning public pages is not CFAA "without
12
+ authorization." (Still respect ToS/copyright; never scan a private API.)
13
+ * MEASUREMENT, NOT CERTIFICATION. Every field is an observed public fact or a
14
+ deterministic predicate over one. We do not certify compliance or conformity.
15
+ * SIGN ONLY WITH A REAL KEY. Signed on the signing node; UNSIGNED and labelled so
16
+ everywhere else. Never a fake signature.
17
+
18
+ Checks are deterministic functions of the fetched public metadata — recomputable by
19
+ anyone from the same public source. Nothing here is a judgement call.
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import argparse
24
+ import json
25
+ import hashlib
26
+ import sys
27
+ import urllib.request
28
+ import urllib.error
29
+ import urllib.parse
30
+ from datetime import datetime, timezone
31
+ from pathlib import Path
32
+ from typing import Any, Dict, Optional
33
+
34
+ VERSION = "0.1.0"
35
+ HF_MODEL_API = "https://huggingface.co/api/models/{entity}"
36
+ # Art-50 / transparency signal terms we look for in public tags/cardData.
37
+ _MARKING_TERMS = ("c2pa", "content-credential", "content credentials", "watermark",
38
+ "synthid", "ai-generated", "provenance")
39
+
40
+
41
+ def fetch_public_metadata(entity: str, timeout: int = 20) -> Dict[str, Any]:
42
+ """GET the PUBLIC model-info JSON. Raises on network/HTTP error (never fakes data)."""
43
+ url = HF_MODEL_API.format(entity=urllib.parse.quote(entity, safe="/"))
44
+ req = urllib.request.Request(url, headers={"User-Agent": f"council-signal/{VERSION}"})
45
+ with urllib.request.urlopen(req, timeout=timeout) as r:
46
+ return json.loads(r.read().decode("utf-8"))
47
+
48
+
49
+ def compute_checks(meta: Dict[str, Any]) -> Dict[str, Any]:
50
+ """Deterministic transparency predicates over the PUBLIC metadata."""
51
+ card = meta.get("cardData") or {}
52
+ tags = [str(t).lower() for t in (meta.get("tags") or [])]
53
+ blob = (json.dumps(card) + " " + " ".join(tags)).lower()
54
+ return {
55
+ # transparency basics an Art-50/AI-Act reader looks for
56
+ "license_declared": bool(card.get("license") or any(t.startswith("license:") for t in tags)),
57
+ "task_declared": bool(meta.get("pipeline_tag")),
58
+ "model_card_present": bool(card) and card != {},
59
+ "gated": bool(meta.get("gated")), # a transparency/access signal (not good/bad by itself)
60
+ # generative-marking signal (Art 50) — does the card/tags mention any marking scheme
61
+ "generative_marking_declared": any(term in blob for term in _MARKING_TERMS),
62
+ # observed facts (not predicates) kept for the record
63
+ "license_value": card.get("license"),
64
+ "pipeline_tag": meta.get("pipeline_tag"),
65
+ "downloads": meta.get("downloads"),
66
+ "lastModified": meta.get("lastModified"),
67
+ }
68
+
69
+
70
+ def state_record(entity: str) -> Dict[str, Any]:
71
+ meta = fetch_public_metadata(entity)
72
+ checks = compute_checks(meta)
73
+ # the state hash is over the PREDICATES only (facts like downloads churn constantly
74
+ # and are not compliance drift) — so drift means a transparency signal changed.
75
+ predicate_keys = ("license_declared", "task_declared", "model_card_present",
76
+ "gated", "generative_marking_declared")
77
+ predicates = {k: checks[k] for k in predicate_keys}
78
+ state_hash = hashlib.sha256(json.dumps(predicates, sort_keys=True).encode()).hexdigest()
79
+ return {
80
+ "kind": "council_signal.state",
81
+ "version": VERSION,
82
+ "entity": entity,
83
+ "source": "huggingface public model-info (no auth)",
84
+ "fetched_at": datetime.now(timezone.utc).isoformat(),
85
+ "predicates": predicates,
86
+ "observed": {k: checks[k] for k in checks if k not in predicate_keys},
87
+ "state_hash": state_hash,
88
+ "frame": ("Deterministic transparency predicates over PUBLIC artifacts only. "
89
+ "Measurement, not certification. Drift = a transparency predicate changed."),
90
+ }
91
+
92
+
93
+ def diff(prev: Dict[str, Any], curr: Dict[str, Any]) -> Dict[str, Any]:
94
+ """Drift = which transparency predicates changed since the prior signed state."""
95
+ pp, cp = prev.get("predicates", {}), curr.get("predicates", {})
96
+ changed = {k: {"was": pp.get(k), "now": cp.get(k)} for k in cp if pp.get(k) != cp.get(k)}
97
+ return {
98
+ "entity": curr["entity"],
99
+ "drifted": bool(changed) or prev.get("state_hash") != curr["state_hash"],
100
+ "changes": changed,
101
+ "prev_fetched_at": prev.get("fetched_at"),
102
+ "curr_fetched_at": curr["fetched_at"],
103
+ }
104
+
105
+
106
+ def scan_list(entities: list[str]) -> Dict[str, Any]:
107
+ """Scan several public entities in one pass → one batch report.
108
+
109
+ Each entity gets its own deterministic state; a fetch that fails is recorded as
110
+ an error entry (never fabricated), so a broken source is visible, not silent.
111
+ The batch hash is over the per-entity (entity, state_hash) pairs, so the batch
112
+ itself drifts iff any member's transparency state changed.
113
+ """
114
+ states, errors = [], []
115
+ for e in entities:
116
+ try:
117
+ states.append(state_record(e))
118
+ except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError, json.JSONDecodeError) as exc:
119
+ errors.append({"entity": e, "error": str(exc)})
120
+ members = sorted((s["entity"], s["state_hash"]) for s in states)
121
+ batch_hash = hashlib.sha256(json.dumps(members).encode()).hexdigest()
122
+ return {
123
+ "kind": "council_signal.batch",
124
+ "version": VERSION,
125
+ "scanned_at": datetime.now(timezone.utc).isoformat(),
126
+ "n_requested": len(entities),
127
+ "n_scanned": len(states),
128
+ "n_errors": len(errors),
129
+ "batch_hash": batch_hash,
130
+ "states": states,
131
+ "errors": errors,
132
+ "frame": ("Batch of deterministic transparency states over PUBLIC artifacts. "
133
+ "Fetch failures are recorded, not hidden. Measurement, not certification."),
134
+ }
135
+
136
+
137
+ def batch_diff(prev: Dict[str, Any], curr: Dict[str, Any]) -> Dict[str, Any]:
138
+ """Drift-report: which entities changed transparency state since the prior batch."""
139
+ pstate = {s["entity"]: s for s in prev.get("states", [])}
140
+ cstate = {s["entity"]: s for s in curr.get("states", [])}
141
+ drifted, added, removed = [], [], []
142
+ for e, cs in cstate.items():
143
+ if e not in pstate:
144
+ added.append(e)
145
+ elif pstate[e]["state_hash"] != cs["state_hash"]:
146
+ drifted.append({"entity": e, "changes": diff(pstate[e], cs)["changes"]})
147
+ removed = [e for e in pstate if e not in cstate]
148
+ return {
149
+ "kind": "council_signal.drift_report",
150
+ "prev_scanned_at": prev.get("scanned_at"),
151
+ "curr_scanned_at": curr.get("scanned_at"),
152
+ "batch_drifted": prev.get("batch_hash") != curr.get("batch_hash"),
153
+ "drifted": drifted, # entities whose transparency predicates changed
154
+ "added": added, # newly-scanned entities
155
+ "removed": removed, # entities no longer in the batch
156
+ "notify": bool(drifted or added or removed), # the hook the notify channel fires on
157
+ }
158
+
159
+
160
+ def sign_record(record: Dict[str, Any], out_path: str | Path) -> Dict[str, Any]:
161
+ """Ed25519-sign iff the key exists on this node; else emit UNSIGNED, labelled."""
162
+ Path(out_path).write_text(json.dumps(record, indent=2), encoding="utf-8")
163
+ try:
164
+ import os
165
+ try:
166
+ import sign # repo-root context
167
+ except ModuleNotFoundError:
168
+ from csoai import sign # installed-package context
169
+ if os.path.exists(sign.PRIV):
170
+ sign.sign(str(out_path))
171
+ return json.loads(Path(out_path).read_text())
172
+ except Exception as e:
173
+ record["_sign_error"] = str(e)
174
+ record["signature"] = None
175
+ record["signed"] = False
176
+ record["_unsigned_note"] = "no signing key on this node — sign on the signing node: python3 sign.py --sign " + str(out_path)
177
+ Path(out_path).write_text(json.dumps(record, indent=2), encoding="utf-8")
178
+ return record
179
+
180
+
181
+ def selftest() -> int:
182
+ entity = "bert-base-uncased" # a stable, public, non-gated repo
183
+ try:
184
+ rec = state_record(entity)
185
+ except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError) as e:
186
+ print(f" selftest SKIPPED — no network to the public endpoint ({e})")
187
+ return 0 # honest: cannot test offline, do not fake a pass
188
+ print(f" scanned {entity}: predicates = {rec['predicates']}")
189
+ print(f" state_hash = {rec['state_hash'][:16]}…")
190
+ # drift self-check: same scan → no drift; mutated predicate → drift
191
+ d0 = diff(rec, rec)
192
+ mutated = json.loads(json.dumps(rec)); mutated["predicates"]["license_declared"] = not mutated["predicates"]["license_declared"]
193
+ mutated["state_hash"] = "x"
194
+ d1 = diff(rec, mutated)
195
+ ok = (d0["drifted"] is False) and (d1["drifted"] is True) and ("license_declared" in d1["changes"])
196
+ print(f" drift(no-change)={d0['drifted']} drift(mutated)={d1['drifted']} changes={list(d1['changes'])}")
197
+ print(" selftest:", "PASS" if ok else "FAIL")
198
+ return 0 if ok else 1
199
+
200
+
201
+ def main() -> int:
202
+ ap = argparse.ArgumentParser(description=__doc__)
203
+ ap.add_argument("--scan", metavar="ENTITY", help="public HF model repo id, e.g. meta-llama/Llama-3.1-8B-Instruct")
204
+ ap.add_argument("--scan-list", metavar="FILE", help="file of public entity ids (one per line) to scan as a batch")
205
+ ap.add_argument("--against", metavar="PRIOR.json", help="prior signed state to diff for drift")
206
+ ap.add_argument("--against-batch", metavar="PRIOR_BATCH.json", help="prior batch to diff for a drift-report")
207
+ ap.add_argument("--out", default="benchmark-results/council_signal_state.json")
208
+ ap.add_argument("--selftest", action="store_true")
209
+ a = ap.parse_args()
210
+ if a.selftest:
211
+ return selftest()
212
+ if a.scan_list:
213
+ entities = [l.strip() for l in Path(a.scan_list).read_text().splitlines() if l.strip() and not l.startswith("#")]
214
+ batch = sign_record(scan_list(entities), a.out)
215
+ print(f" batch: {batch['n_scanned']}/{batch['n_requested']} scanned, "
216
+ f"{batch['n_errors']} errors, hash {batch['batch_hash'][:16]}…, signed={batch.get('signed')}")
217
+ for s in batch["states"]:
218
+ # `gated` is a neutral observed attribute (False = not gated = normal), not a
219
+ # deficiency — exclude it. Flag only absent transparency signals.
220
+ flags = [k for k, v in s["predicates"].items() if not v and k != "gated"]
221
+ print(f" {s['entity']:40} missing: {flags or 'none'}")
222
+ for e in batch["errors"]:
223
+ print(f" {e['entity']:40} FETCH ERROR: {e['error']}")
224
+ if a.against_batch and Path(a.against_batch).exists():
225
+ rep = batch_diff(json.loads(Path(a.against_batch).read_text()), batch)
226
+ print("\n--- DRIFT REPORT ---")
227
+ print(f" batch_drifted={rep['batch_drifted']} · notify={rep['notify']}")
228
+ print(f" drifted={[d['entity'] for d in rep['drifted']]} added={rep['added']} removed={rep['removed']}")
229
+ return 0
230
+ if a.scan:
231
+ try:
232
+ rec = sign_record(state_record(a.scan), a.out)
233
+ except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError, json.JSONDecodeError) as e:
234
+ print(f"FETCH FAILED for {a.scan}: {e}. Public artifact unreadable — no state emitted "
235
+ "(never fabricated).", file=sys.stderr)
236
+ return 2
237
+ print(json.dumps(rec, indent=2))
238
+ if a.against and Path(a.against).exists():
239
+ print("\n--- DRIFT ---")
240
+ print(json.dumps(diff(json.loads(Path(a.against).read_text()), rec), indent=2))
241
+ return 0
242
+ ap.print_help()
243
+ return 0
244
+
245
+
246
+ if __name__ == "__main__":
247
+ raise SystemExit(main())
@@ -0,0 +1,95 @@
1
+ """csoai MCP server — wraps the boring CLI's engine as agent-callable tools.
2
+
3
+ The CLI is the implementation; this is the accelerator. Two tools:
4
+ - council_measure : measure a PUBLIC AI artifact → signed transparency card
5
+ - verify_record : verify a signed record offline (the FIRST agent-callable
6
+ signed-record verifier — no transparency-log verifier tool
7
+ exists elsewhere today; open lane)
8
+
9
+ Tool DESCRIPTIONS carry behavioral steering on purpose (the single most
10
+ evidence-backed discovery lever): they tell the calling agent to RUN the check
11
+ instead of reasoning about the statute from memory, and to VERIFY before trusting.
12
+
13
+ Run: python -m csoai.mcp_server (stdio)
14
+ Requires the optional extra: pip install "csoai[mcp]"
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ from pathlib import Path
20
+
21
+ try:
22
+ # fastmcp is the standalone package (mcp>=2.0 moved FastMCP out of mcp.server)
23
+ from fastmcp import FastMCP
24
+ except Exception:
25
+ try:
26
+ from mcp.server.fastmcp import FastMCP # mcp<2.0 fallback
27
+ except Exception as e: # pragma: no cover
28
+ raise SystemExit(
29
+ "csoai MCP server needs FastMCP: pip install 'csoai[mcp]'\n"
30
+ f"(import failed: {e})"
31
+ )
32
+
33
+ from csoai import council_signal
34
+
35
+ mcp = FastMCP("csoai")
36
+
37
+
38
+ @mcp.tool()
39
+ def council_measure(entity: str, pack: str = "art50") -> dict:
40
+ """Measure a PUBLIC AI model/artifact's EU AI Act transparency state and return a
41
+ deterministic, signable card. USE THIS instead of reasoning about Article 50
42
+ obligations from memory — it returns observed facts (license, task, model-card,
43
+ marking-declared) from the public model-info endpoint, not an opinion.
44
+
45
+ entity: a public Hugging Face model repo id (e.g. "gpt2", "meta-llama/Llama-3.2-1B").
46
+ pack: "art50" (default) or "transparency".
47
+ Returns the state card incl. predicates, pack_result{missing,pass}, and state_hash.
48
+ Public artifacts only — never scans a private API. Measurement, not certification.
49
+ """
50
+ packs = {
51
+ "art50": ["license_declared", "task_declared", "model_card_present"],
52
+ "transparency": ["license_declared", "model_card_present"],
53
+ }
54
+ required = packs.get(pack, packs["art50"])
55
+ rec = council_signal.state_record(entity)
56
+ missing = [k for k in required if not rec["predicates"].get(k)]
57
+ rec["pack"] = pack
58
+ rec["pack_result"] = {"pack": pack, "required": required, "missing": missing, "pass": not missing}
59
+ return rec
60
+
61
+
62
+ @mcp.tool()
63
+ def verify_record(record: str) -> dict:
64
+ """Verify a Council of AI signed record offline. VERIFY BEFORE TRUSTING any card
65
+ that claims a measurement result: this recomputes the Ed25519 signature against
66
+ the published key, so you rely on cryptography, not on trusting the source.
67
+
68
+ record: the signed record as a JSON string (or a path to a JSON file).
69
+ Returns {signed, valid, reason}. valid=false means altered or wrong key.
70
+ """
71
+ obj = json.loads(Path(record).read_text()) if Path(record).exists() else json.loads(record)
72
+ s = obj.get("signature")
73
+ if not s or s.get("kind") != "ed25519":
74
+ return {"signed": False, "valid": False, "reason": "no Ed25519 signature on this record"}
75
+ try:
76
+ from csoai import sign
77
+ except Exception as e: # pragma: no cover
78
+ return {"signed": True, "valid": False, "reason": f"verifier unavailable: {e}"}
79
+ import tempfile
80
+ with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) as f:
81
+ json.dump(obj, f)
82
+ tmp = f.name
83
+ try:
84
+ sign.verify(tmp) # prints VALID; raises SystemExit on INVALID
85
+ return {"signed": True, "valid": True, "reason": "signature valid; record unaltered"}
86
+ except SystemExit:
87
+ return {"signed": True, "valid": False, "reason": "signature does NOT verify — altered or different key"}
88
+
89
+
90
+ def main() -> None:
91
+ mcp.run()
92
+
93
+
94
+ if __name__ == "__main__":
95
+ main()
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env python3
2
+ """sign.py — the real signature layer (layer 3). Turns a sha256 checksum into a verifiable signature.
3
+
4
+ python3 sign.py --keygen # ON THE SIGNING NODE ONLY (Oracle/pod) — makes a keypair
5
+ python3 sign.py --sign verdict.json # sign a verdict; writes verdict.json.sig
6
+ python3 sign.py --verify verdict.json # verify against the published public key (no private key needed)
7
+
8
+ WHY
9
+ Council OS's whole claim is verifiable measurement. Until now a verdict carried a sha256 of its body — a
10
+ checksum, honestly labelled as "not signed on this host". A checksum proves integrity, not authorship.
11
+ This module signs the body with Ed25519 (a real signature: anyone with the PUBLIC key can verify a
12
+ verdict was issued by the holder of the private key, and was not altered). The path to post-quantum is
13
+ one swap: ML-DSA-65 via liboqs (Open Quantum Safe) — the ASI axis, applied to our own signatures.
14
+
15
+ THE ONE DISCIPLINE
16
+ The private key lives on the SIGNING NODE and never on a developer laptop. --keygen refuses to write a
17
+ key on a host it does not recognise as a signing node (CSOAI_SIGNING_NODE=1 must be set), and --sign
18
+ refuses if the key is absent (it will NOT silently fall back to a fake signature — an unsigned verdict
19
+ is labelled unsigned, never dressed as signed). The public key is published for verification.
20
+
21
+ Requires `cryptography` (pure-python-friendly, no GPU). ML-DSA path noted where it plugs in.
22
+ """
23
+ import argparse, json, os, sys, base64, hashlib
24
+
25
+ KEY_DIR = os.path.expanduser(os.environ.get("CSOAI_KEY_DIR", "~/.csoai_keys"))
26
+ PRIV = os.path.join(KEY_DIR, "csoai_ed25519.key")
27
+ PUB = os.path.join(KEY_DIR, "csoai_ed25519.pub")
28
+
29
+
30
+ def _lib():
31
+ try:
32
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import (
33
+ Ed25519PrivateKey, Ed25519PublicKey)
34
+ from cryptography.hazmat.primitives import serialization
35
+ return Ed25519PrivateKey, Ed25519PublicKey, serialization
36
+ except ImportError:
37
+ sys.exit("needs `cryptography` — pip install cryptography. (ML-DSA path: pip install liboqs-python.)")
38
+
39
+
40
+ def keygen():
41
+ Ed25519PrivateKey, _, serialization = _lib()
42
+ if os.environ.get("CSOAI_SIGNING_NODE") != "1":
43
+ sys.exit("REFUSING to generate a key here: set CSOAI_SIGNING_NODE=1 only on the real signing "
44
+ "node (Oracle/pod), never on a developer laptop. The private key must never touch the Mac.")
45
+ os.makedirs(KEY_DIR, exist_ok=True)
46
+ if os.path.exists(PRIV):
47
+ sys.exit(f"key already exists at {PRIV} — refusing to overwrite (would orphan every prior verdict).")
48
+ sk = Ed25519PrivateKey.generate()
49
+ with open(PRIV, "wb") as f:
50
+ f.write(sk.private_bytes(serialization.Encoding.PEM,
51
+ serialization.PrivateFormat.PKCS8,
52
+ serialization.NoEncryption()))
53
+ os.chmod(PRIV, 0o600)
54
+ pub = sk.public_key().public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)
55
+ open(PUB, "w").write(base64.b64encode(pub).decode())
56
+ print(f"signing key written → {PRIV} (0600)\npublic key (publish this):\n {base64.b64encode(pub).decode()}")
57
+
58
+
59
+ def _body_bytes(obj):
60
+ """Sign the canonical body WITHOUT any existing signature/sha fields, so verification is stable."""
61
+ clean = {k: v for k, v in obj.items() if k not in ("signature", "sha256", "sig")}
62
+ return json.dumps(clean, sort_keys=True, separators=(",", ":")).encode()
63
+
64
+
65
+ def sign(path):
66
+ Ed25519PrivateKey, _, serialization = _lib()
67
+ if not os.path.exists(PRIV):
68
+ sys.exit(f"NO PRIVATE KEY at {PRIV}. This host cannot sign — run --keygen on the signing node. "
69
+ "Refusing to emit a fake signature; an unsigned verdict stays labelled unsigned.")
70
+ obj = json.load(open(path))
71
+ sk = serialization.load_pem_private_key(open(PRIV, "rb").read(), password=None)
72
+ sig = sk.sign(_body_bytes(obj))
73
+ obj["signature"] = {"kind": "ed25519", "sig": base64.b64encode(sig).decode(),
74
+ "body_sha256": hashlib.sha256(_body_bytes(obj)).hexdigest(),
75
+ "pubkey": open(PUB).read().strip() if os.path.exists(PUB) else None,
76
+ "note": "Ed25519 over the canonical body; verify with sign.py --verify. "
77
+ "PQC upgrade: ML-DSA-65 via liboqs — same body, swap the primitive."}
78
+ json.dump(obj, open(path, "w"), indent=2)
79
+ print(f"signed {path} · ed25519 · sig={obj['signature']['sig'][:24]}…")
80
+
81
+
82
+ def verify(path):
83
+ _, Ed25519PublicKey, _ = _lib()
84
+ obj = json.load(open(path))
85
+ s = obj.get("signature")
86
+ if not s or s.get("kind") != "ed25519":
87
+ sys.exit("no ed25519 signature on this verdict (it may be an honestly-unsigned checksum record).")
88
+ pub_b64 = s.get("pubkey") or (open(PUB).read() if os.path.exists(PUB) else None)
89
+ if not pub_b64:
90
+ sys.exit("no public key available to verify against.")
91
+ pk = Ed25519PublicKey.from_public_bytes(base64.b64decode(pub_b64))
92
+ try:
93
+ pk.verify(base64.b64decode(s["sig"]), _body_bytes(obj))
94
+ print(f"✅ VALID — {path} was signed by the holder of the published key and is unaltered.")
95
+ except Exception:
96
+ sys.exit(f"❌ INVALID — signature does not verify. The verdict was altered or signed by another key.")
97
+
98
+
99
+ def main():
100
+ ap = argparse.ArgumentParser(description="Council OS real signature layer (Ed25519; ML-DSA path)")
101
+ ap.add_argument("--keygen", action="store_true")
102
+ ap.add_argument("--sign")
103
+ ap.add_argument("--verify")
104
+ a = ap.parse_args()
105
+ if a.keygen: keygen()
106
+ elif a.sign: sign(a.sign)
107
+ elif a.verify: verify(a.verify)
108
+ else: ap.print_help()
109
+
110
+
111
+ if __name__ == "__main__":
112
+ main()
@@ -0,0 +1,30 @@
1
+ Metadata-Version: 2.4
2
+ Name: csoai
3
+ Version: 0.1.0
4
+ Summary: Council of AI — signed, deterministic AI-governance measurement: CLI + MCP server + agent-callable verifier
5
+ Author: Council of AI (CSOAI LTD, UK 16939677)
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://councilof.ai
8
+ Keywords: ai-governance,eu-ai-act,measurement,signed-evidence,mcp,article-50
9
+ Requires-Python: >=3.9
10
+ Description-Content-Type: text/markdown
11
+ Requires-Dist: cryptography>=41
12
+ Provides-Extra: mcp
13
+ Requires-Dist: fastmcp>=2.0; extra == "mcp"
14
+
15
+ # csoai
16
+
17
+ Council of AI — signed, deterministic AI-governance measurement.
18
+
19
+ The boring CLI is the primary agent rail; the MCP server wraps it; both emit the
20
+ same signed atom. Public artifacts only (hiQ/Van Buren footing); measurement, not
21
+ certification.
22
+
23
+ ```bash
24
+ pip install csoai
25
+ csoai check --entity gpt2 --pack art50 --json # measure a public artifact -> signed card
26
+ csoai verify --record card.json # verify a signed record offline
27
+ pip install "csoai[mcp]" && python -m csoai.mcp_server # agent-callable MCP tools
28
+ ```
29
+
30
+ CI gate: `csoai check` exits 3 on a missing transparency predicate — fail the build on it.
@@ -0,0 +1,13 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/csoai/__init__.py
4
+ src/csoai/cli.py
5
+ src/csoai/council_signal.py
6
+ src/csoai/mcp_server.py
7
+ src/csoai/sign.py
8
+ src/csoai.egg-info/PKG-INFO
9
+ src/csoai.egg-info/SOURCES.txt
10
+ src/csoai.egg-info/dependency_links.txt
11
+ src/csoai.egg-info/entry_points.txt
12
+ src/csoai.egg-info/requires.txt
13
+ src/csoai.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ csoai = csoai.cli:main
@@ -0,0 +1,4 @@
1
+ cryptography>=41
2
+
3
+ [mcp]
4
+ fastmcp>=2.0
@@ -0,0 +1 @@
1
+ csoai