From 8a014e8e93f9a5a61e86cc1a67d33fda56dd79d3 Mon Sep 17 00:00:00 2001 From: Michael Heller <21163552+mdheller@users.noreply.github.com> Date: Sun, 2 Aug 2026 04:38:30 -0400 Subject: [PATCH 1/2] feat(bridge): widen sociosphere-bridge to carry CHRONOS carrier objects Extends the existing sociosphere-bridge seam (validate_bundle.py, emit_run_artifact.py, emit_replay_artifact.py) so it can also carry CHRONOS neuro-symbolic carrier objects (source evidence ref, method family, claim/validation status, non-authority declaration, owning plane, replay ref) as a second carried-object type alongside its existing SourceOS/sociosphere workspace artifact handling -- reusing the same Bundle-spec transport and extraction-function pattern rather than building a parallel bridge. - scripts/validate_bundle.py: add validate_chronos_carrier(), a fail-closed structural gate mirroring validate_sourceos_image_production(). Rejects a declared spec.chronosCarrier that is incomplete, that does not set nonAuthorityDeclaration=true, or that names agentplane as owningPlane. - scripts/emit_run_artifact.py, scripts/emit_replay_artifact.py: add extract_chronos_carrier(), a permissive projector mirroring extract_sourceos_image_production(), so RunArtifact/ReplayArtifact carry whatever chronosCarrier fields were declared. - schemas/bundle.schema.v0.1.json, schemas/run-artifact.schema.v0.1.json, schemas/replay-artifact.schema.v0.1.json: document the new optional field. - docs/sociosphere-bridge.md: document the additive extension and its non-authority stance. - tests/test_chronos_carrier_bridge.py + tests/fixtures/chronos-carrier/: gate/projection unit tests, a superset check proving the pre-existing SourceOS binding path is untouched, and CLI-level valid/reject fixture coverage for the negative cases (missing field, false non-authority declaration, owningPlane=agentplane). - examples/run-replay/minimal-bundle-with-chronos-carrier.json + .github/workflows/run-replay-artifacts.yml: exercise the new carrier type end-to-end in the existing run-replay-artifacts CI job, and run the bridge unit tests there too. - Makefile: new validate-chronos-carrier-bridge target wired into `validate`. Refs #329. --- .github/workflows/run-replay-artifacts.yml | 28 ++- Makefile | 10 + docs/sociosphere-bridge.md | 44 ++++ .../minimal-bundle-with-chronos-carrier.json | 35 ++++ schemas/bundle.schema.v0.1.json | 27 +++ schemas/replay-artifact.schema.v0.1.json | 5 + schemas/run-artifact.schema.v0.1.json | 5 + scripts/emit_replay_artifact.py | 30 +++ scripts/emit_run_artifact.py | 30 +++ scripts/validate_bundle.py | 76 ++++++- ...nos-carrier-bundle.missing-replay-ref.json | 43 ++++ ...undle.non-authority-declaration-false.json | 44 ++++ ...arrier-bundle.owning-plane-agentplane.json | 44 ++++ .../valid.chronos-carrier-bundle.json | 44 ++++ tests/test_chronos_carrier_bridge.py | 193 ++++++++++++++++++ 15 files changed, 655 insertions(+), 3 deletions(-) create mode 100644 examples/run-replay/minimal-bundle-with-chronos-carrier.json create mode 100644 tests/fixtures/chronos-carrier/reject.chronos-carrier-bundle.missing-replay-ref.json create mode 100644 tests/fixtures/chronos-carrier/reject.chronos-carrier-bundle.non-authority-declaration-false.json create mode 100644 tests/fixtures/chronos-carrier/reject.chronos-carrier-bundle.owning-plane-agentplane.json create mode 100644 tests/fixtures/chronos-carrier/valid.chronos-carrier-bundle.json create mode 100644 tests/test_chronos_carrier_bridge.py diff --git a/.github/workflows/run-replay-artifacts.yml b/.github/workflows/run-replay-artifacts.yml index 9049cc6f..c2fa52fe 100644 --- a/.github/workflows/run-replay-artifacts.yml +++ b/.github/workflows/run-replay-artifacts.yml @@ -6,8 +6,12 @@ on: - "docs/sociosphere-bridge.md" - "schemas/run-artifact.schema.v0.1.json" - "schemas/replay-artifact.schema.v0.1.json" + - "schemas/bundle.schema.v0.1.json" - "scripts/emit_run_artifact.py" - "scripts/emit_replay_artifact.py" + - "scripts/validate_bundle.py" + - "tests/test_sourceos_binding_projection.py" + - "tests/test_chronos_carrier_bridge.py" - "examples/run-replay/**" - ".github/workflows/run-replay-artifacts.yml" push: @@ -17,8 +21,12 @@ on: - "docs/sociosphere-bridge.md" - "schemas/run-artifact.schema.v0.1.json" - "schemas/replay-artifact.schema.v0.1.json" + - "schemas/bundle.schema.v0.1.json" - "scripts/emit_run_artifact.py" - "scripts/emit_replay_artifact.py" + - "scripts/validate_bundle.py" + - "tests/test_sourceos_binding_projection.py" + - "tests/test_chronos_carrier_bridge.py" - "examples/run-replay/**" - ".github/workflows/run-replay-artifacts.yml" workflow_dispatch: @@ -38,7 +46,10 @@ jobs: run: | python3 -m json.tool schemas/run-artifact.schema.v0.1.json >/dev/null python3 -m json.tool schemas/replay-artifact.schema.v0.1.json >/dev/null - python3 -m py_compile scripts/emit_run_artifact.py scripts/emit_replay_artifact.py + python3 -m py_compile scripts/emit_run_artifact.py scripts/emit_replay_artifact.py scripts/validate_bundle.py + - name: Bridge binding/carrier projection unit tests + run: | + python3 -m unittest tests.test_sourceos_binding_projection tests.test_chronos_carrier_bridge -v - name: Emit synthetic run and replay artifacts run: | rm -rf /tmp/agentplane-run-replay-fixture @@ -46,3 +57,18 @@ jobs: python3 scripts/emit_replay_artifact.py examples/run-replay/minimal-bundle.json qemu --bundle-rev synthetic-rev --bundle-path examples/run-replay/minimal-bundle.json python3 -m json.tool /tmp/agentplane-run-replay-fixture/run-artifact.json >/dev/null python3 -m json.tool /tmp/agentplane-run-replay-fixture/replay-artifact.json >/dev/null + - name: Emit synthetic run and replay artifacts carrying a CHRONOS carrier + run: | + rm -rf /tmp/agentplane-run-replay-fixture-chronos-carrier + python3 scripts/emit_run_artifact.py examples/run-replay/minimal-bundle-with-chronos-carrier.json qemu 0 --stdout artifact:stdout --stderr artifact:stderr + python3 scripts/emit_replay_artifact.py examples/run-replay/minimal-bundle-with-chronos-carrier.json qemu --bundle-rev synthetic-rev --bundle-path examples/run-replay/minimal-bundle-with-chronos-carrier.json + python3 -m json.tool /tmp/agentplane-run-replay-fixture-chronos-carrier/run-artifact.json >/dev/null + python3 -m json.tool /tmp/agentplane-run-replay-fixture-chronos-carrier/replay-artifact.json >/dev/null + python3 -c " + import json + run = json.load(open('/tmp/agentplane-run-replay-fixture-chronos-carrier/run-artifact.json')) + replay = json.load(open('/tmp/agentplane-run-replay-fixture-chronos-carrier/replay-artifact.json')) + assert run['chronosCarrier']['methodFamily'] == 'neurasp-adjudication', run + assert replay['inputs']['chronosCarrier']['methodFamily'] == 'neurasp-adjudication', replay + print('chronosCarrier passthrough OK') + " diff --git a/Makefile b/Makefile index 1514eb7a..fc8f089f 100644 --- a/Makefile +++ b/Makefile @@ -294,6 +294,7 @@ validate-wallguard-collaboration-admission: test: python3 -m pytest -q tools/tests + .PHONY: validate-workspace-prophet-control-receipt validate-prophet-mesh-agentplane-adapter validate-workspace-prophet-control-receipt: python3 tools/validate_workspace_prophet_control_receipt.py @@ -401,3 +402,12 @@ validate: validate-composition-promotion-gate .PHONY: validate-composition-promotion-gate validate-composition-promotion-gate: python3 tools/validate_composition_promotion_gate.py + +# --- CHRONOS carrier passthrough (sociosphere-bridge additive extension, issue #329) --- +validate: validate-chronos-carrier-bridge +.PHONY: validate-chronos-carrier-bridge +validate-chronos-carrier-bridge: + python3 -m json.tool schemas/run-artifact.schema.v0.1.json >/dev/null + python3 -m json.tool schemas/replay-artifact.schema.v0.1.json >/dev/null + python3 -m json.tool schemas/bundle.schema.v0.1.json >/dev/null + python3 -m unittest tests.test_sourceos_binding_projection tests.test_chronos_carrier_bridge -v diff --git a/docs/sociosphere-bridge.md b/docs/sociosphere-bridge.md index 99c1bdd1..9e095e34 100644 --- a/docs/sociosphere-bridge.md +++ b/docs/sociosphere-bridge.md @@ -28,3 +28,47 @@ These are references only. `agentplane` must not rescan the workspace to redisco ## Non-goals - `agentplane` is not the source of truth for repo inventory or lock drift. - `sociosphere` is not the source of truth for executor placement or runtime replay artifacts. + +## CHRONOS carrier passthrough (additive) + +This bridge also accepts a second carried-object type: CHRONOS neuro-symbolic +carrier objects (per `sociosphere/docs/integration/neurosymbolic-chronos-alignment.md`). +This is the same seam widened to a second shape, not a second bridge — it reuses +the existing transport (declared refs on the `Bundle` spec, projected by the same +extraction functions that already project SourceOS bindings) and the same +consuming artifacts (`ValidationArtifact`, `RunArtifact`, `ReplayArtifact`). + +A `Bundle` may optionally declare `spec.chronosCarrier`: + +| Field | Meaning | +|---|---| +| `sourceEvidenceRef` | Reference to the upstream evidence the carrier grounds on | +| `methodFamily` | CHRONOS-owned method-family tag (e.g. NeurASP-style, dILP-style) | +| `claimStatus` | CHRONOS-owned claim status for the carrier | +| `validationStatus` | CHRONOS-owned validation status for the carrier | +| `nonAuthorityDeclaration` | Must be `true`: an explicit declaration that `agentplane` does not assert canonical authority over this carrier | +| `owningPlane` | The plane that owns this carrier's canonical definition (must not be `agentplane`) | +| `replayRef` | Reference agentplane can fold into its own `ReplayArtifact` | + +`scripts/validate_bundle.py` fail-closes (like the existing SourceOS +image-production gate) when `spec.chronosCarrier` is declared but incomplete, +when `nonAuthorityDeclaration` is not `true`, or when `owningPlane` is +`agentplane` — i.e. an improperly-authorized carrier (one that omits or denies +its non-authority declaration, or that tries to route canonical ownership +through `agentplane`) is rejected at the bridge, the same way a malformed +SourceOS binding is rejected today. + +`scripts/emit_run_artifact.py` and `scripts/emit_replay_artifact.py` project +whatever `chronosCarrier` fields are present into `RunArtifact.chronosCarrier` +and `ReplayArtifact.inputs.chronosCarrier` respectively, exactly as they already +do for `sourceosBindings`. + +### Non-goals (CHRONOS carrier passthrough) +- `agentplane` does not take on carrier-schema or method-family taxonomy + authority — `methodFamily`, `claimStatus`, and `validationStatus` values are + passed through, not interpreted or enumerated here. +- This is a structural completeness/non-authority gate only, not cryptographic + attestation or full authority/delegation reconstruction (see + `docs/replay-boundary.md`). +- No canonical-schema authority moves into `sociosphere` or `agentplane`; it + stays with Ontogenesis / `sourceos-spec` per CHRONOS's own definitions. diff --git a/examples/run-replay/minimal-bundle-with-chronos-carrier.json b/examples/run-replay/minimal-bundle-with-chronos-carrier.json new file mode 100644 index 00000000..9fdc8a36 --- /dev/null +++ b/examples/run-replay/minimal-bundle-with-chronos-carrier.json @@ -0,0 +1,35 @@ +{ + "metadata": { + "name": "minimal-run-replay-fixture-chronos-carrier", + "version": "0.1.0" + }, + "spec": { + "artifacts": { + "outDir": "/tmp/agentplane-run-replay-fixture-chronos-carrier" + }, + "policy": { + "lane": "staging", + "policyPackRef": "policy-pack:synthetic-run-replay", + "policyPackHash": "sha256:1111111111111111111111111111111111111111111111111111111111111111" + }, + "vm": { + "backendIntent": "qemu" + }, + "secrets": { + "required": [] + }, + "chronosCarrier": { + "sourceEvidenceRef": "urn:chronos:evidence:neurasp-adjudication-0007", + "methodFamily": "neurasp-adjudication", + "claimStatus": "proposed", + "validationStatus": "unvalidated", + "nonAuthorityDeclaration": true, + "owningPlane": "ontogenesis", + "replayRef": "urn:chronos:replay:neurasp-adjudication-0007" + }, + "governanceContext": { + "fixture": "run-replay-artifact-validation-chronos-carrier", + "nonProductionOnly": true + } + } +} diff --git a/schemas/bundle.schema.v0.1.json b/schemas/bundle.schema.v0.1.json index 54748ccf..374ec1ef 100644 --- a/schemas/bundle.schema.v0.1.json +++ b/schemas/bundle.schema.v0.1.json @@ -251,6 +251,33 @@ }, "type": "object" }, + "chronosCarrier": { + "description": "Optional CHRONOS neuro-symbolic carrier passthrough (additive bridge extension; agentplane does not own this taxonomy -- see docs/sociosphere-bridge.md).", + "properties": { + "sourceEvidenceRef": { + "type": "string" + }, + "methodFamily": { + "type": "string" + }, + "claimStatus": { + "type": "string" + }, + "validationStatus": { + "type": "string" + }, + "nonAuthorityDeclaration": { + "type": "boolean" + }, + "owningPlane": { + "type": "string" + }, + "replayRef": { + "type": "string" + } + }, + "type": "object" + }, "agentMachine": { "description": "Optional SourceOS Agent Machine binding for local Mac/Windows/Linux Podman workspaces and cluster-local TopoLVM placement.", "properties": { diff --git a/schemas/replay-artifact.schema.v0.1.json b/schemas/replay-artifact.schema.v0.1.json index 0317bbb9..540ff456 100644 --- a/schemas/replay-artifact.schema.v0.1.json +++ b/schemas/replay-artifact.schema.v0.1.json @@ -47,6 +47,11 @@ "sourceosImageProduction": { "type": "object", "additionalProperties": true + }, + "chronosCarrier": { + "description": "CHRONOS neuro-symbolic carrier passthrough fields projected from Bundle.spec.chronosCarrier when present (additive bridge extension; see docs/sociosphere-bridge.md).", + "type": "object", + "additionalProperties": true } } } diff --git a/schemas/run-artifact.schema.v0.1.json b/schemas/run-artifact.schema.v0.1.json index 1bd54f35..dca6ab48 100644 --- a/schemas/run-artifact.schema.v0.1.json +++ b/schemas/run-artifact.schema.v0.1.json @@ -40,6 +40,11 @@ "sourceosImageProduction": { "type": "object", "additionalProperties": true + }, + "chronosCarrier": { + "description": "CHRONOS neuro-symbolic carrier passthrough fields projected from Bundle.spec.chronosCarrier when present (additive bridge extension; see docs/sociosphere-bridge.md).", + "type": "object", + "additionalProperties": true } } } diff --git a/scripts/emit_replay_artifact.py b/scripts/emit_replay_artifact.py index fea134cb..f81a4724 100644 --- a/scripts/emit_replay_artifact.py +++ b/scripts/emit_replay_artifact.py @@ -48,6 +48,20 @@ "smokeReceiptRef": "AGENTPLANE_SOURCEOS_SMOKE_RECEIPT_REF", } +# CHRONOS neuro-symbolic carrier passthrough (additive to this bridge; see +# docs/sociosphere-bridge.md "CHRONOS carrier passthrough"). This is a +# permissive projection -- the fail-closed structural gate lives in +# scripts/validate_bundle.py, which runs upstream of this script. +CHRONOS_CARRIER_KEYS = ( + "sourceEvidenceRef", + "methodFamily", + "claimStatus", + "validationStatus", + "nonAuthorityDeclaration", + "owningPlane", + "replayRef", +) + def die(msg: str, code: int = 2) -> None: print(f"[replay-artifact] ERROR: {msg}", file=sys.stderr) @@ -174,6 +188,21 @@ def extract_sourceos_image_production(spec: dict[str, Any]) -> dict[str, Any]: } +def extract_chronos_carrier(spec: dict[str, Any]) -> dict[str, Any]: + """Project a declared CHRONOS carrier's fields, if present. + + Permissive by design: the fail-closed structural gate (required fields, + non-authority declaration, owning-plane check) lives in + scripts/validate_bundle.py and runs before this script does. This function + only records whatever was declared, the same way extract_sourceos_bindings + records whatever SourceOS binding was declared. + """ + carrier = spec.get("chronosCarrier") if isinstance(spec.get("chronosCarrier"), dict) else {} + if not carrier: + return {} + return _copy_non_empty(carrier, CHRONOS_CARRIER_KEYS) + + def main() -> int: ap = argparse.ArgumentParser(prog="emit_replay_artifact") ap.add_argument("bundle", help="path to bundle.json") @@ -225,6 +254,7 @@ def main() -> int: "upstreamArtifacts": upstream, "sourceosBindings": extract_sourceos_bindings(spec), "sourceosImageProduction": extract_sourceos_image_production(spec), + "chronosCarrier": extract_chronos_carrier(spec), } if governance_context is not None: inputs["governanceContext"] = governance_context diff --git a/scripts/emit_run_artifact.py b/scripts/emit_run_artifact.py index 19fc2675..82e840e1 100644 --- a/scripts/emit_run_artifact.py +++ b/scripts/emit_run_artifact.py @@ -49,6 +49,20 @@ "smokeReceiptRef": "AGENTPLANE_SOURCEOS_SMOKE_RECEIPT_REF", } +# CHRONOS neuro-symbolic carrier passthrough (additive to this bridge; see +# docs/sociosphere-bridge.md "CHRONOS carrier passthrough"). This is a +# permissive projection -- the fail-closed structural gate lives in +# scripts/validate_bundle.py, which runs upstream of this script. +CHRONOS_CARRIER_KEYS = ( + "sourceEvidenceRef", + "methodFamily", + "claimStatus", + "validationStatus", + "nonAuthorityDeclaration", + "owningPlane", + "replayRef", +) + def die(msg: str, code: int = 2) -> None: print(f"[run-artifact] ERROR: {msg}", file=sys.stderr) @@ -183,6 +197,21 @@ def extract_sourceos_image_production(spec: dict[str, Any]) -> dict[str, Any]: } +def extract_chronos_carrier(spec: dict[str, Any]) -> dict[str, Any]: + """Project a declared CHRONOS carrier's fields, if present. + + Permissive by design: the fail-closed structural gate (required fields, + non-authority declaration, owning-plane check) lives in + scripts/validate_bundle.py and runs before this script does. This function + only records whatever was declared, the same way extract_sourceos_bindings + records whatever SourceOS binding was declared. + """ + carrier = spec.get("chronosCarrier") if isinstance(spec.get("chronosCarrier"), dict) else {} + if not carrier: + return {} + return _copy_non_empty(carrier, CHRONOS_CARRIER_KEYS) + + def main() -> int: ap = argparse.ArgumentParser(prog="emit_run_artifact") ap.add_argument("bundle", help="path to bundle.json") @@ -241,6 +270,7 @@ def main() -> int: "upstreamArtifacts": upstream, "sourceosBindings": extract_sourceos_bindings(spec), "sourceosImageProduction": extract_sourceos_image_production(spec), + "chronosCarrier": extract_chronos_carrier(spec), } if governance_context is not None: diff --git a/scripts/validate_bundle.py b/scripts/validate_bundle.py index daa925c7..3577def1 100644 --- a/scripts/validate_bundle.py +++ b/scripts/validate_bundle.py @@ -15,6 +15,19 @@ "remoteExecutionProtocolRef", } +# CHRONOS neuro-symbolic carrier passthrough (additive to this bridge; see +# docs/sociosphere-bridge.md "CHRONOS carrier passthrough"). agentplane does not +# own this taxonomy -- these keys are structural only. +CHRONOS_CARRIER_REQUIRED_KEYS = ( + "sourceEvidenceRef", + "methodFamily", + "claimStatus", + "validationStatus", + "nonAuthorityDeclaration", + "owningPlane", + "replayRef", +) + def die(msg: str, code: int = 2) -> None: print(f"[validate] ERROR: {msg}", file=sys.stderr) @@ -27,11 +40,13 @@ def _require_mapping(obj, path: str): return obj -def _require_non_empty(obj: dict, path: str, keys: tuple[str, ...]) -> None: +def _require_non_empty( + obj: dict, path: str, keys: tuple[str, ...], reason: str = "for SourceOS image-production bundles" +) -> None: for key in keys: value = obj.get(key) if value is None or value == "" or value == []: - die(f"{path}.{key} is required for SourceOS image-production bundles", 2) + die(f"{path}.{key} is required {reason}", 2) def extract_sourceos_bindings(spec: dict) -> dict: @@ -143,6 +158,61 @@ def validate_sourceos_image_production(spec: dict) -> dict: } +def validate_chronos_carrier(spec: dict) -> dict: + """Validate the optional CHRONOS neuro-symbolic carrier passthrough lane. + + The lane is intentionally optional so existing bundles (and the existing + SourceOS / sociosphere-workspace bridge lanes) continue to pass unchanged. + When a bundle declares `spec.chronosCarrier`, we fail closed unless the + structural fields the evidence/replay surface needs are present, and unless + the carrier is explicitly self-declared as not under agentplane's authority. + This does not validate or enumerate CHRONOS's own method-family / claim / + validation taxonomy -- those values are opaque passthrough here. + """ + if "chronosCarrier" not in spec: + return {"enabled": False, "result": "not_applicable"} + + carrier = _require_mapping(spec.get("chronosCarrier") or {}, "spec.chronosCarrier") + _require_non_empty( + carrier, + "spec.chronosCarrier", + CHRONOS_CARRIER_REQUIRED_KEYS, + reason="when spec.chronosCarrier is declared (CHRONOS carrier passthrough)", + ) + + if carrier.get("nonAuthorityDeclaration") is not True: + die( + "spec.chronosCarrier.nonAuthorityDeclaration must be true: agentplane must " + "explicitly disclaim canonical authority over any CHRONOS carrier it carries", + 2, + ) + + owning_plane = carrier.get("owningPlane") + if not isinstance(owning_plane, str) or owning_plane.strip().lower() == "agentplane": + die( + "spec.chronosCarrier.owningPlane must name a plane other than agentplane " + "(agentplane is a carrier surface, not the carrier's owning authority)", + 2, + ) + + for key in ("sourceEvidenceRef", "methodFamily", "claimStatus", "validationStatus", "replayRef"): + value = carrier.get(key) + if not isinstance(value, str) or not value.strip(): + die(f"spec.chronosCarrier.{key} must be a non-empty string", 2) + + return { + "enabled": True, + "result": "pass", + "sourceEvidenceRef": carrier.get("sourceEvidenceRef"), + "methodFamily": carrier.get("methodFamily"), + "claimStatus": carrier.get("claimStatus"), + "validationStatus": carrier.get("validationStatus"), + "nonAuthorityDeclaration": carrier.get("nonAuthorityDeclaration"), + "owningPlane": owning_plane, + "replayRef": carrier.get("replayRef"), + } + + def main() -> int: if len(sys.argv) != 2: die("usage: scripts/validate_bundle.py ", 2) @@ -181,6 +251,7 @@ def main() -> int: sourceos_bindings = extract_sourceos_bindings(spec) sourceos_image_production_gate = validate_sourceos_image_production(spec) + chronos_carrier_gate = validate_chronos_carrier(spec) pol = spec.get("policy") or {} mrs = pol.get("maxRunSeconds") @@ -256,6 +327,7 @@ def main() -> int: "result": "pass", "sourceosBindings": sourceos_bindings, "sourceosImageProductionGate": sourceos_image_production_gate, + "chronosCarrierGate": chronos_carrier_gate, "controlGate": { "result": gate_artifact["result"], "reason": gate_artifact["reason"], diff --git a/tests/fixtures/chronos-carrier/reject.chronos-carrier-bundle.missing-replay-ref.json b/tests/fixtures/chronos-carrier/reject.chronos-carrier-bundle.missing-replay-ref.json new file mode 100644 index 00000000..2b0ceb4d --- /dev/null +++ b/tests/fixtures/chronos-carrier/reject.chronos-carrier-bundle.missing-replay-ref.json @@ -0,0 +1,43 @@ +{ + "apiVersion": "agentplane.socioprophet.org/v0.1", + "kind": "Bundle", + "metadata": { + "createdAt": "2026-08-02T00:00:00-05:00", + "licensePolicy": { + "allowAGPL": false + }, + "name": "chronos-carrier-bridge-fixture", + "version": "0.1.0" + }, + "spec": { + "artifacts": { + "outDir": "/tmp/agentplane-chronos-carrier-bridge-fixture" + }, + "chronosCarrier": { + "sourceEvidenceRef": "urn:chronos:evidence:neurasp-adjudication-0007", + "methodFamily": "neurasp-adjudication", + "claimStatus": "proposed", + "validationStatus": "unvalidated", + "nonAuthorityDeclaration": true, + "owningPlane": "ontogenesis" + }, + "policy": { + "failOnTimeout": true, + "humanGateRequired": false, + "lane": "staging", + "maxRunSeconds": 20, + "policyPackHash": "UNSET", + "policyPackRef": "policy-packs/dev/default" + }, + "secrets": { + "required": [] + }, + "smoke": { + "script": "bundles/example-agent/smoke.sh" + }, + "vm": { + "backendIntent": "lima-process", + "modulePath": "bundles/example-agent/vm.nix" + } + } +} \ No newline at end of file diff --git a/tests/fixtures/chronos-carrier/reject.chronos-carrier-bundle.non-authority-declaration-false.json b/tests/fixtures/chronos-carrier/reject.chronos-carrier-bundle.non-authority-declaration-false.json new file mode 100644 index 00000000..181752aa --- /dev/null +++ b/tests/fixtures/chronos-carrier/reject.chronos-carrier-bundle.non-authority-declaration-false.json @@ -0,0 +1,44 @@ +{ + "apiVersion": "agentplane.socioprophet.org/v0.1", + "kind": "Bundle", + "metadata": { + "createdAt": "2026-08-02T00:00:00-05:00", + "licensePolicy": { + "allowAGPL": false + }, + "name": "chronos-carrier-bridge-fixture", + "version": "0.1.0" + }, + "spec": { + "artifacts": { + "outDir": "/tmp/agentplane-chronos-carrier-bridge-fixture" + }, + "chronosCarrier": { + "sourceEvidenceRef": "urn:chronos:evidence:neurasp-adjudication-0007", + "methodFamily": "neurasp-adjudication", + "claimStatus": "proposed", + "validationStatus": "unvalidated", + "nonAuthorityDeclaration": false, + "owningPlane": "ontogenesis", + "replayRef": "urn:chronos:replay:neurasp-adjudication-0007" + }, + "policy": { + "failOnTimeout": true, + "humanGateRequired": false, + "lane": "staging", + "maxRunSeconds": 20, + "policyPackHash": "UNSET", + "policyPackRef": "policy-packs/dev/default" + }, + "secrets": { + "required": [] + }, + "smoke": { + "script": "bundles/example-agent/smoke.sh" + }, + "vm": { + "backendIntent": "lima-process", + "modulePath": "bundles/example-agent/vm.nix" + } + } +} \ No newline at end of file diff --git a/tests/fixtures/chronos-carrier/reject.chronos-carrier-bundle.owning-plane-agentplane.json b/tests/fixtures/chronos-carrier/reject.chronos-carrier-bundle.owning-plane-agentplane.json new file mode 100644 index 00000000..9f5304cb --- /dev/null +++ b/tests/fixtures/chronos-carrier/reject.chronos-carrier-bundle.owning-plane-agentplane.json @@ -0,0 +1,44 @@ +{ + "apiVersion": "agentplane.socioprophet.org/v0.1", + "kind": "Bundle", + "metadata": { + "createdAt": "2026-08-02T00:00:00-05:00", + "licensePolicy": { + "allowAGPL": false + }, + "name": "chronos-carrier-bridge-fixture", + "version": "0.1.0" + }, + "spec": { + "artifacts": { + "outDir": "/tmp/agentplane-chronos-carrier-bridge-fixture" + }, + "chronosCarrier": { + "sourceEvidenceRef": "urn:chronos:evidence:neurasp-adjudication-0007", + "methodFamily": "neurasp-adjudication", + "claimStatus": "proposed", + "validationStatus": "unvalidated", + "nonAuthorityDeclaration": true, + "owningPlane": "agentplane", + "replayRef": "urn:chronos:replay:neurasp-adjudication-0007" + }, + "policy": { + "failOnTimeout": true, + "humanGateRequired": false, + "lane": "staging", + "maxRunSeconds": 20, + "policyPackHash": "UNSET", + "policyPackRef": "policy-packs/dev/default" + }, + "secrets": { + "required": [] + }, + "smoke": { + "script": "bundles/example-agent/smoke.sh" + }, + "vm": { + "backendIntent": "lima-process", + "modulePath": "bundles/example-agent/vm.nix" + } + } +} \ No newline at end of file diff --git a/tests/fixtures/chronos-carrier/valid.chronos-carrier-bundle.json b/tests/fixtures/chronos-carrier/valid.chronos-carrier-bundle.json new file mode 100644 index 00000000..569243b4 --- /dev/null +++ b/tests/fixtures/chronos-carrier/valid.chronos-carrier-bundle.json @@ -0,0 +1,44 @@ +{ + "apiVersion": "agentplane.socioprophet.org/v0.1", + "kind": "Bundle", + "metadata": { + "createdAt": "2026-08-02T00:00:00-05:00", + "licensePolicy": { + "allowAGPL": false + }, + "name": "chronos-carrier-bridge-fixture", + "version": "0.1.0" + }, + "spec": { + "artifacts": { + "outDir": "/tmp/agentplane-chronos-carrier-bridge-fixture" + }, + "chronosCarrier": { + "sourceEvidenceRef": "urn:chronos:evidence:neurasp-adjudication-0007", + "methodFamily": "neurasp-adjudication", + "claimStatus": "proposed", + "validationStatus": "unvalidated", + "nonAuthorityDeclaration": true, + "owningPlane": "ontogenesis", + "replayRef": "urn:chronos:replay:neurasp-adjudication-0007" + }, + "policy": { + "failOnTimeout": true, + "humanGateRequired": false, + "lane": "staging", + "maxRunSeconds": 20, + "policyPackHash": "UNSET", + "policyPackRef": "policy-packs/dev/default" + }, + "secrets": { + "required": [] + }, + "smoke": { + "script": "bundles/example-agent/smoke.sh" + }, + "vm": { + "backendIntent": "lima-process", + "modulePath": "bundles/example-agent/vm.nix" + } + } +} diff --git a/tests/test_chronos_carrier_bridge.py b/tests/test_chronos_carrier_bridge.py new file mode 100644 index 00000000..a54dd207 --- /dev/null +++ b/tests/test_chronos_carrier_bridge.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 +"""Coverage for the CHRONOS carrier passthrough extension to the sociosphere bridge. + +This is additive coverage alongside tests/test_sourceos_binding_projection.py: +same three modules (validate_bundle, emit_run_artifact, emit_replay_artifact), +same bridge, a second carried-object type. See docs/sociosphere-bridge.md +("CHRONOS carrier passthrough") and issue #329. +""" +from __future__ import annotations + +import subprocess +import sys +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +SCRIPTS = ROOT / "scripts" +FIXTURES = ROOT / "tests" / "fixtures" / "chronos-carrier" +sys.path.insert(0, str(SCRIPTS)) + +import emit_replay_artifact # noqa: E402 +import emit_run_artifact # noqa: E402 +import validate_bundle # noqa: E402 + +VALID_CARRIER = { + "sourceEvidenceRef": "urn:chronos:evidence:neurasp-adjudication-0007", + "methodFamily": "neurasp-adjudication", + "claimStatus": "proposed", + "validationStatus": "unvalidated", + "nonAuthorityDeclaration": True, + "owningPlane": "ontogenesis", + "replayRef": "urn:chronos:replay:neurasp-adjudication-0007", +} + +# SourceOS binding fixture reused verbatim from test_sourceos_binding_projection.py +# to prove the two carried-object types coexist without interference. +SOURCEOS_BINDING = { + "contentSpecRef": "urn:srcos:content-spec:sourceos-workstation", + "evidenceBundleRef": "urn:srcos:evidence-bundle:sourceos-workstation-dev-0001", +} + + +class ChronosCarrierGateTests(unittest.TestCase): + """scripts/validate_bundle.py's fail-closed structural gate.""" + + def test_absent_carrier_is_not_applicable(self) -> None: + result = validate_bundle.validate_chronos_carrier({}) + self.assertEqual(result, {"enabled": False, "result": "not_applicable"}) + + def test_valid_carrier_passes_and_projects_fields(self) -> None: + spec = {"chronosCarrier": dict(VALID_CARRIER)} + result = validate_bundle.validate_chronos_carrier(spec) + self.assertEqual(result["enabled"], True) + self.assertEqual(result["result"], "pass") + for key, value in VALID_CARRIER.items(): + self.assertEqual(result[key], value) + + def test_missing_required_field_rejected(self) -> None: + for missing_key in validate_bundle.CHRONOS_CARRIER_REQUIRED_KEYS: + carrier = dict(VALID_CARRIER) + del carrier[missing_key] + spec = {"chronosCarrier": carrier} + with self.assertRaises(SystemExit, msg=f"expected rejection when {missing_key} is missing"): + validate_bundle.validate_chronos_carrier(spec) + + def test_non_authority_declaration_false_rejected(self) -> None: + """An improperly-authorized carrier: it does not disclaim agentplane authority.""" + carrier = dict(VALID_CARRIER) + carrier["nonAuthorityDeclaration"] = False + spec = {"chronosCarrier": carrier} + with self.assertRaises(SystemExit): + validate_bundle.validate_chronos_carrier(spec) + + def test_owning_plane_agentplane_rejected(self) -> None: + """An improperly-authorized carrier: it tries to route canonical ownership through agentplane.""" + carrier = dict(VALID_CARRIER) + carrier["owningPlane"] = "agentplane" + spec = {"chronosCarrier": carrier} + with self.assertRaises(SystemExit): + validate_bundle.validate_chronos_carrier(spec) + + def test_owning_plane_agentplane_case_insensitive_rejected(self) -> None: + carrier = dict(VALID_CARRIER) + carrier["owningPlane"] = "AgentPlane" + spec = {"chronosCarrier": carrier} + with self.assertRaises(SystemExit): + validate_bundle.validate_chronos_carrier(spec) + + def test_non_string_method_family_rejected(self) -> None: + carrier = dict(VALID_CARRIER) + carrier["methodFamily"] = 12345 + spec = {"chronosCarrier": carrier} + with self.assertRaises(SystemExit): + validate_bundle.validate_chronos_carrier(spec) + + +class ChronosCarrierEmitProjectionTests(unittest.TestCase): + """scripts/emit_run_artifact.py and scripts/emit_replay_artifact.py permissive projection.""" + + def test_valid_carrier_projects_consistently_across_emitters(self) -> None: + spec = {"chronosCarrier": dict(VALID_CARRIER)} + + self.assertEqual(emit_run_artifact.extract_chronos_carrier(spec), VALID_CARRIER) + self.assertEqual(emit_replay_artifact.extract_chronos_carrier(spec), VALID_CARRIER) + + def test_empty_carrier_projects_to_empty_object(self) -> None: + spec: dict = {} + + self.assertEqual(emit_run_artifact.extract_chronos_carrier(spec), {}) + self.assertEqual(emit_replay_artifact.extract_chronos_carrier(spec), {}) + + def test_emitters_do_not_fail_closed(self) -> None: + """The fail-closed gate lives only in validate_bundle; emitters just record facts, + matching the existing extract_sourceos_image_production permissive-collector pattern.""" + carrier = dict(VALID_CARRIER) + del carrier["nonAuthorityDeclaration"] + spec = {"chronosCarrier": carrier} + + # Must not raise, unlike validate_bundle.validate_chronos_carrier on the same input. + run_projection = emit_run_artifact.extract_chronos_carrier(spec) + replay_projection = emit_replay_artifact.extract_chronos_carrier(spec) + self.assertEqual(run_projection["methodFamily"], VALID_CARRIER["methodFamily"]) + self.assertEqual(replay_projection["methodFamily"], VALID_CARRIER["methodFamily"]) + + +class SupersetTests(unittest.TestCase): + """Confirm the widened bridge still carries the pre-existing artifact type unchanged.""" + + def test_sourceos_binding_and_chronos_carrier_coexist(self) -> None: + spec = { + "integrationRefs": {"sourceos": dict(SOURCEOS_BINDING)}, + "chronosCarrier": dict(VALID_CARRIER), + } + + # Pre-existing SourceOS projection is untouched by the new carrier field. + self.assertEqual(validate_bundle.extract_sourceos_bindings(spec), SOURCEOS_BINDING) + self.assertEqual(emit_run_artifact.extract_sourceos_bindings(spec), SOURCEOS_BINDING) + self.assertEqual(emit_replay_artifact.extract_sourceos_bindings(spec), SOURCEOS_BINDING) + + # New CHRONOS carrier projection is present alongside it. + gate = validate_bundle.validate_chronos_carrier(spec) + self.assertEqual(gate["result"], "pass") + self.assertEqual(emit_run_artifact.extract_chronos_carrier(spec), VALID_CARRIER) + self.assertEqual(emit_replay_artifact.extract_chronos_carrier(spec), VALID_CARRIER) + + def test_bundle_with_only_sourceos_binding_has_not_applicable_chronos_gate(self) -> None: + spec = {"integrationRefs": {"sourceos": dict(SOURCEOS_BINDING)}} + + gate = validate_bundle.validate_chronos_carrier(spec) + self.assertEqual(gate, {"enabled": False, "result": "not_applicable"}) + self.assertEqual(emit_run_artifact.extract_chronos_carrier(spec), {}) + self.assertEqual(emit_replay_artifact.extract_chronos_carrier(spec), {}) + + +class ChronosCarrierCliFixtureTests(unittest.TestCase): + """End-to-end CLI coverage: scripts/validate_bundle.py against full Bundle fixtures, + following this repo's valid.*/reject.* fixture convention (see e.g. + tests/fixtures/reviews/, Makefile's `!`-prefixed negative-case targets).""" + + def _run_validate(self, fixture_name: str) -> subprocess.CompletedProcess: + fixture = FIXTURES / fixture_name + self.assertTrue(fixture.exists(), f"missing fixture: {fixture}") + return subprocess.run( + [sys.executable, str(SCRIPTS / "validate_bundle.py"), str(fixture)], + capture_output=True, + text=True, + ) + + def test_valid_chronos_carrier_bundle_passes(self) -> None: + result = self._run_validate("valid.chronos-carrier-bundle.json") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("[validate] OK", result.stdout) + + def test_reject_non_authority_declaration_false(self) -> None: + """Improperly-authorized carrier: does not disclaim agentplane authority.""" + result = self._run_validate("reject.chronos-carrier-bundle.non-authority-declaration-false.json") + self.assertEqual(result.returncode, 2) + self.assertIn("nonAuthorityDeclaration must be true", result.stderr) + + def test_reject_owning_plane_agentplane(self) -> None: + """Improperly-authorized carrier: routes canonical ownership through agentplane.""" + result = self._run_validate("reject.chronos-carrier-bundle.owning-plane-agentplane.json") + self.assertEqual(result.returncode, 2) + self.assertIn("owningPlane must name a plane other than agentplane", result.stderr) + + def test_reject_missing_replay_ref(self) -> None: + result = self._run_validate("reject.chronos-carrier-bundle.missing-replay-ref.json") + self.assertEqual(result.returncode, 2) + self.assertIn("spec.chronosCarrier.replayRef is required", result.stderr) + + +if __name__ == "__main__": + unittest.main() From 1154879bd5de782b0d3aef96435be343a36fe4bf Mon Sep 17 00:00:00 2001 From: Michael Heller <21163552+mdheller@users.noreply.github.com> Date: Sun, 2 Aug 2026 04:49:07 -0400 Subject: [PATCH 2/2] fix(ci): harden run-replay-artifacts workflow per Copilot review on #330 - Convert the inline python3 -c "..." block scalar to the repo's established python3 - <<'PY' heredoc convention (already used in ci.yml/lint.yml). The quoted -c string relied on every physical line sharing identical literal indentation so YAML's block-scalar stripping would land it flush-left; that held today but was one careless edit away from an IndentationError. The heredoc form is the same normalization mechanism the rest of the repo already uses and removes the fragility Copilot flagged. - Add a python3 -m json.tool check for schemas/bundle.schema.v0.1.json in the schema-validation step, since this workflow is triggered by changes to that file but wasn't parsing it for syntax errors. --- .github/workflows/run-replay-artifacts.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run-replay-artifacts.yml b/.github/workflows/run-replay-artifacts.yml index c2fa52fe..4132c629 100644 --- a/.github/workflows/run-replay-artifacts.yml +++ b/.github/workflows/run-replay-artifacts.yml @@ -46,6 +46,7 @@ jobs: run: | python3 -m json.tool schemas/run-artifact.schema.v0.1.json >/dev/null python3 -m json.tool schemas/replay-artifact.schema.v0.1.json >/dev/null + python3 -m json.tool schemas/bundle.schema.v0.1.json >/dev/null python3 -m py_compile scripts/emit_run_artifact.py scripts/emit_replay_artifact.py scripts/validate_bundle.py - name: Bridge binding/carrier projection unit tests run: | @@ -64,11 +65,11 @@ jobs: python3 scripts/emit_replay_artifact.py examples/run-replay/minimal-bundle-with-chronos-carrier.json qemu --bundle-rev synthetic-rev --bundle-path examples/run-replay/minimal-bundle-with-chronos-carrier.json python3 -m json.tool /tmp/agentplane-run-replay-fixture-chronos-carrier/run-artifact.json >/dev/null python3 -m json.tool /tmp/agentplane-run-replay-fixture-chronos-carrier/replay-artifact.json >/dev/null - python3 -c " + python3 - <<'PY' import json run = json.load(open('/tmp/agentplane-run-replay-fixture-chronos-carrier/run-artifact.json')) replay = json.load(open('/tmp/agentplane-run-replay-fixture-chronos-carrier/replay-artifact.json')) assert run['chronosCarrier']['methodFamily'] == 'neurasp-adjudication', run assert replay['inputs']['chronosCarrier']['methodFamily'] == 'neurasp-adjudication', replay print('chronosCarrier passthrough OK') - " + PY