From 90d790013ad03ef0d7cbc75336c6153af264020c Mon Sep 17 00:00:00 2001 From: James Ross Date: Thu, 14 May 2026 23:45:31 -0700 Subject: [PATCH 1/2] feat(core): expose canonical runtime optic requirements artifact --- CHANGELOG.md | 9 + crates/wesley-core/src/adapters/apollo.rs | 41 +++-- crates/wesley-core/src/domain/ir.rs | 7 +- crates/wesley-core/src/domain/optic.rs | 22 +++ .../tests/runtime_optic_artifact.rs | 170 ++++++++++++++++++ docs/NORTHSTAR.md | 13 +- docs/SDL.md | 18 ++ 7 files changed, 263 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd8b63f5..f1abb094 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/crates/wesley-core/src/adapters/apollo.rs b/crates/wesley-core/src/adapters/apollo.rs index dffc9a32..67c7a143 100644 --- a/crates/wesley-core/src/adapters/apollo.rs +++ b/crates/wesley-core/src/adapters/apollo.rs @@ -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; @@ -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, }), @@ -1106,6 +1108,7 @@ pub fn compile_runtime_optic( artifact_hash, schema_id, requirements_digest, + requirements_artifact, operation: OpticOperation { operation_id, name: operation_name, @@ -3184,6 +3187,20 @@ fn stable_json_hash(value: &T, area: &str) -> Result( + value: &T, +) -> Result { + 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(value: &T, area: &str) -> Result { to_canonical_json(value) .map_err(|err| lowering_error_value(area, format!("Failed to serialize JSON: {err}"))) diff --git a/crates/wesley-core/src/domain/ir.rs b/crates/wesley-core/src/domain/ir.rs index facab842..76103aaf 100644 --- a/crates/wesley-core/src/domain/ir.rs +++ b/crates/wesley-core/src/domain/ir.rs @@ -166,8 +166,13 @@ pub fn compute_registry_hash(ir: &WesleyIR) -> Result /// 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) diff --git a/crates/wesley-core/src/domain/optic.rs b/crates/wesley-core/src/domain/optic.rs index cd799a91..f3cfe2bb 100644 --- a/crates/wesley-core/src/domain/optic.rs +++ b/crates/wesley-core/src/domain/optic.rs @@ -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")] @@ -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. @@ -77,6 +83,22 @@ pub struct OpticAdmissionRequirements { pub forbidden_resources: Vec, } +/// 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, +} + /// Host, user, quorum, or policy authority grant for invoking an artifact. /// /// Wesley core defines this shape so artifacts and runtime receipts can speak a diff --git a/crates/wesley-core/tests/runtime_optic_artifact.rs b/crates/wesley-core/tests/runtime_optic_artifact.rs index 89cecab7..3c7688b7 100644 --- a/crates/wesley-core/tests/runtime_optic_artifact.rs +++ b/crates/wesley-core/tests/runtime_optic_artifact.rs @@ -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] @@ -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::(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"); @@ -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()) +} diff --git a/docs/NORTHSTAR.md b/docs/NORTHSTAR.md index cf1dbecb..af4effb7 100644 --- a/docs/NORTHSTAR.md +++ b/docs/NORTHSTAR.md @@ -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 @@ -252,6 +253,7 @@ That flow preserves five 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. | @@ -259,7 +261,10 @@ That flow preserves five separate nouns: 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 @@ -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. diff --git a/docs/SDL.md b/docs/SDL.md index 974c4327..ad804b94 100644 --- a/docs/SDL.md +++ b/docs/SDL.md @@ -101,6 +101,24 @@ include `__typename` selections, variable default values, interface inheritance, non-root `@wes_footprint`, duplicate executable directive arguments, and duplicate footprint labels. +## Canonical Runtime Requirements Artifact + +Runtime optic artifacts carry both structured `OpticAdmissionRequirements` for +compiler-side inspection and a Wesley-owned +`OpticAdmissionRequirementsArtifact` for runtime import. + +The requirements artifact contains: + +- `bytes`: canonical requirements bytes generated by Wesley +- `digest`: a digest computed from those exact bytes +- `codec`: the explicit codec, currently + `wesley.requirements.canonical-json.v0` + +Downstream runtimes may import the bytes, digest, and codec directly. They +should not serialize Wesley structs with local `serde_json` settings to create +admission truth, because Wesley owns the canonical runtime optic requirement +bytes. + ## Extension Interpretation Extensions own interpretation. From c0f84e9e729c85839023b9cbd9d4c1d0ae5ee025 Mon Sep 17 00:00:00 2001 From: James Ross Date: Fri, 15 May 2026 03:20:55 -0700 Subject: [PATCH 2/2] Fix: correct runtime optic noun count wording --- docs/NORTHSTAR.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/NORTHSTAR.md b/docs/NORTHSTAR.md index af4effb7..a72846ca 100644 --- a/docs/NORTHSTAR.md +++ b/docs/NORTHSTAR.md @@ -248,7 +248,7 @@ application declares GraphQL operation -> runtime emits LawWitness / receipt ``` -That flow preserves five separate nouns: +That flow preserves separate nouns: | Noun | Owner | Job | | --- | --- | --- |