From ebf653f27ffa37b0a356456e6bae13f1a10c5b89 Mon Sep 17 00:00:00 2001 From: Kevin Morrison <16977371+nccrypto@users.noreply.github.com> Date: Tue, 14 Jul 2026 17:00:37 -0400 Subject: [PATCH] commit --- CHANGELOG.md | 1 + DESIGN.md | 4 ++ ROADMAP.md | 2 +- docs/provenance-schemas.md | 11 ++- examples/agent-job-result/README.md | 5 +- .../reppo-inspection-result-v1.example.json | 16 +++++ schemas/README.md | 2 +- schemas/agent-job-result-v1.schema.json | 71 ++++++++++++++++++- tests/test_schema_contract.py | 30 ++++++++ 9 files changed, 136 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5371b8e..ab5e6c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and - 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. +- Add backward-compatible cost, timeout, and freshness metadata to agent-job results. ### Security diff --git a/DESIGN.md b/DESIGN.md index 4220ef5..57667dc 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -80,3 +80,7 @@ Runtime networking is pinned to the canonical Reppo public API and uses `urllib` ### D8. Defend against upstream pagination and availability drift The public pod catalog currently returns the full catalog even when a smaller `limit` is requested. The inspector therefore validates `data.subnets` and `data.pods`, downloads only within the hard response cap, and slices returned collections to the caller's requested limit. The documented public stats route currently returns HTTP 404; aggregate commands preserve successful catalog data and mark the result partial rather than inventing replacement metrics. + +### D9. Add operational observations to v1 without breaking existing documents + +Cost, timeout, and freshness are optional, strictly bounded objects in the agent-job result v1 schema so documents created against the initial v1 contract remain valid. Costs use non-negative decimal strings rather than JSON numbers to avoid floating-point ambiguity. These fields expose public job-level observations only and exclude account, wallet, payment-credential, internal-budget, and provider-secret data. diff --git a/ROADMAP.md b/ROADMAP.md index 5c1d297..84477d0 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -31,7 +31,7 @@ A phase is complete only when its artifacts are exercised and its verification g - [x] Versioned source-manifest schema - [x] Structured agent-job result schema -- [ ] Cost, timeout, and freshness fields +- [x] Cost, timeout, and freshness fields - [ ] Dry-run and approval-control reference patterns **Gate:** another example can consume the schemas without private project context. diff --git a/docs/provenance-schemas.md b/docs/provenance-schemas.md index 7b1b270..eec5dc5 100644 --- a/docs/provenance-schemas.md +++ b/docs/provenance-schemas.md @@ -40,10 +40,19 @@ See `examples/source-manifest/reppo-public-api-manifest-v1.example.json` for a s - 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; +- optional bounded cost, timeout, and freshness metadata; - 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. +The optional operational fields are backward-compatible additions to v1: + +- `cost` records a non-negative decimal-string `amount`, a bounded uppercase currency or settlement-unit code, and whether the amount was measured or estimated. Decimal strings avoid floating-point ambiguity. +- `timeout` records the configured limit, observed elapsed time, and whether the job timed out. Durations are bounded integer milliseconds. +- `freshness` records when freshness was evaluated and whether data was `fresh`, `stale`, or `unknown`. Fresh and stale records also require `dataAsOf` and `maxAgeSeconds`; producers determine the status and must document relevant limitations. + +These fields expose public job-level observations only. They must not contain account identifiers, payment credentials, wallet data, internal budgets, billing records, or provider secrets. + +See `examples/agent-job-result/reppo-inspection-result-v1.example.json` for a synthetic partial inspection result linked to the source-manifest example. diff --git a/examples/agent-job-result/README.md b/examples/agent-job-result/README.md index 1218fc3..a353a75 100644 --- a/examples/agent-job-result/README.md +++ b/examples/agent-job-result/README.md @@ -2,13 +2,14 @@ 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 v1 envelope records a stable public job identifier, job type and status, timestamps, a bounded request summary, structured output, source-manifest provenance, optional operational metadata, 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; +- optional cost, timeout, and freshness records are bounded and contain no account or payment details; - 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. +The example reports a synthetic zero measured cost, a five-second timeout limit, and stale source data. These values demonstrate the contract and are not live operational measurements. diff --git a/examples/agent-job-result/reppo-inspection-result-v1.example.json b/examples/agent-job-result/reppo-inspection-result-v1.example.json index 0e04b25..c02b14f 100644 --- a/examples/agent-job-result/reppo-inspection-result-v1.example.json +++ b/examples/agent-job-result/reppo-inspection-result-v1.example.json @@ -45,6 +45,22 @@ "reppo-public-datanets-endpoint" ] }, + "cost": { + "amount": "0", + "currency": "USD", + "basis": "measured" + }, + "timeout": { + "limitMs": 5000, + "elapsedMs": 2000, + "timedOut": false + }, + "freshness": { + "evaluatedAt": "2026-07-14T12:00:02Z", + "status": "stale", + "dataAsOf": "2026-07-11T00:00:00Z", + "maxAgeSeconds": 86400 + }, "errors": [ { "code": "HTTP_ERROR", diff --git a/schemas/README.md b/schemas/README.md index 032b397..631620f 100644 --- a/schemas/README.md +++ b/schemas/README.md @@ -4,7 +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. +- `agent-job-result-v1.schema.json` — bounded public result envelope for structured agent jobs, including optional cost, timeout, and freshness metadata. - `../examples/agent-job-result/reppo-inspection-result-v1.example.json` — synthetic conforming agent-job result validated in CI. 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 index e59a417..0684ee7 100644 --- a/schemas/agent-job-result-v1.schema.json +++ b/schemas/agent-job-result-v1.schema.json @@ -55,7 +55,10 @@ "minLength": 1, "maxLength": 500 } - } + }, + "cost": {"$ref": "#/$defs/cost"}, + "timeout": {"$ref": "#/$defs/timeout"}, + "freshness": {"$ref": "#/$defs/freshness"} }, "allOf": [ { @@ -81,6 +84,72 @@ } ], "$defs": { + "cost": { + "type": "object", + "additionalProperties": false, + "required": ["amount", "currency", "basis"], + "properties": { + "amount": { + "type": "string", + "pattern": "^(0|[1-9][0-9]{0,17})(\\.[0-9]{1,18})?$", + "description": "Non-negative decimal cost encoded as a string to avoid floating-point ambiguity." + }, + "currency": { + "type": "string", + "pattern": "^[A-Z][A-Z0-9]{2,11}$", + "description": "Public uppercase currency or settlement-unit code, such as USD or USDC." + }, + "basis": { + "enum": ["measured", "estimated"] + } + } + }, + "timeout": { + "type": "object", + "additionalProperties": false, + "required": ["limitMs", "elapsedMs", "timedOut"], + "properties": { + "limitMs": { + "type": "integer", + "minimum": 1, + "maximum": 604800000 + }, + "elapsedMs": { + "type": "integer", + "minimum": 0, + "maximum": 604800000 + }, + "timedOut": {"type": "boolean"} + } + }, + "freshness": { + "type": "object", + "additionalProperties": false, + "required": ["evaluatedAt", "status"], + "properties": { + "evaluatedAt": {"type": "string", "format": "date-time"}, + "status": { + "enum": ["fresh", "stale", "unknown"] + }, + "dataAsOf": {"type": "string", "format": "date-time"}, + "maxAgeSeconds": { + "type": "integer", + "minimum": 0, + "maximum": 315360000 + } + }, + "allOf": [ + { + "if": { + "properties": {"status": {"enum": ["fresh", "stale"]}}, + "required": ["status"] + }, + "then": { + "required": ["dataAsOf", "maxAgeSeconds"] + } + } + ] + }, "request": { "type": "object", "additionalProperties": false, diff --git a/tests/test_schema_contract.py b/tests/test_schema_contract.py index 9367b88..e425b54 100644 --- a/tests/test_schema_contract.py +++ b/tests/test_schema_contract.py @@ -38,6 +38,36 @@ def test_source_manifest_example_conforms_to_source_manifest_v1(self): 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_agent_job_operational_fields_are_backward_compatible_and_optional(self): + schema = load_json(AGENT_JOB_RESULT_SCHEMA) + validator = Draft202012Validator(schema, format_checker=FormatChecker()) + job_result = load_json(AGENT_JOB_RESULT_EXAMPLE) + job_result.pop("cost") + job_result.pop("timeout") + job_result.pop("freshness") + + self.assertTrue(validator.is_valid(job_result)) + + def test_agent_job_operational_fields_are_bounded(self): + schema = load_json(AGENT_JOB_RESULT_SCHEMA) + validator = Draft202012Validator(schema, format_checker=FormatChecker()) + + invalid_cost = load_json(AGENT_JOB_RESULT_EXAMPLE) + invalid_cost["cost"]["amount"] = "-1" + self.assertFalse(validator.is_valid(invalid_cost)) + + private_cost = load_json(AGENT_JOB_RESULT_EXAMPLE) + private_cost["cost"]["accountId"] = "not allowed" + self.assertFalse(validator.is_valid(private_cost)) + + invalid_timeout = load_json(AGENT_JOB_RESULT_EXAMPLE) + invalid_timeout["timeout"]["limitMs"] = 0 + self.assertFalse(validator.is_valid(invalid_timeout)) + + incomplete_freshness = load_json(AGENT_JOB_RESULT_EXAMPLE) + incomplete_freshness["freshness"].pop("dataAsOf") + self.assertFalse(validator.is_valid(incomplete_freshness)) + def test_source_manifest_requires_public_source_records(self): schema = load_json(SOURCE_MANIFEST_SCHEMA) validator = Draft202012Validator(schema, format_checker=FormatChecker())