Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 78 additions & 22 deletions integrations/ontoguard-decision-authorization/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@ Thin interoperability adapter. It consumes a *signed* OntoGuard Decision
Authorization result and, only after an *independent* execution receipt binds
to that exact result, emits a TRACE v0.2 Level 0 Trust Record.

This adapter contains no OntoGuard core authorization or semantic-governance implementation.
This adapter contains no OntoGuard core authorization or semantic-governance
implementation.

Production authorization objects consumed by this adapter are issued by
OntoGuard Headless Decision Authorization Runtime or another implementation
conforming to the same authorization contract.

Runtime documentation:
https://ontoguard.ai/headless-decision-authorization-runtime.html

## What this integration does

Expand Down Expand Up @@ -37,13 +45,73 @@ if ALLOW + independently proven execution:
`ALLOW` never proves that an action ran. A BLOCK or ESCALATE paired with
`executed=true` is rejected, not silently reclassified.

## Time bounds on captured versus live objects

The captured historical authorization is time-bounded
(`expires_at_utc = 2026-09-24…`) and has expired. It remains valid
historical signed evidence. Signatures and digests stay inspectable after
expiry.

By default, `ontoguard_trace` verifies expiry against the current UTC time.
Therefore, a normal/live replay of an expired authorization fails closed
with `authorization has expired`.

Historical fixture verification is different: tests and historical replay
utilities pass an explicit `verification_time_utc` that falls inside the
authorization's original validity interval. This is used only to reproduce
the already-captured historical event deterministically. It does **not**
extend the authorization, change its signed bytes, alter its expiry, or make
the authorization valid for a current execution.

Current historical-fixture verification time:

```text
2026-09-18T00:00:00Z
```

The fixed historical time is used by:

- `tests/test_adapter.py`
- `examples/emit_record.py`
- `examples/controlled-execution-proof-2026-09-17/replay_adapter.py`

Production/live callers should rely on the adapter's current-time default
unless they are explicitly performing bounded historical evidence replay.
An untrusted caller must not be allowed to choose a verification time for a
live authorization decision.

The live executor harness never reuses the captured authorization. It mints
a fresh TEST-ONLY authorization at runtime, verifies it before any commit,
then reruns $250k (commit) and $260k (refuse) through this adapter. That
runtime object is harness-only and is not a live OntoGuard Decision API
result.

Ordinary fixtures in `examples/fixtures/` also carry `expires_at_utc`.
Treat them as dated evidence, not as unexpiring production objects.

## Pre-commit enforcement composition

This repository does not contain OntoGuard's semantic authorization engine.
A downstream enforcement runtime may consume a current signed OntoGuard
authorization before protected execution. The bounded example strictly
validates the proposed partner action, verifies the signed authorization and
exact-action binding, and re-verifies those conditions at the controlled
executor's commit boundary. A caller-computed digest alone is not authority.
A materially different or malformed action, BLOCK, ESCALATE, expired
authorization, invalid signature or binding mismatch must not proceed.

OntoGuard determines semantic authorization. The external runtime retains
enforcement. TRACE records execution evidence only after execution is
independently proven.

A sanitized example of that seam is in `examples/precommit-enforcement/`.

## What this integration does not claim

- Not production L5 or non-bypassable route topology.
- Not hardware attestation, TEE, or confidential computing.
- Not continuously checked. No CI workflow runs `trace-tests` on this
integration yet; the Level 0 result rests on the maintainer run recorded
under "Verified-tier review".
- Marketplace Verified; TRACE Level 0 conformance is established only by
signed-record CI verification.
- Not an OntoGuard semantic engine. Authorization remains in OntoGuard.
- Per TRACE spec 3.1.2, a Trust Record is issued per execution and a
reference cannot carry a pre-execution commitment.
Expand All @@ -68,6 +136,11 @@ trace-tests verify \
--level 0
```

`pytest` and `examples/emit_record.py` intentionally evaluate the frozen
historical fixture at the fixed historical verification time documented
above. This keeps CI deterministic after the fixture's real-world expiry
without weakening live expiry enforcement.

A captured historical controlled-execution proof plus a live executor
harness live in `examples/controlled-execution-proof-2026-09-17/`.

Expand All @@ -76,13 +149,6 @@ python examples/controlled-execution-proof-2026-09-17/verify_proof.py
python examples/controlled-execution-proof-2026-09-17/controlled_executor.py
```

The historical ALLOW authorization is frozen evidence and expires
2026-09-24. The live harness does **not** reuse that object. It mints a
TEST-ONLY authorization at runtime, verifies it before any commit, then
reruns $250k (commit) and $260k (refuse) through this adapter. That
runtime authorization is harness-only and is not a live OntoGuard
Decision API result.

Level 1 is unsupported (`runtime.platform=software-only`) and must fail
`TR-RTE-001`.

Expand All @@ -108,21 +174,11 @@ Level 1 is unsupported (`runtime.platform=software-only`) and must fail
## Responsibility boundary

- OntoGuard: authorization result and signature over the exact result bytes.
- Executing runtime: independent execution receipt after the action runs.
- Executing runtime: independent execution receipt after the action ran.
- This adapter: verify both objects, refuse TRACE when either is missing or
mismatched, project an executed event into TRACE v0.2.
- Downstream enforcement: consume the current handoff before commit.

## Verified-tier review

A maintainer ran "Run it" on 2026-09-21 in an isolated environment against
released `agentrust-trace` 0.10.0 and `agentrust-trace-tests` 0.5.1: 27 tests
passed, `emit_record.py` printed `STATE=ALLOW_EXECUTION_PROVEN SIGNED=True`, and
`trace-tests verify --level 0` gave `Result: PASS (8 checks, 0 skipped)` with the
signature verified. Level 1 fails on `TR-RTE-001` and `TR-RTE-004`, as the list
above says it must. The manifest is now `tier: verified`. Re-verification happens
at every release that touches this integration.

## License

Apache-2.0, matching `agentrust-io/integrations`. OntoGuard core
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,17 @@ OntoGuard Decision API result.

Order of operations:

1. mint and cryptographically verify the test authorization
2. derive the $250,000 partner action binding
3. only then PENDING → RELEASED, commit_count 0 → 1
4. sign a fresh ephemeral executor receipt
5. pass the live objects through `ontoguard_trace.project`
6. separately rerun $260,000, refuse, commit_count stays 0, no TRACE
1. mint the TEST-ONLY signed authorization
2. strictly validate the proposed partner action
3. verify the signed authorization again at the controlled executor's commit boundary
4. require ALLOW + release authorization + exact validated action binding
5. only then PENDING → RELEASED, commit_count 0 → 1
6. sign a fresh ephemeral executor receipt
7. pass the live objects through `ontoguard_trace.project`
8. separately rerun $260,000 and digest-only bypass attempts; refuse before commit

A caller-computed action digest is not authority. The controlled executor requires
the signed authorization and trusted JWKS at its bounded commit boundary.

This is a controlled software-only store. Not a bank transfer, not L5,
and not OntoGuard core.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
1f45c4a6894e70ce73204cd7c654a2a81bd3b8d77098bfa5c5121063406bb503 README.md
83836847ae16688d05db9bd7e18b200c3b632175d940e65fc5b87888097e4aef controlled_executor.py
705fd4d6451a31d36b3df7de96f83f30ac976c9b4a6d1e51671d8e2f33e2d0da .gitattributes
30a0a39bbce27b31e76c576f3813741c1378db954e5da43782dfd4f0f239342c README.md
1b325857bb28ebd9c48974ea251d902109fe5f5a7fd51be094439dad4726da38 controlled_executor.py
0e8757f8f9d76626ff28f969ab0906be50a01bfd10715b847ee135fd248f9f97 negative_action_mutation/mutated_action.json
24b2cf0beb31e190e7d7dd89c04295078206107615556ffcf9103a5345ccfa6a negative_action_mutation/rejection_result.json
7d248f18e2974d1d11453b0c1b547c1650c1228ceb79741c9dc05750e777e551 positive/after_state.json
Expand All @@ -18,4 +19,4 @@ c05e327faaaa7b4edd3d6b8e9cb679f752b2ee8fe647b070602892bd398eb86b positive/ontog
51464c725f51f14c0ba959d2b8752f7a614d9acc6386ba467af71229fa8e3776 positive/trace_claim_candidate.json
2f49d1277bb9d21c8decee0526b9c51f974fa69b4ed83b1049974af79e91b414 positive/trace_level0_conformance.txt
0d641d7084e3444c191e409a2d57c6faf9472751ad2d0edbb9d296f3cab95997 replay_adapter.py
c33b5605cf1bbe646368012eec941b7257e373aa213b610455026fc7a6fab050 verify_proof.py
c84b3b6a2fa9c7f6bc4870bdde71988d465e8bf89f24c65104bc735a2a6e8480 verify_proof.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
partner_action_binding_object,
project,
sha256_digest,
validate_partner_action_binding_object,
)

AUTHORIZED_ACTION = partner_action_binding_object(
Expand Down Expand Up @@ -116,20 +117,78 @@ def write_execution_jwks(self, path: Path) -> Path:
path.write_text(json.dumps({"keys": [self.public_jwk]}, indent=2) + "\n", encoding="utf-8")
return path

def attempt(self, proposed: dict[str, Any], authorized_digest: str) -> dict[str, Any]:
executed_digest = partner_action_binding_digest(proposed)
if executed_digest != authorized_digest:
self.store.history.append("REFUSED_BINDING_MISMATCH")
def attempt(
self,
proposed: dict[str, Any],
authorization: Any = None,
*,
ontoguard_jwks_path: Path | None = None,
allow_test_keys: bool | None = None,
verification_time_utc: datetime | None = None,
) -> dict[str, Any]:
"""Verify signed authorization at the bounded commit boundary before mutation."""

def refuse(
reason: str,
*,
authorized_digest: str | None = None,
executed_digest: str | None = None,
) -> dict[str, Any]:
self.store.history.append("REFUSED")
return {
"result": "EXECUTION_REFUSED",
"reason": "executed_action_binding_digest != authorized_action_binding_digest",
"reason": reason,
"authorized_action_binding_digest": authorized_digest,
"executed_action_binding_digest": executed_digest,
"protected_effect_formed": False,
"commit_count": self.store.commit_count,
"status": self.store.status,
"TRACE_RECORD_EMITTED": False,
}

try:
validated_action = validate_partner_action_binding_object(proposed)
executed_digest = partner_action_binding_digest(validated_action)
except (AdapterError, TypeError, ValueError) as exc:
return refuse(str(exc))

if not isinstance(authorization, dict):
return refuse(
"signed OntoGuard authorization is required at commit",
executed_digest=executed_digest,
)
if ontoguard_jwks_path is None:
return refuse(
"trusted OntoGuard JWKS is required at commit",
executed_digest=executed_digest,
)

try:
bound = bind_authorization(
result_bytes=authorization.get("result_bytes"),
signature_b64url=authorization.get("signature"),
public_jwk=authorization.get("public_jwk"),
ontoguard_jwks_path=ontoguard_jwks_path,
allow_test_keys=allow_test_keys,
verification_time_utc=verification_time_utc,
)
except (AdapterError, TypeError, ValueError) as exc:
return refuse(str(exc), executed_digest=executed_digest)

authorized_digest = bound["action_binding_digest"]
if bound["action"] != "ALLOW" or bound["release_authorized"] is not True:
return refuse(
f"{bound['action']} is not a releasable authorization",
authorized_digest=authorized_digest,
executed_digest=executed_digest,
)
if executed_digest != authorized_digest:
return refuse(
"executed_action_binding_digest != authorized_action_binding_digest",
authorized_digest=authorized_digest,
executed_digest=executed_digest,
)

commit_id = "commit-" + secrets.token_hex(8)
self.store.status = "RELEASED"
self.store.commit_count += 1
Expand All @@ -144,6 +203,7 @@ def attempt(self, proposed: dict[str, Any], authorized_digest: str) -> dict[str,
"protected_effect_formed": True,
"commit_count": self.store.commit_count,
"status": self.store.status,
"validated_action": validated_action,
}

def build_receipt(
Expand Down Expand Up @@ -299,7 +359,12 @@ def run_live(proof_dir: Path = PROOF_DIR) -> dict[str, Any]:
# Positive $250k — commit only after verification
pos_exec = ControlledExecutor()
pos_before = pos_exec.store.snapshot()
pos_attempt = pos_exec.attempt(AUTHORIZED_ACTION, authorized_digest)
pos_attempt = pos_exec.attempt(
AUTHORIZED_ACTION,
minted,
ontoguard_jwks_path=og_jwks,
allow_test_keys=True,
)
pos_receipt = pos_exec.build_receipt(
auth=minted["auth"],
result_digest=minted["result_digest"],
Expand Down Expand Up @@ -337,7 +402,12 @@ def run_live(proof_dir: Path = PROOF_DIR) -> dict[str, Any]:
# Negative $260k — new store, same verified authorization
neg_exec = ControlledExecutor()
neg_before = neg_exec.store.snapshot()
neg_attempt = neg_exec.attempt(MUTATED_ACTION, authorized_digest)
neg_attempt = neg_exec.attempt(
MUTATED_ACTION,
minted,
ontoguard_jwks_path=og_jwks,
allow_test_keys=True,
)
forged = dict(neg_attempt)
forged["execution_event_id"] = "commit-forged-mutation"
forged["authorized_action_binding_digest"] = authorized_digest
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@

from __future__ import annotations

import base64
import hashlib
import json
from pathlib import Path

from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
import base64

ROOT = Path(__file__).resolve().parent
POS = ROOT / "positive"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import argparse
import json
import sys
from datetime import datetime, timezone
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
Expand All @@ -19,6 +20,12 @@

DEFAULT_FIXTURE = Path(__file__).resolve().parent / "fixtures" / "allow_execution_proven.json"

# The bundled fixture is frozen historical evidence. Verify it at a time when
# the signed authorization was actually valid instead of against today's wall
# clock. Production calls that do not pass verification_time_utc continue to
# use the real current UTC time inside ontoguard_trace.
HISTORICAL_VERIFICATION_TIME = datetime(2026, 9, 18, 0, 0, tzinfo=timezone.utc)


def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
Expand All @@ -31,6 +38,7 @@ def main() -> int:
if not fixture.get("execution_receipt"):
print("ERROR: fixture has no independent execution receipt", file=sys.stderr)
return 2

try:
result = project(
fixture["authorization_result"],
Expand All @@ -41,19 +49,27 @@ def main() -> int:
execution_receipt=fixture["execution_receipt"],
sign_trace=not args.unsigned,
allow_test_keys=True,
verification_time_utc=HISTORICAL_VERIFICATION_TIME,
)
except AdapterError as exc:
print(f"ERROR: {exc}", file=sys.stderr)
return 2

if args.unsigned:
out = Path(args.out)
out.write_text(json.dumps(result.get("trace_claim_candidate"), indent=2) + "\n", encoding="utf-8")
out.write_text(
json.dumps(result.get("trace_claim_candidate"), indent=2) + "\n",
encoding="utf-8",
)
print(f"WROTE_CANDIDATE {out}")
print("TRACE_RECORD_EMITTED=false")
return 0

if not result.get("trace_record_emitted"):
print(f"ERROR: no TRACE record emitted (state={result.get('state')})", file=sys.stderr)
print(
f"ERROR: no TRACE record emitted (state={result.get('state')})",
file=sys.stderr,
)
return 2

out = Path(args.out)
Expand Down
Loading
Loading