From 788c5da3d7098fd054d8406d1febbd6a8d068689 Mon Sep 17 00:00:00 2001 From: Kevin Morrison <16977371+nccrypto@users.noreply.github.com> Date: Mon, 13 Jul 2026 05:56:26 -0400 Subject: [PATCH] Add source manifest provenance schema --- CHANGELOG.md | 4 + README.md | 2 + ROADMAP.md | 2 +- docs/README.md | 1 + docs/provenance-schemas.md | 33 ++++ examples/source-manifest/README.md | 12 ++ .../reppo-public-api-manifest-v1.example.json | 52 ++++++ schemas/README.md | 4 +- schemas/source-manifest-v1.schema.json | 168 ++++++++++++++++++ tests/test_schema_contract.py | 47 ++++- 10 files changed, 317 insertions(+), 8 deletions(-) create mode 100644 docs/provenance-schemas.md create mode 100644 examples/source-manifest/README.md create mode 100644 examples/source-manifest/reppo-public-api-manifest-v1.example.json create mode 100644 schemas/source-manifest-v1.schema.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 3666f6e..71720e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and ## [Unreleased] +### Added + +- Add a versioned source-manifest schema, documentation, and synthetic conforming example for public-source provenance records. + ### Security - Generalize public-boundary policy and scanner rules so documentation does not enumerate non-public system identifiers. diff --git a/README.md b/README.md index 9d9e164..2432442 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,8 @@ python3 -m build See [docs/reppo-inspector.md](docs/reppo-inspector.md) for the JSON contract, canonical-host policy, input limits, exit codes, endpoint boundary, and failure behavior. Release maintainers should follow [docs/releasing.md](docs/releasing.md). +For portable public-source provenance records, see [docs/provenance-schemas.md](docs/provenance-schemas.md) and `schemas/source-manifest-v1.schema.json`. + For silent compatibility drift detection and bounded weekly project evidence, see [docs/automation.md](docs/automation.md). These helpers are read-only and never perform GitHub mutations. At the 2026-07-11 compatibility check, the datanet and pod catalogs were live. The documented public stats route returned HTTP 404, so `status` and `snapshot` correctly returned partial result code `2` while preserving catalog data. The upstream pods route also ignored its requested page size; the client applies the requested limit after a capped download. diff --git a/ROADMAP.md b/ROADMAP.md index adb486a..9800741 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -29,7 +29,7 @@ A phase is complete only when its artifacts are exercised and its verification g ## Phase 2 — Provenance and safety (`v0.2.0`) -- [ ] Versioned source-manifest schema +- [x] Versioned source-manifest schema - [ ] Structured agent-job result schema - [ ] Cost, timeout, and freshness fields - [ ] Dry-run and approval-control reference patterns diff --git a/docs/README.md b/docs/README.md index 5767122..61f49ef 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,7 @@ # Documentation - [Reppo read-only ecosystem inspector](reppo-inspector.md) +- [Provenance schemas](provenance-schemas.md) - [Read-only maintenance automation](automation.md) - [Release process](releasing.md) diff --git a/docs/provenance-schemas.md b/docs/provenance-schemas.md new file mode 100644 index 0000000..0374be6 --- /dev/null +++ b/docs/provenance-schemas.md @@ -0,0 +1,33 @@ +# Provenance schemas + +The toolkit's provenance schemas provide portable, public-only records for the sources and evidence behind agentic-commerce artifacts. + +## Source manifest v1 + +`schemas/source-manifest-v1.schema.json` defines a manifest for documenting public sources used by a job, dataset, inspector output, compatibility check, or future reference service. + +A conforming manifest records: + +- `schemaVersion` — currently `1.0`; +- `manifestId` — a stable, non-secret identifier; +- `generatedAt` — when the manifest was produced; +- `subject` — the public artifact or topic the manifest describes; +- `sources` — one or more public source records; +- optional `notes` — bounded public notes. + +Each source record includes: + +- a stable `sourceId`; +- title, URL, type, publisher, and access time; +- optional license, version, retrieval time, or SHA-256 digest; +- a `usage` section describing why the source was used, what public facts were derived, and known limitations. + +## Boundary rules + +Source manifests must not contain credentials, private keys, wallet exports, local runtime state, private communications, unpublished research, or absolute local paths. Use public URLs, intentionally authored descriptions, and synthetic examples. + +Manifests are provenance records, not proof that a source is still reachable or authoritative. Consumers should treat them as evidence to review and should re-check upstream sources when freshness matters. + +## Example + +See `examples/source-manifest/reppo-public-api-manifest-v1.example.json` for a synthetic manifest describing public Reppo inspector inputs. diff --git a/examples/source-manifest/README.md b/examples/source-manifest/README.md new file mode 100644 index 0000000..5111282 --- /dev/null +++ b/examples/source-manifest/README.md @@ -0,0 +1,12 @@ +# Source manifest example + +This directory contains synthetic examples for `schemas/source-manifest-v1.schema.json`. + +A source manifest records the public sources that support an agentic-commerce artifact. It is intended for agent jobs, inspector outputs, compatibility checks, examples, and future reference services that need portable provenance without private runtime context. + +The example manifest is intentionally small and public-only: + +- no credentials; +- no wallet, account, budget, or transaction data; +- no local filesystem paths; +- no private communications or unpublished research. diff --git a/examples/source-manifest/reppo-public-api-manifest-v1.example.json b/examples/source-manifest/reppo-public-api-manifest-v1.example.json new file mode 100644 index 0000000..fa17505 --- /dev/null +++ b/examples/source-manifest/reppo-public-api-manifest-v1.example.json @@ -0,0 +1,52 @@ +{ + "schemaVersion": "1.0", + "manifestId": "example:reppo-public-api:2026-07-11", + "generatedAt": "2026-07-11T00:00:00Z", + "subject": { + "kind": "protocol-source", + "name": "Reppo public API inspector inputs", + "version": "v0.1.0", + "description": "Synthetic example manifest for public sources used by the read-only Reppo inspector." + }, + "sources": [ + { + "sourceId": "reppo-docs-api-reference", + "title": "Reppo API reference", + "url": "https://docs.reppo.xyz/", + "sourceType": "documentation", + "publisher": "Reppo", + "accessedAt": "2026-07-11T00:00:00Z", + "usage": { + "purpose": "schema-reference", + "derivedFacts": [ + "The inspector should use public Reppo endpoints only.", + "Source URLs and fetch timing belong in the machine-readable output envelope." + ], + "limitations": [ + "Documentation can drift from live API behavior, so compatibility checks should record partial failures instead of fabricating replacement data." + ] + } + }, + { + "sourceId": "reppo-public-datanets-endpoint", + "title": "Reppo public datanet catalog endpoint", + "url": "https://api.reppo.xyz/public/subnets", + "sourceType": "api", + "publisher": "Reppo", + "accessedAt": "2026-07-11T00:00:00Z", + "usage": { + "purpose": "input", + "derivedFacts": [ + "Datanet catalog responses are preserved under the inspector envelope data field.", + "The endpoint is queried read-only and does not require authentication." + ], + "limitations": [ + "Availability and response shape can change upstream." + ] + } + } + ], + "notes": [ + "This example is synthetic and contains no credentials, private account data, or local runtime state." + ] +} diff --git a/schemas/README.md b/schemas/README.md index da453cb..ebfcefe 100644 --- a/schemas/README.md +++ b/schemas/README.md @@ -2,5 +2,7 @@ - `inspector-envelope-v1.schema.json` — stable top-level contract for Reppo inspector output. - `../examples/reppo-inspector/datanets-envelope-v1.example.json` — synthetic conforming example validated in CI. +- `source-manifest-v1.schema.json` — public-source provenance manifest for agentic-commerce artifacts. +- `../examples/source-manifest/reppo-public-api-manifest-v1.example.json` — synthetic conforming source-manifest example validated in CI. -Versioned JSON schemas for ecosystem status, provenance, and agent-job results will live here. +Versioned JSON schemas for structured agent-job results and safety patterns will live here. diff --git a/schemas/source-manifest-v1.schema.json b/schemas/source-manifest-v1.schema.json new file mode 100644 index 0000000..4d9cbb0 --- /dev/null +++ b/schemas/source-manifest-v1.schema.json @@ -0,0 +1,168 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/nccrypto/agentic-commerce-toolkit/main/schemas/source-manifest-v1.schema.json", + "title": "Agentic Commerce Source Manifest v1", + "description": "A portable manifest describing public sources used by an agentic-commerce job or dataset.", + "type": "object", + "additionalProperties": false, + "required": [ + "schemaVersion", + "manifestId", + "generatedAt", + "subject", + "sources" + ], + "properties": { + "schemaVersion": {"const": "1.0"}, + "manifestId": { + "type": "string", + "pattern": "^[a-z0-9][a-z0-9._:-]{2,127}$", + "description": "Stable, non-secret identifier for this manifest." + }, + "generatedAt": {"type": "string", "format": "date-time"}, + "subject": { + "type": "object", + "additionalProperties": false, + "required": ["kind", "name"], + "properties": { + "kind": { + "type": "string", + "pattern": "^[a-z][a-z0-9._-]{1,63}$", + "description": "Public category for the thing being documented, such as protocol, dataset, inspector-output, or agent-job." + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 160 + }, + "version": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "description": { + "type": "string", + "minLength": 1, + "maxLength": 500 + } + } + }, + "sources": { + "type": "array", + "minItems": 1, + "maxItems": 100, + "items": {"$ref": "#/$defs/source"} + }, + "notes": { + "type": "array", + "maxItems": 20, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 500 + } + } + }, + "$defs": { + "source": { + "type": "object", + "additionalProperties": false, + "required": [ + "sourceId", + "title", + "url", + "sourceType", + "publisher", + "accessedAt", + "usage" + ], + "properties": { + "sourceId": { + "type": "string", + "pattern": "^[a-z0-9][a-z0-9._:-]{1,127}$" + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 240 + }, + "url": {"type": "string", "format": "uri"}, + "sourceType": { + "enum": [ + "api", + "documentation", + "repository", + "schema", + "example", + "issue", + "release", + "article", + "other" + ] + }, + "publisher": { + "type": "string", + "minLength": 1, + "maxLength": 160 + }, + "accessedAt": {"type": "string", "format": "date-time"}, + "retrievedAt": {"type": "string", "format": "date-time"}, + "license": { + "type": "string", + "minLength": 1, + "maxLength": 120 + }, + "version": { + "type": "string", + "minLength": 1, + "maxLength": 120 + }, + "contentDigest": { + "type": "object", + "additionalProperties": false, + "required": ["algorithm", "value"], + "properties": { + "algorithm": {"enum": ["sha256"]}, + "value": {"type": "string", "pattern": "^[a-f0-9]{64}$"} + } + }, + "usage": { + "type": "object", + "additionalProperties": false, + "required": ["purpose", "derivedFacts"], + "properties": { + "purpose": { + "enum": [ + "input", + "validation", + "compatibility-check", + "citation", + "fixture-basis", + "schema-reference", + "other" + ] + }, + "derivedFacts": { + "type": "array", + "maxItems": 20, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 300 + } + }, + "limitations": { + "type": "array", + "maxItems": 20, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 300 + } + } + } + } + } + } + } +} diff --git a/tests/test_schema_contract.py b/tests/test_schema_contract.py index 3c043f5..855d737 100644 --- a/tests/test_schema_contract.py +++ b/tests/test_schema_contract.py @@ -6,19 +6,54 @@ ROOT = Path(__file__).parents[1] -SCHEMA = ROOT / "schemas" / "inspector-envelope-v1.schema.json" -EXAMPLE = ROOT / "examples" / "reppo-inspector" / "datanets-envelope-v1.example.json" +INSPECTOR_SCHEMA = ROOT / "schemas" / "inspector-envelope-v1.schema.json" +INSPECTOR_EXAMPLE = ROOT / "examples" / "reppo-inspector" / "datanets-envelope-v1.example.json" +SOURCE_MANIFEST_SCHEMA = ROOT / "schemas" / "source-manifest-v1.schema.json" +SOURCE_MANIFEST_EXAMPLE = ROOT / "examples" / "source-manifest" / "reppo-public-api-manifest-v1.example.json" + + +def load_json(path): + return json.loads(path.read_text(encoding="utf-8")) + + +def validation_errors(schema_path, example_path): + validator = Draft202012Validator(load_json(schema_path), format_checker=FormatChecker()) + example = load_json(example_path) + return sorted(validator.iter_errors(example), key=lambda error: list(error.path)) class SchemaContractTests(unittest.TestCase): + def assert_conforms(self, schema_path, example_path): + errors = validation_errors(schema_path, example_path) + self.assertEqual([], [error.message for error in errors]) + def test_reppo_example_conforms_to_inspector_envelope_v1(self): - schema = json.loads(SCHEMA.read_text(encoding="utf-8")) - example = json.loads(EXAMPLE.read_text(encoding="utf-8")) + self.assert_conforms(INSPECTOR_SCHEMA, INSPECTOR_EXAMPLE) + + def test_source_manifest_example_conforms_to_source_manifest_v1(self): + self.assert_conforms(SOURCE_MANIFEST_SCHEMA, SOURCE_MANIFEST_EXAMPLE) + + def test_source_manifest_requires_public_source_records(self): + schema = load_json(SOURCE_MANIFEST_SCHEMA) validator = Draft202012Validator(schema, format_checker=FormatChecker()) + manifest = load_json(SOURCE_MANIFEST_EXAMPLE) + manifest["sources"] = [] - errors = sorted(validator.iter_errors(example), key=lambda error: list(error.path)) + errors = sorted(validator.iter_errors(manifest), key=lambda error: list(error.path)) - self.assertEqual([], [error.message for error in errors]) + self.assertIn("[] should be non-empty", [error.message for error in errors]) + + def test_source_manifest_rejects_unbounded_extra_fields(self): + schema = load_json(SOURCE_MANIFEST_SCHEMA) + validator = Draft202012Validator(schema, format_checker=FormatChecker()) + manifest = load_json(SOURCE_MANIFEST_EXAMPLE) + manifest["privateRuntimeState"] = "not allowed" + manifest["sources"][0]["localPath"] = "runtime-cache" + + messages = [error.message for error in validator.iter_errors(manifest)] + + self.assertTrue(any("privateRuntimeState" in message for message in messages)) + self.assertTrue(any("localPath" in message for message in messages)) if __name__ == "__main__":