diff --git a/.github/workflows/run-replay-artifacts.yml b/.github/workflows/run-replay-artifacts.yml index 9049cc6f..4132c629 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,11 @@ 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 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: | + 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 +58,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 - <<'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 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()