From 5dfd882ef4fcda96f4c9ef0798fcdc972113c04d Mon Sep 17 00:00:00 2001 From: azender1 Date: Wed, 30 Sep 2026 22:15:29 -0400 Subject: [PATCH 1/8] Enforce action-ref-v1 timestamp grammar --- safeagent_exec_guard/attestation_gate.py | 42 +++++++++++++++++++++--- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/safeagent_exec_guard/attestation_gate.py b/safeagent_exec_guard/attestation_gate.py index 101e2ac..e3389b1 100644 --- a/safeagent_exec_guard/attestation_gate.py +++ b/safeagent_exec_guard/attestation_gate.py @@ -21,6 +21,7 @@ from __future__ import annotations import hashlib +import re from base64 import urlsafe_b64decode from datetime import datetime, timezone from typing import Any @@ -30,6 +31,8 @@ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey _BLOCKING = ("critical", "high") +_ACTION_REF_FIELDS = {"agent_id", "action_type", "scope", "timestamp"} +_ACTION_REF_TS_RE = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$") def _parse_ts(value: str | None) -> datetime | None: @@ -52,7 +55,26 @@ def sha256hex(data: bytes) -> str: return hashlib.sha256(data).hexdigest() +def validate_action_ref_preimage(preimage: dict) -> None: + """Enforce the frozen action-ref-v1 four-field grammar before hashing.""" + if not isinstance(preimage, dict): + raise ValueError("action_ref preimage must be an object") + if set(preimage) != _ACTION_REF_FIELDS: + raise ValueError("action_ref preimage must contain exactly agent_id, action_type, scope, timestamp") + for field in ("agent_id", "action_type", "scope"): + if not isinstance(preimage[field], str): + raise ValueError(f"action_ref {field} must be a string") + timestamp = preimage["timestamp"] + if not isinstance(timestamp, str) or not _ACTION_REF_TS_RE.fullmatch(timestamp): + raise ValueError("action_ref timestamp must be YYYY-MM-DDTHH:MM:SS.mmmZ") + try: + datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%S.%fZ") + except ValueError as exc: + raise ValueError("action_ref timestamp is not a valid UTC instant") from exc + + def compute_action_ref(preimage: dict) -> str: + validate_action_ref_preimage(preimage) return sha256hex(jcs(preimage)) @@ -136,7 +158,6 @@ def gate( - Bad signature, binding mismatch, verdict != admit, or a critical/high finding: DENY. - Otherwise: ALLOW, carrying the action_ref for the exactly-once guard. """ - # availability stop — distinct from a safety stop if unreachable: if require_attestation: return {"decision": "DENY", "reason": "attestation_unreachable"} @@ -153,7 +174,6 @@ def gate( if not verify_signature(attestation, jwks): return {"decision": "DENY", "reason": "attestation_signature_invalid"} - # bind to THIS claim — an attestation can't be replayed against a different charge expected = compute_binding_digest(claim_binding_preimage) claimed = ((attestation.get("binding") or attestation.get("attestation_binding") or {}) .get("binding_digest")) @@ -161,7 +181,6 @@ def gate( return {"decision": "DENY", "reason": "binding_digest_mismatch", "expected": expected, "claimed": claimed} - # freshness — a valid, correctly-bound verdict that is no longer current must not admit if freshness_ttl_seconds is not None: now = now or datetime.now(timezone.utc) issued = _parse_ts(attestation.get("issued_at")) @@ -186,9 +205,22 @@ def gate( return {"decision": "DENY", "reason": f"safety_findings: {parts}", "entities": entities} action_ref_preimage = attestation.get("action_ref_preimage") - action_ref = compute_action_ref(action_ref_preimage) if action_ref_preimage else None + if action_ref_preimage: + try: + action_ref = compute_action_ref(action_ref_preimage) + except ValueError: + return {"decision": "DENY", "reason": "action_ref_preimage_invalid"} + else: + action_ref = None return {"decision": "ALLOW", "reason": "admit", "action_ref": action_ref, "binding_digest": expected} -__all__ = ["gate", "verify_signature", "compute_action_ref", "compute_binding_digest", "jcs"] +__all__ = [ + "gate", + "verify_signature", + "compute_action_ref", + "compute_binding_digest", + "validate_action_ref_preimage", + "jcs", +] From af36bce4cde3aeaff014673e7c65da8085e1cb6b Mon Sep 17 00:00:00 2001 From: azender1 Date: Wed, 30 Sep 2026 22:15:31 -0400 Subject: [PATCH 2/8] Use strict action-ref-v1 derivation for trails --- safeagent_exec_guard/mycelium_trail.py | 339 +++++++++++++------------ 1 file changed, 176 insertions(+), 163 deletions(-) diff --git a/safeagent_exec_guard/mycelium_trail.py b/safeagent_exec_guard/mycelium_trail.py index 58da0fd..b377404 100644 --- a/safeagent_exec_guard/mycelium_trail.py +++ b/safeagent_exec_guard/mycelium_trail.py @@ -1,163 +1,176 @@ -""" -mycelium_trail.py — Mycelium on-chain anchor for SafeAgent - -Submits a Nexus trail to Arbitrum via argentum-api.rgiskard.xyz/nexus/trail. -Returns trail_id (full UUID). Anchor confirmation polled via verify_chain. - -Used by main.py _submit_and_anchor background task on every /settle. -""" -from __future__ import annotations - -import asyncio -import hashlib -import json -import logging -import os -import time -from datetime import datetime, timezone -from typing import Any, Dict, Optional - -import httpx - -_log = logging.getLogger(__name__) - -MYCELIUM_API_KEY = os.environ.get("MYCELIUM_API_KEY", "") -MYCELIUM_AGENT_ID = os.environ.get("MYCELIUM_AGENT_ID", "safeagent-prod") -MYCELIUM_ENABLED = os.environ.get("MYCELIUM_ENABLED", "1") == "1" - -NEXUS_TRAIL_URL = "https://argentum.rgiskard.xyz/nexus/trail" -VERIFY_CHAIN_URL = "https://argentum.rgiskard.xyz/mycelium/trails/{trail_id}/verify_chain" - - -# --------------------------------------------------------------------------- -# Helpers -# --------------------------------------------------------------------------- - -def jcs(obj: dict) -> bytes: - """RFC 8785 canonical JSON — sorted keys, no whitespace.""" - return json.dumps( - obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False - ).encode("utf-8") - - -def sha256hex(data: bytes) -> str: - return hashlib.sha256(data).hexdigest() - - -def enabled() -> bool: - return MYCELIUM_ENABLED and bool(MYCELIUM_API_KEY) - - -# --------------------------------------------------------------------------- -# action_ref derivation — argentum-core action-ref-v1 -# --------------------------------------------------------------------------- - -def compute_action_ref(agent_id: str, action_type: str, scope: str, ts: int) -> str: - """SHA-256 of JCS({agent_id, action_type, scope, timestamp}).""" - preimage = { - "agent_id": agent_id, - "action_type": action_type, - "scope": scope, - "timestamp": ts, - } - return sha256hex(jcs(preimage)) - - -# --------------------------------------------------------------------------- -# Nexus trail submission -# --------------------------------------------------------------------------- - -async def submit_trail_async( - request_id: str, - action: str, - agent_id: Optional[str], - claimed_at: float, - result: Dict[str, Any], -) -> Optional[str]: - if not enabled(): - return None - - from datetime import datetime, timezone - _ct = claimed_at if claimed_at else time.time() - ts_str = datetime.fromtimestamp(_ct, tz=timezone.utc).strftime('%Y-%m-%dT%H:%M:%S.') + f'{int(_ct * 1000) % 1000:03d}Z' - _agent_id = agent_id or MYCELIUM_AGENT_ID - scope = request_id - - preimage = { - 'action_type': action, - 'agent_id': _agent_id, - 'scope': scope, - 'timestamp': ts_str, - } - action_ref = sha256hex(jcs(preimage)) - payment_hash = 'sha256-' + sha256hex(f'payment:{action_ref}'.encode()) - output_hash = 'sha256-' + sha256hex(json.dumps(result, sort_keys=True).encode()) - - payload = { - 'packet_version': '1.0', - 'canonicalization_profile_id': '8c7f71754e3daae1a0390d5e0287d51097d011e40df36bf15cad5c0f47efa05a', - 'action_ref': action_ref, - 'service': 'safeagent', - 'preimage': preimage, - 'payment_hash': payment_hash, - 'output_hash': output_hash, - 'hash_algo': 'SHA-256', - 'preimage_format': 'jcs', - 'timestamp': int(_ct * 1000), - 'api_key': MYCELIUM_API_KEY, - } - - try: - async with httpx.AsyncClient(timeout=30) as client: - resp = await client.post(NEXUS_TRAIL_URL, json=payload) - if resp.status_code not in (200, 201): - _log.warning('Mycelium trail error: %s', resp.text) - resp.raise_for_status() - data = resp.json() - trail_id = data.get('trail_id') or data.get('id') - if trail_id: - _log.info('Mycelium trail submitted: trail_id=%s action_ref=%s', trail_id, action_ref) - return trail_id - _log.warning('Mycelium trail: no trail_id in response: %s', data) - return None - except Exception as exc: - _log.warning('Mycelium trail submission failed: %s', exc) - return None - - -# --------------------------------------------------------------------------- -# Anchor polling — verify_chain -# --------------------------------------------------------------------------- - -async def get_trail_anchor(trail_id: str) -> Optional[Dict[str, Any]]: - """ - Poll verify_chain for Arbitrum confirmation. - Returns anchor dict with block_time and tx_hash when confirmed, else None. - """ - url = VERIFY_CHAIN_URL.format(trail_id=trail_id) - try: - async with httpx.AsyncClient(timeout=30) as client: - resp = await client.get(url) - if resp.status_code == 404: - return None - resp.raise_for_status() - data = resp.json() - block_time = ( - data.get("block_time") - or data.get("anchor_block_time") - or data.get("anchor", {}).get("block_time") - ) - if not block_time: - return None - return { - "block_time": block_time, - "tx_hash": ( - data.get("tx_hash") - or data.get("transaction_hash") - or data.get("anchor", {}).get("tx_hash") - ), - "trail_id": trail_id, - } - except Exception as exc: - _log.warning("Mycelium verify_chain failed for %s: %s", trail_id, exc) - return None +""" +mycelium_trail.py — Mycelium on-chain anchor for SafeAgent + +Submits a Nexus trail to Arbitrum via argentum-api.rgiskard.xyz/nexus/trail. +Returns trail_id (full UUID). Anchor confirmation polled via verify_chain. + +Used by main.py _submit_and_anchor background task on every /settle. +""" +from __future__ import annotations + +import asyncio +import hashlib +import json +import logging +import os +import re +import time +from datetime import datetime, timezone +from typing import Any, Dict, Optional + +import httpx + +_log = logging.getLogger(__name__) + +MYCELIUM_API_KEY = os.environ.get("MYCELIUM_API_KEY", "") +MYCELIUM_AGENT_ID = os.environ.get("MYCELIUM_AGENT_ID", "safeagent-prod") +MYCELIUM_ENABLED = os.environ.get("MYCELIUM_ENABLED", "1") == "1" + +NEXUS_TRAIL_URL = "https://argentum.rgiskard.xyz/nexus/trail" +VERIFY_CHAIN_URL = "https://argentum.rgiskard.xyz/mycelium/trails/{trail_id}/verify_chain" +_ACTION_REF_TS_RE = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$") + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def jcs(obj: dict) -> bytes: + """RFC 8785 canonical JSON — sorted keys, no whitespace.""" + return json.dumps( + obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False + ).encode("utf-8") + + +def sha256hex(data: bytes) -> str: + return hashlib.sha256(data).hexdigest() + + +def enabled() -> bool: + return MYCELIUM_ENABLED and bool(MYCELIUM_API_KEY) + + +# --------------------------------------------------------------------------- +# action_ref derivation — argentum-core action-ref-v1 +# --------------------------------------------------------------------------- + +def _validate_action_ref_timestamp(timestamp: str) -> None: + if not isinstance(timestamp, str) or not _ACTION_REF_TS_RE.fullmatch(timestamp): + raise ValueError("action_ref timestamp must be YYYY-MM-DDTHH:MM:SS.mmmZ") + try: + datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%S.%fZ") + except ValueError as exc: + raise ValueError("action_ref timestamp is not a valid UTC instant") from exc + + +def compute_action_ref(agent_id: str, action_type: str, scope: str, ts: str) -> str: + """SHA-256 of the strict action-ref-v1 JCS preimage.""" + if not all(isinstance(value, str) for value in (agent_id, action_type, scope)): + raise ValueError("action_ref agent_id, action_type, and scope must be strings") + _validate_action_ref_timestamp(ts) + preimage = { + "agent_id": agent_id, + "action_type": action_type, + "scope": scope, + "timestamp": ts, + } + return sha256hex(jcs(preimage)) + + +# --------------------------------------------------------------------------- +# Nexus trail submission +# --------------------------------------------------------------------------- + +async def submit_trail_async( + request_id: str, + action: str, + agent_id: Optional[str], + claimed_at: float, + result: Dict[str, Any], +) -> Optional[str]: + if not enabled(): + return None + + _ct = claimed_at if claimed_at else time.time() + ts_str = datetime.fromtimestamp(_ct, tz=timezone.utc).strftime('%Y-%m-%dT%H:%M:%S.') + f'{int(_ct * 1000) % 1000:03d}Z' + _agent_id = agent_id or MYCELIUM_AGENT_ID + scope = request_id + + preimage = { + 'action_type': action, + 'agent_id': _agent_id, + 'scope': scope, + 'timestamp': ts_str, + } + action_ref = compute_action_ref(_agent_id, action, scope, ts_str) + payment_hash = 'sha256-' + sha256hex(f'payment:{action_ref}'.encode()) + output_hash = 'sha256-' + sha256hex(json.dumps(result, sort_keys=True).encode()) + + payload = { + 'packet_version': '1.0', + 'canonicalization_profile_id': '8c7f71754e3daae1a0390d5e0287d51097d011e40df36bf15cad5c0f47efa05a', + 'action_ref': action_ref, + 'service': 'safeagent', + 'preimage': preimage, + 'payment_hash': payment_hash, + 'output_hash': output_hash, + 'hash_algo': 'SHA-256', + 'preimage_format': 'jcs', + 'timestamp': int(_ct * 1000), + 'api_key': MYCELIUM_API_KEY, + } + + try: + async with httpx.AsyncClient(timeout=30) as client: + resp = await client.post(NEXUS_TRAIL_URL, json=payload) + if resp.status_code not in (200, 201): + _log.warning('Mycelium trail error: %s', resp.text) + resp.raise_for_status() + data = resp.json() + trail_id = data.get('trail_id') or data.get('id') + if trail_id: + _log.info('Mycelium trail submitted: trail_id=%s action_ref=%s', trail_id, action_ref) + return trail_id + _log.warning('Mycelium trail: no trail_id in response: %s', data) + return None + except Exception as exc: + _log.warning('Mycelium trail submission failed: %s', exc) + return None + + +# --------------------------------------------------------------------------- +# Anchor polling — verify_chain +# --------------------------------------------------------------------------- + +async def get_trail_anchor(trail_id: str) -> Optional[Dict[str, Any]]: + """ + Poll verify_chain for Arbitrum confirmation. + Returns anchor dict with block_time and tx_hash when confirmed, else None. + """ + url = VERIFY_CHAIN_URL.format(trail_id=trail_id) + try: + async with httpx.AsyncClient(timeout=30) as client: + resp = await client.get(url) + if resp.status_code == 404: + return None + resp.raise_for_status() + data = resp.json() + block_time = ( + data.get("block_time") + or data.get("anchor_block_time") + or data.get("anchor", {}).get("block_time") + ) + if not block_time: + return None + return { + "block_time": block_time, + "tx_hash": ( + data.get("tx_hash") + or data.get("transaction_hash") + or data.get("anchor", {}).get("tx_hash") + ), + "trail_id": trail_id, + } + except Exception as exc: + _log.warning("Mycelium verify_chain failed for %s: %s", trail_id, exc) + return None From adbfe74efad9acc1971b98f3b890db56bf6a72e5 Mon Sep 17 00:00:00 2001 From: azender1 Date: Wed, 30 Sep 2026 22:15:33 -0400 Subject: [PATCH 3/8] Test action-ref-v1 negative timestamp cases --- tests/test_attestation_gate.py | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/tests/test_attestation_gate.py b/tests/test_attestation_gate.py index b1689e7..6c4fe73 100644 --- a/tests/test_attestation_gate.py +++ b/tests/test_attestation_gate.py @@ -12,6 +12,7 @@ import json from base64 import urlsafe_b64encode +import pytest import rfc8785 from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey @@ -70,10 +71,35 @@ def test_allow_clean(): att = _sign(_attestation("A", 0, 0, 1), sk, kid) r = ag.gate(CLAIM, att, jwks=jwks, require_attestation=True) assert r["decision"] == "ALLOW" - # action_ref byte-identical to exactly-once-v1.json assert r["action_ref"] == "281c2b33068cd63b2f89f09ea49b75edddf2d0c298f939a8eac13c2e34433b18" +def test_action_ref_rejects_epoch_integer_timestamp(): + preimage = { + "action_type": "oracle.signal", + "agent_id": "nexus-agent-xa12.onrender.com", + "scope": "BTC", + "timestamp": 1781166600123, + } + with pytest.raises(ValueError): + ag.compute_action_ref(preimage) + + +def test_action_ref_rejects_noncanonical_timestamp_precision(): + for timestamp in ("2026-06-11T08:45:00Z", "2026-06-11T08:30:00.123456Z"): + with pytest.raises(ValueError): + ag.compute_action_ref({**ACTION_REF_PREIMAGE, "timestamp": timestamp}) + + +def test_gate_denies_invalid_action_ref_preimage(): + sk, kid, jwks = _issuer() + att = _attestation("A", 0, 0) + att["action_ref_preimage"] = {**ACTION_REF_PREIMAGE, "timestamp": 1781166600123} + att = _sign(att, sk, kid) + r = ag.gate(CLAIM, att, jwks=jwks) + assert r == {"decision": "DENY", "reason": "action_ref_preimage_invalid"} + + def test_deny_critical(): sk, kid, jwks = _issuer() att = _sign(_attestation("F", 2, 1, 3), sk, kid) @@ -93,7 +119,7 @@ def test_deny_unattested_when_required(): def test_deny_bad_signature(): sk, kid, jwks = _issuer() att = _sign(_attestation("A", 0, 0), sk, kid) - att["attestation"]["payload"]["grade"] = "F" # mutate after signing + att["attestation"]["payload"]["grade"] = "F" r = ag.gate(CLAIM, att, jwks=jwks) assert r["decision"] == "DENY" and r["reason"] == "attestation_signature_invalid" @@ -110,4 +136,4 @@ def test_jcs_handles_nested_unlike_naive_sort(): nested = {"attestation": {"payload": {"findings": { "total": 7, "critical": 0, "high": 2, "medium": 5}, "grade": "B"}}} naive = json.dumps(dict(sorted(nested.items())), separators=(",", ":")).encode() - assert ag.jcs(nested) != naive # the bug this module avoids + assert ag.jcs(nested) != naive From 3c803887f46ceddab2bcaa875560e28bb4084c61 Mon Sep 17 00:00:00 2001 From: azender1 Date: Wed, 30 Sep 2026 22:15:36 -0400 Subject: [PATCH 4/8] Add action-ref-v1 conformance adapter --- scripts/action_ref_v1_impl.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 scripts/action_ref_v1_impl.py diff --git a/scripts/action_ref_v1_impl.py b/scripts/action_ref_v1_impl.py new file mode 100644 index 0000000..e4691c5 --- /dev/null +++ b/scripts/action_ref_v1_impl.py @@ -0,0 +1,20 @@ +"""stdin/stdout adapter for the frozen action-ref-v1 conformance Action.""" +from __future__ import annotations + +import json +import sys + +from safeagent_exec_guard.attestation_gate import compute_action_ref + + +def main() -> int: + try: + preimage = json.load(sys.stdin) + print(compute_action_ref(preimage)) + return 0 + except (ValueError, TypeError, KeyError, json.JSONDecodeError): + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) From fc8744748caf52497b315acb82ee5a18ad6c62ed Mon Sep 17 00:00:00 2001 From: azender1 Date: Wed, 30 Sep 2026 22:15:39 -0400 Subject: [PATCH 5/8] Run lab action-ref-v1 conformance in CI --- .../workflows/action-ref-v1-conformance.yml | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/workflows/action-ref-v1-conformance.yml diff --git a/.github/workflows/action-ref-v1-conformance.yml b/.github/workflows/action-ref-v1-conformance.yml new file mode 100644 index 0000000..c4ceac8 --- /dev/null +++ b/.github/workflows/action-ref-v1-conformance.yml @@ -0,0 +1,34 @@ +name: action-ref-v1 conformance + +on: + push: + pull_request: + +permissions: + contents: read + +jobs: + conformance: + runs-on: ubuntu-latest + steps: + - name: Checkout SafeAgent + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install SafeAgent and focused test dependency + run: | + python -m pip install --upgrade pip + pip install . pytest + + - name: Run action-ref focused tests + run: python -m pytest tests/test_attestation_gate.py -q + + - name: Verify frozen action-ref-v1 vectors + uses: Composable-Evidence-Criteria/action-ref-conformance@v1 + with: + mode: verify-impl + command: python scripts/action_ref_v1_impl.py From edb4aae2c8ab43797b79aa5043e597415fe4663b Mon Sep 17 00:00:00 2001 From: azender1 Date: Wed, 30 Sep 2026 22:52:29 -0400 Subject: [PATCH 6/8] Restore attestation security rationale comments --- safeagent_exec_guard/attestation_gate.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/safeagent_exec_guard/attestation_gate.py b/safeagent_exec_guard/attestation_gate.py index e3389b1..afcdc62 100644 --- a/safeagent_exec_guard/attestation_gate.py +++ b/safeagent_exec_guard/attestation_gate.py @@ -158,6 +158,7 @@ def gate( - Bad signature, binding mismatch, verdict != admit, or a critical/high finding: DENY. - Otherwise: ALLOW, carrying the action_ref for the exactly-once guard. """ + # availability stop — distinct from a safety stop if unreachable: if require_attestation: return {"decision": "DENY", "reason": "attestation_unreachable"} @@ -174,6 +175,7 @@ def gate( if not verify_signature(attestation, jwks): return {"decision": "DENY", "reason": "attestation_signature_invalid"} + # bind to THIS claim — an attestation can't be replayed against a different charge expected = compute_binding_digest(claim_binding_preimage) claimed = ((attestation.get("binding") or attestation.get("attestation_binding") or {}) .get("binding_digest")) @@ -181,6 +183,7 @@ def gate( return {"decision": "DENY", "reason": "binding_digest_mismatch", "expected": expected, "claimed": claimed} + # freshness — a valid, correctly-bound verdict that is no longer current must not admit if freshness_ttl_seconds is not None: now = now or datetime.now(timezone.utc) issued = _parse_ts(attestation.get("issued_at")) From 31b7798cfff9cead973f7031f3ea23afd1968c22 Mon Sep 17 00:00:00 2001 From: azender1 Date: Wed, 30 Sep 2026 22:52:35 -0400 Subject: [PATCH 7/8] Add Mycelium action-ref conformance adapter --- scripts/action_ref_v1_mycelium_impl.py | 27 ++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 scripts/action_ref_v1_mycelium_impl.py diff --git a/scripts/action_ref_v1_mycelium_impl.py b/scripts/action_ref_v1_mycelium_impl.py new file mode 100644 index 0000000..790e2ee --- /dev/null +++ b/scripts/action_ref_v1_mycelium_impl.py @@ -0,0 +1,27 @@ +"""stdin/stdout adapter for SafeAgent's Mycelium action-ref-v1 implementation.""" +from __future__ import annotations + +import json +import sys + +from safeagent_exec_guard.mycelium_trail import compute_action_ref + + +def main() -> int: + try: + preimage = json.load(sys.stdin) + print( + compute_action_ref( + preimage["agent_id"], + preimage["action_type"], + preimage["scope"], + preimage["timestamp"], + ) + ) + return 0 + except (ValueError, TypeError, KeyError, json.JSONDecodeError): + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) From 7783f7a0347c926866908059b50c22fb587c602d Mon Sep 17 00:00:00 2001 From: azender1 Date: Wed, 30 Sep 2026 22:52:38 -0400 Subject: [PATCH 8/8] Pin lab action and verify both implementations --- .github/workflows/action-ref-v1-conformance.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/action-ref-v1-conformance.yml b/.github/workflows/action-ref-v1-conformance.yml index c4ceac8..a8a95cd 100644 --- a/.github/workflows/action-ref-v1-conformance.yml +++ b/.github/workflows/action-ref-v1-conformance.yml @@ -27,8 +27,14 @@ jobs: - name: Run action-ref focused tests run: python -m pytest tests/test_attestation_gate.py -q - - name: Verify frozen action-ref-v1 vectors - uses: Composable-Evidence-Criteria/action-ref-conformance@v1 + - name: Verify attestation-gate action-ref-v1 vectors + uses: Composable-Evidence-Criteria/action-ref-conformance@56e25a8353ba02b76edb072c5bd405b56238bb12 with: mode: verify-impl command: python scripts/action_ref_v1_impl.py + + - name: Verify Mycelium action-ref-v1 vectors + uses: Composable-Evidence-Criteria/action-ref-conformance@56e25a8353ba02b76edb072c5bd405b56238bb12 + with: + mode: verify-impl + command: python scripts/action_ref_v1_mycelium_impl.py