Skip to content
Merged
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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ The format is based on Keep a Changelog, and this project adheres to Semantic Ve

## [Unreleased]

### Added

- **Runtime optic requirements artifact**: `compile_runtime_optic()` now emits a
Wesley-owned `OpticAdmissionRequirementsArtifact` containing canonical
requirements bytes, an explicit `wesley.requirements.canonical-json.v0` codec,
and a digest computed from those exact bytes. Downstream runtimes can import
the bytes, digest, and codec directly without reserializing
`OpticAdmissionRequirements` to create admission truth.

## [0.0.3] - 2026-05-14

### Added
Expand Down
41 changes: 29 additions & 12 deletions crates/wesley-core/src/adapters/apollo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ use crate::domain::operation::{
};
use crate::domain::optic::{
CodecField, CodecShape, DirectiveRecord, EvidenceKind, Footprint, IdentityRequirement,
LawClaimTemplate, OperationKind, OpticAdmissionRequirements, OpticArtifact, OpticOperation,
OpticRegistrationDescriptor, PermissionAction, PermissionRequirement, RootArgumentBinding,
SelectionArgumentBinding,
LawClaimTemplate, OperationKind, OpticAdmissionRequirements,
OpticAdmissionRequirementsArtifact, OpticArtifact, OpticOperation, OpticRegistrationDescriptor,
PermissionAction, PermissionRequirement, RootArgumentBinding, SelectionArgumentBinding,
OPTIC_ADMISSION_REQUIREMENTS_ARTIFACT_CODEC,
};
use crate::domain::schema_delta::{diff_schema_ir, SchemaDelta};
use crate::ports::lowering::LoweringPort;
Expand Down Expand Up @@ -1073,20 +1074,21 @@ pub fn compile_runtime_optic(
let law_claims =
law_claims_for_operation(&operation_id, &directives, declared_footprint.as_ref())?;
let requirements = admission_requirements_from_footprint(declared_footprint.as_ref());
let requirements_digest = stable_json_hash(
&serde_json::json!({
"declaredFootprint": &declared_footprint,
"lawClaims": &law_claims,
"requirements": &requirements,
}),
"runtime optic requirements digest",
)?;
let requirements_artifact = canonical_requirements_artifact(&serde_json::json!({
"declaredFootprint": &declared_footprint,
"lawClaims": &law_claims,
"requirements": &requirements,
}))?;
let requirements_digest = requirements_artifact.digest.clone();
let artifact_hash = stable_json_hash(
&serde_json::json!({
"directives": &directives,
"operationId": &operation_id,
"payloadShape": &payload_shape,
"requirementsDigest": &requirements_digest,
"requirementsArtifact": {
"codec": &requirements_artifact.codec,
"digest": &requirements_artifact.digest,
},
"schemaId": &schema_id,
"variableShape": &variable_shape,
}),
Expand All @@ -1106,6 +1108,7 @@ pub fn compile_runtime_optic(
artifact_hash,
schema_id,
requirements_digest,
requirements_artifact,
operation: OpticOperation {
operation_id,
name: operation_name,
Expand Down Expand Up @@ -3184,6 +3187,20 @@ fn stable_json_hash<T: serde::Serialize>(value: &T, area: &str) -> Result<String
Ok(compute_content_hash(&canonical))
}

fn canonical_requirements_artifact<T: serde::Serialize>(
value: &T,
) -> Result<OpticAdmissionRequirementsArtifact, WesleyError> {
let canonical = stable_json_string(value, "runtime optic requirements artifact")?;
let bytes = canonical.into_bytes();
let digest = compute_content_hash_bytes(&bytes);

Ok(OpticAdmissionRequirementsArtifact {
digest,
codec: OPTIC_ADMISSION_REQUIREMENTS_ARTIFACT_CODEC.to_string(),
bytes,
})
}

fn stable_json_string<T: serde::Serialize>(value: &T, area: &str) -> Result<String, WesleyError> {
to_canonical_json(value)
.map_err(|err| lowering_error_value(area, format!("Failed to serialize JSON: {err}")))
Expand Down
7 changes: 6 additions & 1 deletion crates/wesley-core/src/domain/ir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,13 @@ pub fn compute_registry_hash(ir: &WesleyIR) -> Result<String, serde_json::Error>

/// Computes a stable SHA-256 hex hash for arbitrary UTF-8 content.
pub fn compute_content_hash(content: &str) -> String {
compute_content_hash_bytes(content.as_bytes())
}

/// Computes a stable SHA-256 hex hash for arbitrary bytes.
pub fn compute_content_hash_bytes(content: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(content.as_bytes());
hasher.update(content);
let result = hasher.finalize();

hex::encode(result)
Expand Down
22 changes: 22 additions & 0 deletions crates/wesley-core/src/domain/optic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::fmt;

/// Canonical JSON codec for Wesley-owned runtime optic admission requirements.
pub const OPTIC_ADMISSION_REQUIREMENTS_ARTIFACT_CODEC: &str =
"wesley.requirements.canonical-json.v0";

/// Compiled contract for one runtime-declared GraphQL optic operation.
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
Expand All @@ -22,6 +26,8 @@ pub struct OpticArtifact {
pub schema_id: String,
/// Stable digest of admission requirements and law claim templates.
pub requirements_digest: String,
/// Wesley-owned canonical byte artifact for admission requirements.
pub requirements_artifact: OpticAdmissionRequirementsArtifact,
/// The selected GraphQL operation compiled into an inspectable contract.
pub operation: OpticOperation,
/// Admission requirements Echo or another runtime must enforce.
Expand Down Expand Up @@ -77,6 +83,22 @@ pub struct OpticAdmissionRequirements {
pub forbidden_resources: Vec<String>,
}

/// Wesley-owned canonical admission requirements byte artifact.
///
/// Downstream runtimes import these bytes directly. They should verify the
/// digest and codec, but they should not serialize Wesley structs to create
/// admission truth.
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct OpticAdmissionRequirementsArtifact {
/// Stable digest computed from `bytes` exactly.
pub digest: String,
/// Explicit codec describing how `bytes` were generated.
pub codec: String,
/// Canonical requirements bytes generated by Wesley.
pub bytes: Vec<u8>,
}

/// Host, user, quorum, or policy authority grant for invoking an artifact.
///
/// Wesley core defines this shape so artifacts and runtime receipts can speak a
Expand Down
170 changes: 170 additions & 0 deletions crates/wesley-core/tests/runtime_optic_artifact.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
use sha2::{Digest, Sha256};
use wesley_core::{
compile_runtime_optic, compile_runtime_optic_registration, AdmissionTicket, ApertureConstraint,
BasisConstraint, BudgetConstraint, CapabilityGrant, CapabilityPresentation, CodecField,
DirectiveRecord, EvidenceKind, InMemoryOpticArtifactRegistry, LawVerdict, LawWitness,
ObserverClass, OperationKind, OpticArtifactHandle, OpticArtifactResolver, PermissionAction,
PermissionRequirement, PrincipalRef, ReplayHint, ResolveError,
OPTIC_ADMISSION_REQUIREMENTS_ARTIFACT_CODEC,
};

#[test]
Expand Down Expand Up @@ -250,6 +252,168 @@ fn artifact_hashes_are_stable_and_sensitive_to_shape_and_requirements() {
);
}

#[test]
fn canonical_requirements_bytes_are_deterministic() {
let schema = include_str!("../../../test/fixtures/runtime-optics/workspace_schema.graphql");
let operation = include_str!("../../../test/fixtures/runtime-optics/rename_symbol.graphql");

let artifact = compile_runtime_optic(schema, operation, Some("RenameSymbol"))
.expect("runtime optic should compile");
let expected = wesley_core::to_canonical_json(&serde_json::json!({
"declaredFootprint": &artifact.operation.declared_footprint,
"lawClaims": &artifact.operation.law_claims,
"requirements": &artifact.requirements,
}))
.expect("expected requirements body should canonicalize")
.into_bytes();

assert!(!artifact.requirements_artifact.bytes.is_empty());
assert_eq!(
artifact.requirements_artifact.bytes, expected,
"compiled artifact should expose Wesley's canonical requirements byte buffer"
);
}

#[test]
fn canonical_requirements_digest_matches_bytes() {
let schema = include_str!("../../../test/fixtures/runtime-optics/workspace_schema.graphql");
let operation = include_str!("../../../test/fixtures/runtime-optics/rename_symbol.graphql");

let artifact = compile_runtime_optic(schema, operation, Some("RenameSymbol"))
.expect("runtime optic should compile");
let actual_digest = sha256_hex(&artifact.requirements_artifact.bytes);

assert_eq!(artifact.requirements_artifact.digest, actual_digest);
assert_eq!(
artifact.requirements_digest,
artifact.requirements_artifact.digest
);
}

#[test]
fn canonical_requirements_codec_is_explicit() {
let schema = include_str!("../../../test/fixtures/runtime-optics/workspace_schema.graphql");
let operation = include_str!("../../../test/fixtures/runtime-optics/rename_symbol.graphql");

let artifact = compile_runtime_optic(schema, operation, Some("RenameSymbol"))
.expect("runtime optic should compile");

assert_eq!(
artifact.requirements_artifact.codec,
OPTIC_ADMISSION_REQUIREMENTS_ARTIFACT_CODEC
);
}

#[test]
fn canonical_requirements_bytes_are_stable_across_repeated_compile() {
let schema = include_str!("../../../test/fixtures/runtime-optics/workspace_schema.graphql");
let operation = include_str!("../../../test/fixtures/runtime-optics/rename_symbol.graphql");

let first = compile_runtime_optic(schema, operation, Some("RenameSymbol"))
.expect("runtime optic should compile");
let second = compile_runtime_optic(schema, operation, Some("RenameSymbol"))
.expect("runtime optic should compile repeatedly");

assert_eq!(
first.requirements_artifact.bytes,
second.requirements_artifact.bytes
);
assert_eq!(
first.requirements_artifact.digest,
second.requirements_artifact.digest
);
}

#[test]
fn canonical_requirements_json_keys_are_ordered_if_json_codec_is_used() {
let schema = include_str!("../../../test/fixtures/runtime-optics/workspace_schema.graphql");
let operation = include_str!("../../../test/fixtures/runtime-optics/rename_symbol.graphql");

let artifact = compile_runtime_optic(schema, operation, Some("RenameSymbol"))
.expect("runtime optic should compile");
assert_eq!(
artifact.requirements_artifact.codec,
OPTIC_ADMISSION_REQUIREMENTS_ARTIFACT_CODEC
);

let canonical = std::str::from_utf8(&artifact.requirements_artifact.bytes)
.expect("canonical requirements bytes should be UTF-8 JSON");
serde_json::from_str::<serde_json::Value>(canonical)
.expect("canonical requirements bytes should parse as JSON");

assert!(
canonical.starts_with("{\"declaredFootprint\":"),
"top-level JSON keys should begin in sorted order: {canonical}"
);
assert!(
canonical.find("\"lawClaims\"").expect("lawClaims key")
< canonical
.find("\"requirements\"")
.expect("requirements key"),
"top-level JSON keys should stay lexicographically ordered: {canonical}"
);
assert!(
canonical
.find("\"forbiddenResources\"")
.expect("forbiddenResources key")
< canonical.find("\"identity\"").expect("identity key"),
"nested requirements keys should stay lexicographically ordered: {canonical}"
);
}

#[test]
fn runtime_optic_artifact_exposes_requirements_artifact() {
let schema = include_str!("../../../test/fixtures/runtime-optics/workspace_schema.graphql");
let operation = include_str!("../../../test/fixtures/runtime-optics/rename_symbol.graphql");

let artifact = compile_runtime_optic(schema, operation, Some("RenameSymbol"))
.expect("runtime optic should compile");

assert_eq!(
artifact.requirements_artifact.digest,
artifact.requirements_digest
);
assert_eq!(
artifact.requirements_artifact.codec,
OPTIC_ADMISSION_REQUIREMENTS_ARTIFACT_CODEC
);
assert!(!artifact.requirements_artifact.bytes.is_empty());
}

#[test]
fn changing_footprint_law_requirements_changes_requirements_digest() {
let schema = include_str!("../../../test/fixtures/runtime-optics/workspace_schema.graphql");
let operation = include_str!("../../../test/fixtures/runtime-optics/rename_symbol.graphql");

let baseline = compile_runtime_optic(schema, operation, Some("RenameSymbol"))
.expect("baseline runtime optic should compile");

let footprint_changed = operation.replace("\"symbol.index\"", "\"diagnostics\"");
let footprint_artifact =
compile_runtime_optic(schema, &footprint_changed, Some("RenameSymbol"))
.expect("footprint-changed runtime optic should compile");
assert_ne!(
baseline.requirements_digest, footprint_artifact.requirements_digest,
"changing footprint requirements must change requirements digest"
);
assert_ne!(
baseline.requirements_artifact.bytes, footprint_artifact.requirements_artifact.bytes,
"changing footprint requirements must change canonical requirements bytes"
);

let law_changed = operation.replace("bounded.rewrite.v1", "bounded.rewrite.audit.v1");
let law_artifact = compile_runtime_optic(schema, &law_changed, Some("RenameSymbol"))
.expect("law-changed runtime optic should compile");
assert_ne!(
baseline.requirements_digest, law_artifact.requirements_digest,
"changing law requirements must change requirements digest"
);
assert_ne!(
baseline.requirements_artifact.bytes, law_artifact.requirements_artifact.bytes,
"changing law requirements must change canonical requirements bytes"
);
}

#[test]
fn runtime_optic_rejects_invalid_root_argument_bindings() {
let schema = include_str!("../../../test/fixtures/runtime-optics/workspace_schema.graphql");
Expand Down Expand Up @@ -1599,3 +1763,9 @@ fn assert_operation_lowering_error(
Ok(_) => panic!("runtime optic should reject invalid operation"),
}
}

fn sha256_hex(bytes: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(bytes);
hex::encode(hasher.finalize())
}
15 changes: 10 additions & 5 deletions docs/NORTHSTAR.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,8 @@ The runtime handoff should stay explicit:
```text
application declares GraphQL operation
-> Wesley compiles OpticArtifact
-> Wesley returns OpticArtifact plus artifact hash and requirements digest
-> Wesley returns OpticArtifact plus artifact hash, requirements digest,
and canonical requirements artifact
-> application registers artifact with Echo or another runtime registry
-> runtime verifies Wesley artifact hash and stores requirements
-> runtime returns opaque OpticArtifactHandle
Expand All @@ -247,19 +248,23 @@ application declares GraphQL operation
-> runtime emits LawWitness / receipt
```

That flow preserves five separate nouns:
That flow preserves separate nouns:

| Noun | Owner | Job |
| --- | --- | --- |
| `OpticArtifact` | Wesley | Compiled, content-addressed declaration of operation shape, codecs, law claims, and admission requirements. |
| `OpticAdmissionRequirementsArtifact` | Wesley | Canonical requirements bytes, explicit codec, and digest computed from those exact bytes. |
| `OpticRegistrationDescriptor` | Wesley | Artifact id, artifact hash, schema id, operation id, and requirements digest used when registering the artifact with a runtime. |
| `OpticArtifactHandle` | Echo or another runtime | Opaque registry handle proving the runtime accepted and stored a specific Wesley artifact hash. |
| `CapabilityGrant` / `CapabilityPresentation` | User, host, quorum, or policy authority | Bounded authority plus invocation-time proof to attempt a registered artifact under explicit constraints. |
| `LawWitness` / receipt | Echo, runtime, or verifier | Evidence describing admission, obstruction, access, basis, budget, and law satisfaction posture. |

The critical boundary is that Wesley compiles the requirements, while the
runtime registers the artifact, admits the interaction, instruments access, and
witnesses what happened. A runtime handle proves registration, not authority.
witnesses what happened. Wesley owns canonical runtime optic requirement truth:
downstream runtimes may import the requirements bytes, digest, and codec
directly, but must not serialize Wesley structs to create admission truth. A
runtime handle proves registration, not authority.

## What Wesley Must Become

Expand Down Expand Up @@ -326,8 +331,8 @@ identity, artifact hash, operation identity, operation kind, operation name,
canonical root argument bindings, canonical selected field argument bindings,
variable codec shape, response payload codec shape, preserved directive records
from the executable operation and selected field tree, declared footprint, law
claim templates, admission requirements, requirements digest, and an
`OpticRegistrationDescriptor`.
claim templates, structured admission requirements, canonical admission
requirements artifact, requirements digest, and an `OpticRegistrationDescriptor`.
`compile_runtime_optic_registration()` returns just the registration descriptor
for callers that need the cross-process registration reference without
receiving the full in-memory artifact object.
Expand Down
Loading
Loading