From a9b22367fa87b943c8acc8b2c2af954f8a2ff6b7 Mon Sep 17 00:00:00 2001 From: Kevin Morrison <16977371+nccrypto@users.noreply.github.com> Date: Tue, 14 Jul 2026 14:36:13 -0400 Subject: [PATCH] update --- CHANGELOG.md | 1 + README.md | 2 +- ROADMAP.md | 2 +- docs/README.md | 1 + docs/provenance-schemas.md | 16 ++ examples/README.md | 4 +- examples/agent-job-result/README.md | 14 ++ .../reppo-inspection-result-v1.example.json | 59 +++++ schemas/README.md | 4 +- schemas/agent-job-result-v1.schema.json | 235 ++++++++++++++++++ tests/test_schema_contract.py | 47 ++++ 11 files changed, 381 insertions(+), 4 deletions(-) create mode 100644 examples/agent-job-result/README.md create mode 100644 examples/agent-job-result/reppo-inspection-result-v1.example.json create mode 100644 schemas/agent-job-result-v1.schema.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 71720e9..5371b8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and ### Added - Add a versioned source-manifest schema, documentation, and synthetic conforming example for public-source provenance records. +- Add a bounded agent-job result schema, documentation, and synthetic conforming example linked to source-manifest provenance. ### Security diff --git a/README.md b/README.md index 2432442..4db70b7 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ 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 portable public-source provenance records and bounded structured job results, see [docs/provenance-schemas.md](docs/provenance-schemas.md), `schemas/source-manifest-v1.schema.json`, and `schemas/agent-job-result-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. diff --git a/ROADMAP.md b/ROADMAP.md index 9800741..5c1d297 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -30,7 +30,7 @@ A phase is complete only when its artifacts are exercised and its verification g ## Phase 2 — Provenance and safety (`v0.2.0`) - [x] Versioned source-manifest schema -- [ ] Structured agent-job result schema +- [x] 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 61f49ef..d375f81 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,6 +2,7 @@ - [Reppo read-only ecosystem inspector](reppo-inspector.md) - [Provenance schemas](provenance-schemas.md) +- [Agent job result schema](provenance-schemas.md#agent-job-result-v1) - [Read-only maintenance automation](automation.md) - [Release process](releasing.md) diff --git a/docs/provenance-schemas.md b/docs/provenance-schemas.md index 0374be6..7b1b270 100644 --- a/docs/provenance-schemas.md +++ b/docs/provenance-schemas.md @@ -31,3 +31,19 @@ Manifests are provenance records, not proof that a source is still reachable or ## Example See `examples/source-manifest/reppo-public-api-manifest-v1.example.json` for a synthetic manifest describing public Reppo inspector inputs. + +## Agent job result v1 + +`schemas/agent-job-result-v1.schema.json` defines a bounded, public-only envelope for the outcome of an agentic-commerce job. It records: + +- a stable `jobId`, `jobType`, status, and start and completion timestamps; +- a bounded request summary made of named public scalar inputs; +- structured result data for successful and partial jobs, or `null` for failed jobs; +- provenance linking the result to a source manifest by `manifestId`, with an optional public manifest URL and source identifiers; +- bounded error and limitation records. + +The status values are `succeeded`, `partial`, and `failed`. Failed jobs must contain at least one error and use `null` for `result`; successful and partial jobs must contain a structured result object. Producers must ensure `completedAt` is not earlier than `startedAt`, because JSON Schema cannot compare the two timestamps. + +Request and result containers have explicit item, property, string, and finite nesting limits. These limits reduce the risk of accidental log or blob embedding, but they do not make private content safe: producers must still exclude credentials, wallet or account data, local paths, private runtime identifiers, and unpublished material. + +See `examples/agent-job-result/reppo-inspection-result-v1.example.json` for a synthetic partial inspection result linked to the source-manifest example. Cost, timeout, and freshness fields remain a separate Phase 2 roadmap item. diff --git a/examples/README.md b/examples/README.md index ae9bbd9..b051a7f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,5 +1,7 @@ # Examples - [Reppo inspector](reppo-inspector/README.md) +- [Source manifest](source-manifest/README.md) +- [Agent job result](agent-job-result/README.md) -Tested, public-source-only examples will live here. +Tested, public-source-only examples live here. diff --git a/examples/agent-job-result/README.md b/examples/agent-job-result/README.md new file mode 100644 index 0000000..1218fc3 --- /dev/null +++ b/examples/agent-job-result/README.md @@ -0,0 +1,14 @@ +# Agent job result example + +This directory contains synthetic examples for `schemas/agent-job-result-v1.schema.json`. + +The v1 envelope records a stable public job identifier, job type and status, timestamps, a bounded request summary, structured output, source-manifest provenance, errors, and limitations. A failed job uses `null` for `result` and includes at least one error; successful and partial jobs include a structured result object. + +The example is intentionally public-only and bounded: + +- request inputs are named scalar values or bounded scalar arrays; +- result strings, arrays, and objects have per-container and finite nesting limits; +- provenance refers to a public source manifest by stable identifier and optional public URL; +- no credentials, wallets, account identifiers, local paths, private runtime state, or unbounded blobs are included. + +Cost, timeout, and freshness fields are intentionally deferred to the next Phase 2 roadmap item. diff --git a/examples/agent-job-result/reppo-inspection-result-v1.example.json b/examples/agent-job-result/reppo-inspection-result-v1.example.json new file mode 100644 index 0000000..0e04b25 --- /dev/null +++ b/examples/agent-job-result/reppo-inspection-result-v1.example.json @@ -0,0 +1,59 @@ +{ + "schemaVersion": "1.0", + "jobId": "example:reppo-inspection:2026-07-14", + "jobType": "reppo.public-inspection", + "status": "partial", + "startedAt": "2026-07-14T12:00:00Z", + "completedAt": "2026-07-14T12:00:02Z", + "request": { + "summary": "Inspect bounded public Reppo catalog endpoints using a synthetic request summary.", + "inputs": [ + { + "name": "catalogs", + "value": ["datanets", "pods", "stats"] + }, + { + "name": "limit", + "value": 10 + } + ] + }, + "result": { + "summary": "The public datanet and pod catalogs returned data; the documented stats route was unavailable.", + "data": { + "catalogs": { + "datanets": { + "available": true, + "itemsReturned": 2 + }, + "pods": { + "available": true, + "itemsReturned": 2 + }, + "stats": { + "available": false, + "httpStatus": 404 + } + } + } + }, + "provenance": { + "manifestId": "example:reppo-public-api:2026-07-11", + "manifestUrl": "https://raw.githubusercontent.com/nccrypto/agentic-commerce-toolkit/main/examples/source-manifest/reppo-public-api-manifest-v1.example.json", + "sourceIds": [ + "reppo-docs-api-reference", + "reppo-public-datanets-endpoint" + ] + }, + "errors": [ + { + "code": "HTTP_ERROR", + "message": "The documented public stats route returned HTTP 404.", + "retryable": true + } + ], + "limitations": [ + "This synthetic example does not represent a live compatibility check.", + "The provenance reference identifies public evidence but does not prove that an upstream source is still available." + ] +} diff --git a/schemas/README.md b/schemas/README.md index ebfcefe..032b397 100644 --- a/schemas/README.md +++ b/schemas/README.md @@ -4,5 +4,7 @@ - `../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. +- `agent-job-result-v1.schema.json` — bounded public result envelope for structured agent jobs. +- `../examples/agent-job-result/reppo-inspection-result-v1.example.json` — synthetic conforming agent-job result validated in CI. -Versioned JSON schemas for structured agent-job results and safety patterns will live here. +Versioned JSON schemas for additional safety patterns will live here. diff --git a/schemas/agent-job-result-v1.schema.json b/schemas/agent-job-result-v1.schema.json new file mode 100644 index 0000000..e59a417 --- /dev/null +++ b/schemas/agent-job-result-v1.schema.json @@ -0,0 +1,235 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/nccrypto/agentic-commerce-toolkit/main/schemas/agent-job-result-v1.schema.json", + "title": "Agentic Commerce Agent Job Result v1", + "description": "A bounded, public-only result envelope for an agentic-commerce job.", + "type": "object", + "additionalProperties": false, + "required": [ + "schemaVersion", + "jobId", + "jobType", + "status", + "startedAt", + "completedAt", + "request", + "result", + "provenance", + "errors", + "limitations" + ], + "properties": { + "schemaVersion": {"const": "1.0"}, + "jobId": { + "type": "string", + "pattern": "^[a-z0-9][a-z0-9._:-]{2,127}$", + "description": "Stable public identifier for the job; it must not contain account, wallet, credential, or private runtime identifiers." + }, + "jobType": { + "type": "string", + "pattern": "^[a-z][a-z0-9._-]{1,63}$" + }, + "status": { + "enum": ["succeeded", "partial", "failed"] + }, + "startedAt": {"type": "string", "format": "date-time"}, + "completedAt": {"type": "string", "format": "date-time"}, + "request": {"$ref": "#/$defs/request"}, + "result": { + "oneOf": [ + {"$ref": "#/$defs/result"}, + {"type": "null"} + ] + }, + "provenance": {"$ref": "#/$defs/provenance"}, + "errors": { + "type": "array", + "maxItems": 50, + "items": {"$ref": "#/$defs/error"} + }, + "limitations": { + "type": "array", + "maxItems": 50, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 500 + } + } + }, + "allOf": [ + { + "if": { + "properties": {"status": {"enum": ["succeeded", "partial"]}}, + "required": ["status"] + }, + "then": { + "properties": {"result": {"$ref": "#/$defs/result"}} + } + }, + { + "if": { + "properties": {"status": {"const": "failed"}}, + "required": ["status"] + }, + "then": { + "properties": { + "result": {"type": "null"}, + "errors": {"minItems": 1} + } + } + } + ], + "$defs": { + "request": { + "type": "object", + "additionalProperties": false, + "required": ["summary", "inputs"], + "properties": { + "summary": { + "type": "string", + "minLength": 1, + "maxLength": 500 + }, + "inputs": { + "type": "array", + "maxItems": 50, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["name", "value"], + "properties": { + "name": { + "type": "string", + "pattern": "^[a-z][a-zA-Z0-9._-]{0,63}$" + }, + "value": {"$ref": "#/$defs/requestValue"} + } + } + } + } + }, + "requestValue": { + "anyOf": [ + {"$ref": "#/$defs/publicScalar"}, + { + "type": "array", + "maxItems": 100, + "items": {"$ref": "#/$defs/publicScalar"} + } + ] + }, + "result": { + "type": "object", + "additionalProperties": false, + "required": ["summary", "data"], + "properties": { + "summary": { + "type": "string", + "minLength": 1, + "maxLength": 1000 + }, + "data": { + "type": "object", + "maxProperties": 100, + "additionalProperties": {"$ref": "#/$defs/publicValue"} + } + } + }, + "provenance": { + "type": "object", + "additionalProperties": false, + "required": ["manifestId"], + "properties": { + "manifestId": { + "type": "string", + "pattern": "^[a-z0-9][a-z0-9._:-]{2,127}$" + }, + "manifestUrl": { + "type": "string", + "format": "uri", + "description": "Optional public URL for the conforming source manifest." + }, + "sourceIds": { + "type": "array", + "maxItems": 100, + "uniqueItems": true, + "items": { + "type": "string", + "pattern": "^[a-z0-9][a-z0-9._:-]{1,127}$" + } + } + } + }, + "error": { + "type": "object", + "additionalProperties": false, + "required": ["code", "message"], + "properties": { + "code": { + "type": "string", + "pattern": "^[A-Z][A-Z0-9_]{1,63}$" + }, + "message": { + "type": "string", + "minLength": 1, + "maxLength": 500 + }, + "retryable": {"type": "boolean"} + } + }, + "publicScalar": { + "anyOf": [ + {"type": "string", "maxLength": 1000}, + {"type": "number"}, + {"type": "boolean"}, + {"type": "null"} + ] + }, + "publicValue": { + "anyOf": [ + {"$ref": "#/$defs/publicScalar"}, + { + "type": "array", + "maxItems": 100, + "items": {"$ref": "#/$defs/publicValueLevel2"} + }, + { + "type": "object", + "maxProperties": 100, + "additionalProperties": {"$ref": "#/$defs/publicValueLevel2"} + } + ] + }, + "publicValueLevel2": { + "anyOf": [ + {"$ref": "#/$defs/publicScalar"}, + { + "type": "array", + "maxItems": 100, + "items": {"$ref": "#/$defs/publicValueLevel3"} + }, + { + "type": "object", + "maxProperties": 100, + "additionalProperties": {"$ref": "#/$defs/publicValueLevel3"} + } + ] + }, + "publicValueLevel3": { + "anyOf": [ + {"$ref": "#/$defs/publicScalar"}, + { + "type": "array", + "maxItems": 100, + "items": {"$ref": "#/$defs/publicScalar"} + }, + { + "type": "object", + "maxProperties": 100, + "additionalProperties": {"$ref": "#/$defs/publicScalar"} + } + ] + } + } +} diff --git a/tests/test_schema_contract.py b/tests/test_schema_contract.py index 855d737..9367b88 100644 --- a/tests/test_schema_contract.py +++ b/tests/test_schema_contract.py @@ -10,6 +10,8 @@ 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" +AGENT_JOB_RESULT_SCHEMA = ROOT / "schemas" / "agent-job-result-v1.schema.json" +AGENT_JOB_RESULT_EXAMPLE = ROOT / "examples" / "agent-job-result" / "reppo-inspection-result-v1.example.json" def load_json(path): @@ -33,6 +35,9 @@ def test_reppo_example_conforms_to_inspector_envelope_v1(self): def test_source_manifest_example_conforms_to_source_manifest_v1(self): self.assert_conforms(SOURCE_MANIFEST_SCHEMA, SOURCE_MANIFEST_EXAMPLE) + def test_agent_job_result_example_conforms_to_agent_job_result_v1(self): + self.assert_conforms(AGENT_JOB_RESULT_SCHEMA, AGENT_JOB_RESULT_EXAMPLE) + def test_source_manifest_requires_public_source_records(self): schema = load_json(SOURCE_MANIFEST_SCHEMA) validator = Draft202012Validator(schema, format_checker=FormatChecker()) @@ -55,6 +60,48 @@ def test_source_manifest_rejects_unbounded_extra_fields(self): self.assertTrue(any("privateRuntimeState" in message for message in messages)) self.assertTrue(any("localPath" in message for message in messages)) + def test_agent_job_result_rejects_private_or_unbounded_fields(self): + schema = load_json(AGENT_JOB_RESULT_SCHEMA) + validator = Draft202012Validator(schema, format_checker=FormatChecker()) + job_result = load_json(AGENT_JOB_RESULT_EXAMPLE) + job_result["privateRuntimeState"] = "not allowed" + job_result["request"]["localPath"] = "runtime-cache" + job_result["request"]["inputs"][0]["value"] = ["item"] * 101 + + errors = list(validator.iter_errors(job_result)) + messages = [error.message for error in errors] + + self.assertTrue(any("privateRuntimeState" in message for message in messages)) + self.assertTrue(any("localPath" in message for message in messages)) + self.assertTrue( + any( + list(error.absolute_path) == ["request", "inputs", 0, "value"] + for error in errors + ) + ) + + def test_agent_job_result_rejects_excessive_result_nesting(self): + schema = load_json(AGENT_JOB_RESULT_SCHEMA) + validator = Draft202012Validator(schema, format_checker=FormatChecker()) + job_result = load_json(AGENT_JOB_RESULT_EXAMPLE) + job_result["result"]["data"] = { + "one": {"two": {"three": {"four": {"five": "too deep"}}}} + } + + self.assertFalse(validator.is_valid(job_result)) + + def test_failed_agent_job_requires_null_result_and_an_error(self): + schema = load_json(AGENT_JOB_RESULT_SCHEMA) + validator = Draft202012Validator(schema, format_checker=FormatChecker()) + job_result = load_json(AGENT_JOB_RESULT_EXAMPLE) + job_result["status"] = "failed" + job_result["errors"] = [] + + messages = [error.message for error in validator.iter_errors(job_result)] + + self.assertTrue(any("is not of type 'null'" in message for message in messages)) + self.assertTrue(any("should be non-empty" in message for message in messages)) + if __name__ == "__main__": unittest.main()