diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3bc2972e..68b93a51 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,9 @@
 
 ### Added
 
+- `dispatch_optic_intent(...)` and the default `KernelPort` optic dispatch path
+  now reject EINT payloads that use Echo's reserved scheduler/control op id,
+  closing the remaining application-facing control-intent ingress path.
 - `warp-core` optic invocation admission now has a narrow ApertureResolution v0
   boundary. Identity-covered invocations with exact fixture basis bytes
   `basis-request:resolved-fixture:v0` and exact fixture aperture bytes
diff --git a/README.md b/README.md
index 8a91b45a..b67a5a31 100644
--- a/README.md
+++ b/README.md
@@ -27,47 +27,115 @@
 
 # Echo
 
-Echo is the hot runtime optic in the WARP stack.
+Echo is a real-time, deterministic
+[WARP optic](https://github.com/flyingrobots/aion) that participates in the
+[Continuum protocol](https://github.com/flyingrobots/continuum): a protocol for
+exchanging shared, witnessed causal history between distributed peers.
+
+Unlike traditional runtimes that model state as a mutable object graph, Echo
+treats witnessed causal history as the immutable source of truth. State is not
+the substrate; it is materialized on demand as a lawful, witnessed, replayable
+reading over that history. Graph-shaped state is a reading. Files are readings.
+Build outputs are readings. Debugger views are readings. Echo exists to govern
+the integrity of those views without making any one view the territory.
+
+Echo does this through WARP optics: structured, law-named processes for
+observing, transforming, importing, and retaining causal history.
+
+- **Ticking** is an optic that advances a worldline by applying a deterministic
+  batch of canonical intents.
+- **Braiding** is an optic that merges two or more concurrent strands of
+  history under explicit settlement law.
+- **Querying** is an optic that materializes a bounded reading for an observer.
+- **Admission** is an optic that lawfully incorporates transported history from
+  a remote peer.
+
+Because transformations go through lawful optics, Echo can emit computational
+holograms: compact, evidence-bearing boundary artifacts that name the causal
+basis, law, aperture, identity, posture, witnesses, and retained support needed
+to replay or audit a computation. For ticked execution, that support includes
+the initial basis and ordered tick receipts or provenance payloads, so Echo does
+not need to retain every intermediate materialized state as authoritative truth.
+
+Echo operates in a deterministic cycle: it admits canonical intents, schedules
+work, settles speculative paths, emits evidence-bearing receipts for every
+transition, serves bounded observations to clients, and retains the minimal
+artifacts needed for replay or verification.
+
+You can use Echo to write deterministic real-time simulations, verifiable build
+systems, and other applications that demand strong auditability. To get
+started, read [Writing An Echo Application](#writing-an-echo-application) and
+[Application Contract Hosting](docs/architecture/application-contract-hosting.md).
+
+## Developer Experience
+
+Echo is a participant in a larger architecture for provable determinism. Most
+applications do not call Echo with application objects directly. They:
+
+```mermaid
+flowchart LR
+    contract["Author GraphQL contract"]
+    wesley["Compile with Wesley"]
+    helpers["Use generated helpers"]
+    intents["Submit canonical EINT intents"]
+    runtime["Echo-owned tick, scheduling, and settlement"]
+    readings["Observe ReadingEnvelope-backed results"]
+
+    contract --> wesley --> helpers --> intents
+    intents --> runtime --> readings
+```
 
-It does not treat a graph, database, file tree, editor buffer, or in-memory
-object heap as the ultimate truth. Echo's substrate is witnessed causal history:
-admitted transitions, frontiers, receipts, witnesses, patches, checkpoints,
-retained readings, and boundary artifacts.
+Echo handles causal admission, scheduling, receipts, witnesses, retention,
+replay, and bounded observations. The application owns domain semantics.
+Wesley bridges the two by turning authored contracts into generated Echo-facing
+surfaces.
 
-The hard doctrine is:
+### 1. Declare A Contract
 
-```text
-There is no privileged graph.
-There are causal histories and lawful readings of those histories.
-```
+The developer journey starts with GraphQL SDL that names the application's
+data, operations, readings, and law metadata. GraphQL is the authoring surface,
+not Echo's runtime language. Data types define the nouns; operation metadata,
+footprints, policies, capabilities, and constraints define the verbs and laws
+that govern those nouns.
 
-Echo turns that doctrine into runtime machinery.
+### 2. Compile With Wesley
 
-It admits canonical intents, schedules deterministic work, settles speculative
-paths, emits evidence-bearing receipts, serves bounded observations, and retains
-the artifacts needed to replay or verify what happened. Graph-shaped state is a
-reading. Files are readings. Build outputs are readings. Debugger views are
-readings. Echo exists to make those readings lawful, witnessed, and
-replayable.
+The [Wesley](https://github.com/flyingrobots/wesley) compiler lowers the
+authored contract into generated artifacts that Echo can verify and host:
 
-## Thirty Second Version
+- type-safe codecs and operation helpers;
+- registry metadata, operation ids, and artifact identity;
+- footprint certificates and runtime-facing contract metadata.
 
-Echo is a deterministic runtime for admitting canonical intents and producing
-witnessed readings.
+The checked-in Echo path is Rust-first through
+[`echo-wesley-gen`](crates/echo-wesley-gen/README.md). TypeScript and browser
+generation should follow the same contract identity, registry,
+artifact-verification, and footprint-honesty rules instead of inventing a
+parallel Echo API.
 
-Most applications do not call Echo with application objects directly. They:
+### 3. Submit Intents
 
-```text
-author GraphQL contract
-  -> compile with Wesley
-  -> use generated helpers
-  -> dispatch canonical EINT intents
-  -> observe ReadingEnvelope-backed results
-```
+Applications do not mutate Echo state directly, and they do not decide when
+Echo ticks. They submit canonical EINT bytes through `dispatch_intent(...)`.
+That call is ingress: it gives Echo causal input and returns dispatch evidence.
+It is not an application-owned tick command or a domain mutation RPC.
+
+Echo owns the tick boundary, pending-set order, scheduler, and settlement law.
+When Echo reaches a runtime-owned tick boundary, it evaluates pending intents
+against installed contract law, footprints, and scheduler constraints, then
+emits receipts for what was admitted, staged, pluralized, conflicted, or
+obstructed.
+
+### 4. Observe Readings
+
+Clients do not ask Echo for "the state." They ask for a bounded reading from an
+explicit basis under an observer plan.
 
-Echo handles causal admission, receipts, witnesses, retention, replay, and
-bounded observations. The application owns domain semantics. Wesley bridges the
-two by turning authored contracts into typed generated surfaces.
+Generated query helpers build `ObservationRequest` values. Echo returns an
+`ObservationArtifact` containing payload bytes and a `ReadingEnvelope` naming
+the basis, observer, witness references, budget posture, rights posture, and
+whether the reading is complete, residual, plural, or obstructed. Application
+code decodes the payload only after inspecting that evidence.
 
 ## Reader Paths
 
@@ -215,18 +283,23 @@ Echo receives canonical intents and returns witnessed readings.
 The current shape is:
 
 ```text
-Application UI / adapter
-  -> Wesley-generated contract client
+Application UI
+  -> application adapter / Wesley-generated contract client
   -> canonical operation variables
   -> EINT intent bytes
-  -> Echo dispatch_intent(...)
-  -> Echo causal admission and receipts
+  -> host-owned Echo ingress: dispatch_intent(...)
+  -> Echo-owned scheduler tick / settlement
+  -> Echo receipts and witness refs
   -> Echo observe(...)
   -> ReadingEnvelope + payload bytes
   -> generated/application decoding
   -> UI
 ```
 
+The application layer submits canonical bytes and reads observations. It does
+not own the scheduler lifecycle or decide when Echo ticks; host/runtime policy
+owns that authority.
+
 This is why a serious text editor such as `jedit` can own its rope model,
 buffer law, edit-group law, checkpoint policy, and UI behavior while Echo stays
 generic. Echo hosts the generated contract, verifies artifact metadata, admits
@@ -244,24 +317,44 @@ The normal authoring loop is contract-first:
    operation mutates or observes application state.
 4. Run `echo-wesley-gen` to generate Rust contract helpers.
 5. Have the host verify the generated registry/artifact metadata.
-6. Use generated helpers to pack EINT intent bytes and build observation
-   requests.
-7. Let Echo admit the intent, emit receipts, retain witnesses, and return a
+6. Use generated helpers to pack EINT intent bytes and submit them to Echo
+   ingress.
+7. Let Echo's runtime-owned tick cycle admit work, emit receipts, retain
+   witnesses, and make observations available.
+8. Use generated query helpers to build observation requests, then decode and
+   present the returned reading in the application after inspecting its
    `ReadingEnvelope`.
-8. Decode and present the result in the application.
 
 The end-to-end shape is:
 
-```text
-counter.graphql
-  -> echo-wesley-gen
-  -> generated.rs
-  -> verify_contract_artifact(...)
-  -> pack_increment_intent(...)
-  -> dispatch_intent(...)
-  -> counter_value_observation_request(...)
-  -> observe(...)
-  -> inspect ReadingEnvelope
+```mermaid
+flowchart TD
+    contract["counter.graphql"]
+    gen["echo-wesley-gen"]
+    generated["generated.rs"]
+
+    verify["verify_contract_artifact(...)"]
+    pack["pack_increment_intent(...)"]
+    dispatch["dispatch_intent(...)"]
+    tick["Echo-owned tick / settlement"]
+
+    request["counter_value_observation_request(...)"]
+    observe["observe(...)"]
+    envelope["inspect ReadingEnvelope"]
+
+    subgraph compile["Compile Contract"]
+        contract --> gen --> generated
+    end
+
+    subgraph write["Admit Intent"]
+        generated --> verify --> pack --> dispatch --> tick
+    end
+
+    subgraph read["Observe Reading"]
+        generated --> request --> observe --> envelope
+    end
+
+    tick -. "receipts and witness refs" .-> observe
 ```
 
 A tiny contract looks like this:
@@ -299,7 +392,8 @@ cargo run -p echo-wesley-gen -- --schema counter.graphql --out generated.rs
 ```
 
 Application code should use generated helpers rather than hand-rolling Echo
-wire bytes. Conceptually:
+wire bytes. Dispatch submits canonical input to Echo; it does not command Echo
+to tick. Conceptually:
 
 ```rust
 let intent = generated::pack_increment_intent(
@@ -308,14 +402,15 @@ let intent = generated::pack_increment_intent(
     },
 )?;
 
-let response = echo_wasm_abi::kernel_port::KernelPort::dispatch_intent(
+let dispatch_response = echo_wasm_abi::kernel_port::KernelPort::dispatch_intent(
     &mut kernel,
     &intent,
 )?;
 ```
 
-For reads, generated query helpers build `ObservationRequest` values. Echo
-returns an `ObservationArtifact` containing payload bytes plus a
+Echo's runtime owns the tick cadence, scheduler, settlement, and receipt
+emission. For reads, generated query helpers build `ObservationRequest` values.
+Echo returns an `ObservationArtifact` containing payload bytes plus a
 `ReadingEnvelope`; the application should inspect that envelope before treating
 the reading as complete.
 
diff --git a/crates/echo-wasm-abi/src/kernel_port.rs b/crates/echo-wasm-abi/src/kernel_port.rs
index 544fc5fa..8ab7bb3a 100644
--- a/crates/echo-wasm-abi/src/kernel_port.rs
+++ b/crates/echo-wasm-abi/src/kernel_port.rs
@@ -316,6 +316,8 @@ pub mod error_codes {
     pub const UNSUPPORTED_OBSERVATION_RIGHTS: u32 = 17;
     /// The requested observation exceeded its explicit read budget.
     pub const OBSERVATION_BUDGET_EXCEEDED: u32 = 18;
+    /// Application-facing intent ingress rejected a privileged control intent.
+    pub const FORBIDDEN_CONTROL_INTENT: u32 = 19;
 }
 
 // ---------------------------------------------------------------------------
@@ -499,7 +501,7 @@ pub struct WriterHeadKey {
     pub head_id: HeadId,
 }
 
-/// Privileged control intents routed through the same intent intake surface.
+/// Privileged control intents routed through trusted runtime control.
 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
 #[serde(tag = "kind", rename_all = "snake_case")]
 pub enum ControlIntentV1 {
@@ -2503,10 +2505,11 @@ fn coordinate_at_tick(at: &CoordinateAt) -> Option<u64> {
 /// WASM is single-threaded, so `KernelPort` does not require `Send` or `Sync`.
 /// Native test harnesses should use appropriate synchronization if needed.
 pub trait KernelPort {
-    /// Ingest a canonical intent envelope into the kernel inbox.
+    /// Ingest a canonical application intent envelope into the kernel inbox.
     ///
     /// The kernel content-addresses the intent and returns whether it was
-    /// newly accepted or a duplicate.
+    /// newly accepted or a duplicate. This is application-facing ingress and
+    /// must not accept [`ControlIntentV1`] envelopes.
     fn dispatch_intent(&mut self, intent_bytes: &[u8]) -> Result<DispatchResponse, AbiError>;
 
     /// Returns the current coordinate for an optic focus when the implementation
@@ -2541,12 +2544,24 @@ pub trait KernelPort {
 
         match &request.payload {
             OpticIntentPayload::EintV1 { bytes } => {
-                if let Err(error) = crate::unpack_intent_v1(bytes) {
-                    return Ok(optic_dispatch_obstruction(
-                        &request,
-                        OpticObstructionKind::UnsupportedIntentFamily,
-                        format!("optic dispatch EINT v1 payload is malformed: {error}"),
-                    ));
+                let (op_id, _) = match crate::unpack_intent_v1(bytes) {
+                    Ok(intent) => intent,
+                    Err(error) => {
+                        return Ok(optic_dispatch_obstruction(
+                            &request,
+                            OpticObstructionKind::UnsupportedIntentFamily,
+                            format!("optic dispatch EINT v1 payload is malformed: {error}"),
+                        ));
+                    }
+                };
+
+                if op_id == crate::CONTROL_INTENT_V1_OP_ID {
+                    return Err(AbiError {
+                        code: error_codes::FORBIDDEN_CONTROL_INTENT,
+                        message:
+                            "application optic dispatch cannot carry scheduler control intents"
+                                .into(),
+                    });
                 }
 
                 let dispatch = self.dispatch_intent(bytes)?;
@@ -2655,7 +2670,20 @@ pub trait KernelPort {
     ///
     /// This call is side-effect free. Implementations may report a run that has
     /// already completed by the time the host polls here; for example, a
-    /// synchronous `Start` can return from `dispatch_intent(...)` with
+    /// synchronous `Start` can return from trusted runtime control with
     /// `state = Inactive` and `last_run_completion` populated immediately.
     fn scheduler_status(&self) -> Result<SchedulerStatus, AbiError>;
 }
+
+/// Privileged scheduler/runtime control boundary for trusted runtime owners.
+///
+/// Application-facing adapters should accept only [`KernelPort`] when they need
+/// intent ingress, observation, status, or registry metadata. Host/runtime
+/// owners that control scheduler lifecycle may require this separate trait.
+pub trait TrustedKernelControlPort: KernelPort {
+    /// Execute a privileged scheduler/runtime control intent.
+    fn dispatch_control_intent_trusted(
+        &mut self,
+        intent: ControlIntentV1,
+    ) -> Result<DispatchResponse, AbiError>;
+}
diff --git a/crates/echo-wasm-abi/src/lib.rs b/crates/echo-wasm-abi/src/lib.rs
index 7c1e974c..78b8e16c 100644
--- a/crates/echo-wasm-abi/src/lib.rs
+++ b/crates/echo-wasm-abi/src/lib.rs
@@ -1381,6 +1381,118 @@ mod tests {
         ));
     }
 
+    #[test]
+    fn test_kernel_port_dispatch_optic_intent_rejects_control_eint_v1() {
+        use crate::kernel_port::{
+            AdmissionLawId, ControlIntentV1, CoordinateAt, DispatchOpticIntentRequest,
+            DispatchResponse, EchoCoordinate, GlobalTick, IntentFamilyId, KernelPort, OpticActorId,
+            OpticCapability, OpticCapabilityId, OpticCause, OpticFocus, OpticId,
+            OpticIntentPayload, OpticReadBudget, ProjectionVersion, RunCompletion, RunId,
+            SchedulerMode, SchedulerState, SchedulerStatus, WorkState, WorldlineId,
+        };
+
+        struct PermissiveKernel;
+
+        impl KernelPort for PermissiveKernel {
+            fn dispatch_intent(
+                &mut self,
+                _intent_bytes: &[u8],
+            ) -> Result<DispatchResponse, kernel_port::AbiError> {
+                Ok(DispatchResponse {
+                    accepted: true,
+                    intent_id: vec![10; 32],
+                    scheduler_status: SchedulerStatus {
+                        state: SchedulerState::Inactive,
+                        active_mode: Some(SchedulerMode::UntilIdle {
+                            cycle_limit: Some(1),
+                        }),
+                        work_state: WorkState::Quiescent,
+                        run_id: Some(RunId(1)),
+                        latest_cycle_global_tick: Some(GlobalTick(1)),
+                        latest_commit_global_tick: Some(GlobalTick(1)),
+                        last_quiescent_global_tick: Some(GlobalTick(1)),
+                        last_run_completion: Some(RunCompletion::Quiesced),
+                    },
+                })
+            }
+
+            fn registry_info(&self) -> kernel_port::RegistryInfo {
+                kernel_port::RegistryInfo {
+                    codec_id: None,
+                    registry_version: None,
+                    schema_sha256_hex: None,
+                    abi_version: kernel_port::ABI_VERSION,
+                }
+            }
+
+            fn scheduler_status(&self) -> Result<SchedulerStatus, kernel_port::AbiError> {
+                Ok(SchedulerStatus {
+                    state: SchedulerState::Inactive,
+                    active_mode: None,
+                    work_state: WorkState::Quiescent,
+                    run_id: None,
+                    latest_cycle_global_tick: None,
+                    latest_commit_global_tick: None,
+                    last_quiescent_global_tick: None,
+                    last_run_completion: None,
+                })
+            }
+        }
+
+        let worldline_id = WorldlineId::from_bytes([3; 32]);
+        let focus = OpticFocus::Worldline { worldline_id };
+        let actor = OpticActorId::from_bytes([4; 32]);
+        let intent_family = IntentFamilyId::from_bytes([5; 32]);
+        let request = DispatchOpticIntentRequest {
+            optic_id: OpticId::from_bytes([1; 32]),
+            base_coordinate: EchoCoordinate::Worldline {
+                worldline_id,
+                at: CoordinateAt::Frontier,
+            },
+            intent_family,
+            focus: focus.clone(),
+            cause: OpticCause {
+                actor,
+                cause_hash: vec![6; 32],
+                label: Some("optic dispatch".into()),
+            },
+            capability: OpticCapability {
+                capability_id: OpticCapabilityId::from_bytes([7; 32]),
+                actor,
+                issuer_ref: None,
+                policy_hash: vec![8; 32],
+                allowed_focus: focus,
+                projection_version: ProjectionVersion(1),
+                reducer_version: None,
+                allowed_intent_family: intent_family,
+                max_budget: OpticReadBudget {
+                    max_bytes: Some(4096),
+                    max_nodes: Some(64),
+                    max_ticks: Some(8),
+                    max_attachments: Some(0),
+                },
+            },
+            admission_law: AdmissionLawId::from_bytes([9; 32]),
+            payload: OpticIntentPayload::EintV1 {
+                bytes: pack_control_intent_v1(&ControlIntentV1::Start {
+                    mode: SchedulerMode::UntilIdle {
+                        cycle_limit: Some(1),
+                    },
+                })
+                .unwrap(),
+            },
+        };
+
+        let error = PermissiveKernel
+            .dispatch_optic_intent(request)
+            .expect_err("control EINT must be rejected before dispatch_intent");
+
+        assert_eq!(
+            error.code,
+            kernel_port::error_codes::FORBIDDEN_CONTROL_INTENT
+        );
+    }
+
     #[test]
     fn test_optic_open_close_models_round_trip() {
         use crate::kernel_port::{
diff --git a/crates/echo-wesley-gen/src/main.rs b/crates/echo-wesley-gen/src/main.rs
index 18ba2f3b..eedad279 100644
--- a/crates/echo-wesley-gen/src/main.rs
+++ b/crates/echo-wesley-gen/src/main.rs
@@ -23,6 +23,7 @@ use ir::{OpKind, TypeKind, WesleyIR};
 const ECHO_IR_VERSION: &str = "echo-ir/v1";
 const DEFAULT_CODEC_ID: &str = "cbor-canon-v1";
 const DEFAULT_REGISTRY_VERSION: u32 = 1;
+const RESERVED_CONTROL_OP_ID: u32 = u32::MAX;
 const WESLEY_CORE_VERSION: &str = "0.0.4";
 
 #[derive(Parser)]
@@ -93,6 +94,14 @@ fn echo_ir_from_schema_sdl(schema_sdl: &str) -> Result<WesleyIR> {
                 operation.field_name
             );
         }
+        if op_id == RESERVED_CONTROL_OP_ID {
+            bail!(
+                "generated operation id for {:?} `{}` uses Echo's reserved control op id; \
+                 add explicit operation ids upstream before generating Echo artifacts",
+                operation.operation_type,
+                operation.field_name
+            );
+        }
         if let Some((existing_type, existing_name)) = used_op_ids.insert(
             op_id,
             (operation.operation_type, operation.field_name.clone()),
@@ -211,6 +220,7 @@ fn fnv1a_step(hash: u32, byte: u8) -> u32 {
 }
 
 fn generate_rust(ir: &WesleyIR, args: &Args) -> Result<String> {
+    validate_operation_ids(ir)?;
     validate_generated_item_names(ir)?;
 
     let mut tokens = quote! {
@@ -826,6 +836,20 @@ fn generate_rust(ir: &WesleyIR, args: &Args) -> Result<String> {
     Ok(prettyplease::unparse(&syntax_tree))
 }
 
+fn validate_operation_ids(ir: &WesleyIR) -> Result<()> {
+    for op in &ir.ops {
+        if op.op_id == RESERVED_CONTROL_OP_ID {
+            bail!(
+                "operation `{}` uses Echo's reserved control op id {RESERVED_CONTROL_OP_ID}; \
+                 application contracts must not generate scheduler control intents",
+                op.name
+            );
+        }
+    }
+
+    Ok(())
+}
+
 fn op_const_ident(name: &str, op_id: u32) -> proc_macro2::Ident {
     format_ident!("{}", op_const_name(name, op_id))
 }
diff --git a/crates/echo-wesley-gen/tests/generation.rs b/crates/echo-wesley-gen/tests/generation.rs
index 96952f58..09c9b116 100644
--- a/crates/echo-wesley-gen/tests/generation.rs
+++ b/crates/echo-wesley-gen/tests/generation.rs
@@ -943,6 +943,42 @@ fn assert_generated_crate_checks(crate_dir: &Path) {
     );
 }
 
+#[test]
+fn test_reserved_control_op_id_fails_closed() {
+    let ir = r#"{
+        "ir_version": "echo-ir/v1",
+        "schema_sha256": "abc123",
+        "codec_id": "cbor-canon-v1",
+        "registry_version": 1,
+        "types": [
+            { "name": "RunStatus", "kind": "OBJECT", "fields": [
+                { "name": "accepted", "type": "Boolean", "required": true }
+            ] }
+        ],
+        "ops": [
+            {
+                "kind": "MUTATION",
+                "name": "startScheduler",
+                "op_id": 4294967295,
+                "args": [],
+                "result_type": "RunStatus"
+            }
+        ]
+    }"#;
+
+    let output = run_wesley_gen(ir);
+    assert!(
+        !output.status.success(),
+        "reserved control op id must fail closed"
+    );
+    let stderr = String::from_utf8_lossy(&output.stderr);
+    assert!(
+        stderr.contains("reserved control op id"),
+        "stderr did not explain reserved control id: {}",
+        String::from_utf8_lossy(&output.stderr)
+    );
+}
+
 #[test]
 fn test_generated_optic_helper_shape_compiles_against_abi() {
     let crate_dir = write_optic_binding_smoke_crate();
diff --git a/crates/warp-core/src/optic_artifact.rs b/crates/warp-core/src/optic_artifact.rs
index 17907068..eb488a9a 100644
--- a/crates/warp-core/src/optic_artifact.rs
+++ b/crates/warp-core/src/optic_artifact.rs
@@ -598,6 +598,7 @@ pub struct OpticInvocation {
 
 const OPTIC_BASIS_RESOLUTION_V0_FIXTURE_BYTES: &[u8] = b"basis-request:resolved-fixture:v0";
 const OPTIC_APERTURE_RESOLUTION_V0_FIXTURE_BYTES: &[u8] = b"aperture-request:resolved-fixture:v0";
+const OPTIC_BUDGET_RESOLUTION_V0_FIXTURE_BYTES: &[u8] = b"budget-request:resolved-fixture:v0";
 
 /// Admission obstruction for an optic invocation.
 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -1189,8 +1190,13 @@ impl OpticArtifactRegistry {
             ) {
                 final_obstruction = Self::resolve_basis_v0(&invocation.basis_request)
                     .unwrap_or_else(|| {
-                        Self::resolve_aperture_v0(&invocation.aperture_request)
-                            .unwrap_or(OpticInvocationObstruction::UnsupportedBudgetResolution)
+                        Self::resolve_aperture_v0(&invocation.aperture_request).unwrap_or_else(
+                            || {
+                                Self::resolve_budget_v0(&invocation.budget_request).unwrap_or(
+                                    OpticInvocationObstruction::RuntimeSupportUnavailable,
+                                )
+                            },
+                        )
                     });
             }
         }
@@ -1226,6 +1232,16 @@ impl OpticArtifactRegistry {
         Some(OpticInvocationObstruction::UnsupportedApertureResolution)
     }
 
+    fn resolve_budget_v0(
+        budget_request: &OpticBudgetRequest,
+    ) -> Option<OpticInvocationObstruction> {
+        if budget_request.bytes == OPTIC_BUDGET_RESOLUTION_V0_FIXTURE_BYTES {
+            return None;
+        }
+
+        Some(OpticInvocationObstruction::UnsupportedBudgetResolution)
+    }
+
     fn classify_aperture_request(
         aperture_request: &OpticApertureRequest,
     ) -> Option<OpticInvocationObstruction> {
diff --git a/crates/warp-core/tests/optic_invocation_admission_tests.rs b/crates/warp-core/tests/optic_invocation_admission_tests.rs
index 515199dd..4618fbe6 100644
--- a/crates/warp-core/tests/optic_invocation_admission_tests.rs
+++ b/crates/warp-core/tests/optic_invocation_admission_tests.rs
@@ -123,6 +123,18 @@ fn fixture_invocation_with_resolved_basis_aperture_and_presentation(
     invocation
 }
 
+fn fixture_invocation_with_resolved_basis_aperture_budget_and_presentation(
+    handle: OpticArtifactHandle,
+    grant_id: &str,
+) -> OpticInvocation {
+    let mut invocation =
+        fixture_invocation_with_resolved_basis_aperture_and_presentation(handle, grant_id);
+    invocation.budget_request = OpticBudgetRequest {
+        bytes: b"budget-request:resolved-fixture:v0".to_vec(),
+    };
+    invocation
+}
+
 fn expected_obstructed_posture(
     invocation: &OpticInvocation,
     obstruction: OpticInvocationObstruction,
@@ -945,6 +957,106 @@ fn resolved_aperture_still_obstructs_before_budget_resolution() -> Result<(), St
     Ok(())
 }
 
+#[test]
+fn unsupported_budget_still_stops_at_unsupported_budget_resolution() -> Result<(), String> {
+    let (mut registry, handle) = fixture_registry_and_handle()?;
+    let invocation =
+        fixture_invocation_with_resolved_basis_aperture_and_presentation(handle, "grant:covered");
+    let mut gate = fixture_gate_with_grant(fixture_grant("grant:covered"));
+
+    let outcome = registry.admit_optic_invocation_with_capability_validator(&invocation, &mut gate);
+
+    assert_eq!(
+        obstruction_for(&outcome),
+        OpticInvocationObstruction::UnsupportedBudgetResolution
+    );
+    assert_ne!(
+        obstruction_for(&outcome),
+        OpticInvocationObstruction::RuntimeSupportUnavailable
+    );
+    Ok(())
+}
+
+#[test]
+fn budget_resolution_is_unreachable_when_basis_resolution_fails() -> Result<(), String> {
+    let (mut registry, handle) = fixture_registry_and_handle()?;
+    let mut invocation = fixture_invocation_with_resolved_basis_aperture_budget_and_presentation(
+        handle,
+        "grant:covered",
+    );
+    invocation.basis_request = OpticBasisRequest {
+        bytes: b"basis-request:unsupported".to_vec(),
+    };
+    let mut gate = fixture_gate_with_grant(fixture_grant("grant:covered"));
+
+    let outcome = registry.admit_optic_invocation_with_capability_validator(&invocation, &mut gate);
+
+    assert_eq!(
+        obstruction_for(&outcome),
+        OpticInvocationObstruction::UnsupportedBasisResolution
+    );
+    assert_ne!(
+        obstruction_for(&outcome),
+        OpticInvocationObstruction::RuntimeSupportUnavailable
+    );
+    Ok(())
+}
+
+#[test]
+fn budget_resolution_is_unreachable_when_aperture_resolution_fails() -> Result<(), String> {
+    let (mut registry, handle) = fixture_registry_and_handle()?;
+    let mut invocation = fixture_invocation_with_resolved_basis_aperture_budget_and_presentation(
+        handle,
+        "grant:covered",
+    );
+    invocation.aperture_request = OpticApertureRequest {
+        bytes: b"aperture-request:unsupported".to_vec(),
+    };
+    let mut gate = fixture_gate_with_grant(fixture_grant("grant:covered"));
+
+    let outcome = registry.admit_optic_invocation_with_capability_validator(&invocation, &mut gate);
+
+    assert_eq!(
+        obstruction_for(&outcome),
+        OpticInvocationObstruction::UnsupportedApertureResolution
+    );
+    assert_ne!(
+        obstruction_for(&outcome),
+        OpticInvocationObstruction::RuntimeSupportUnavailable
+    );
+    Ok(())
+}
+
+#[test]
+fn resolved_budget_still_obstructs_before_runtime_support() -> Result<(), String> {
+    let (mut registry, handle) = fixture_registry_and_handle()?;
+    let invocation = fixture_invocation_with_resolved_basis_aperture_budget_and_presentation(
+        handle,
+        "grant:covered",
+    );
+    let mut gate = fixture_gate_with_grant(fixture_grant("grant:covered"));
+
+    let outcome = registry.admit_optic_invocation_with_capability_validator(&invocation, &mut gate);
+
+    assert_eq!(
+        obstruction_for(&outcome),
+        OpticInvocationObstruction::RuntimeSupportUnavailable
+    );
+    assert!(matches!(
+        latest_invocation_obstruction_fact(&registry)?,
+        GraphFact::OpticInvocationObstructed {
+            budget_request_digest,
+            obstruction,
+            ..
+        } if *budget_request_digest == digest_invocation_request_bytes(
+                b"echo.optic-invocation.budget-request.v0",
+                b"budget-request:resolved-fixture:v0"
+            )
+            && *obstruction == InvocationObstructionKind::RuntimeSupportUnavailable
+    ));
+    Ok(())
+}
+
 #[test]
 fn invocation_obstruction_fact_is_not_counterfactual_candidate() -> Result<(), String> {
     let (mut registry, handle) = fixture_registry_and_handle()?;
diff --git a/crates/warp-wasm/README.md b/crates/warp-wasm/README.md
index 320439ef..e30056d7 100644
--- a/crates/warp-wasm/README.md
+++ b/crates/warp-wasm/README.md
@@ -15,16 +15,17 @@ See the repository root `README.md` for the full overview.
 - Exposes the current observation-first and intent-shaped control surface
   (`ABI_VERSION` 9 in `echo-wasm-abi`): `observe(...)` is the only public
   world-state read export, `scheduler_status()` is the read-only scheduler
-  metadata export, and `dispatch_intent(...)` is the write/control ingress.
+  metadata export, and `dispatch_intent(...)` is application intent ingress.
   The current ABI also publishes strand settlement comparison, planning,
   execution entrypoints, settlement basis evidence, overlap revalidation
   evidence, and read-side basis plus residual posture on observation artifacts.
 - The engine-backed boundary uses logical clocks only:
   `WorldlineTick` is per-worldline append identity and `GlobalTick` is runtime
   cycle correlation metadata. No wall-clock time enters Echo internals.
-- Public scheduler control is expressed as privileged control intents packed
-  into the same EINT envelope format as domain intents. The canonical control
-  surface includes `Start`, `Stop`, and `SetHeadEligibility`.
+- Public application dispatch rejects privileged scheduler control envelopes.
+  Trusted runtime control is kept on a separate Rust host/runtime-owner trait,
+  not on the public browser application dispatch export. Browser adapters must
+  not hand the raw kernel/control surface to untrusted application code.
 - Intended to power future browser-based visualizers and inspectors built on
   top of the same core engine as native tools.
 
diff --git a/crates/warp-wasm/src/lib.rs b/crates/warp-wasm/src/lib.rs
index 2248c021..79e17ac4 100644
--- a/crates/warp-wasm/src/lib.rs
+++ b/crates/warp-wasm/src/lib.rs
@@ -31,8 +31,9 @@ use wasm_bindgen::JsValue;
 use echo_wasm_abi::kernel_port::HeadInfo;
 use echo_wasm_abi::kernel_port::{
     self, AbiError, DispatchOpticIntentRequest, ErrEnvelope, KernelPort, ObservationRequest,
-    ObserveOpticRequest, OkEnvelope, SettlementRequest,
+    ObserveOpticRequest, OkEnvelope, OpticIntentPayload, SettlementRequest,
 };
+use echo_wasm_abi::{unpack_intent_v1, CONTROL_INTENT_V1_OP_ID};
 
 use std::cell::RefCell;
 
@@ -65,7 +66,9 @@ pub fn install_kernel(kernel: Box<dyn KernelPort>) {
 /// keeps generated-contract smoke tests off `js_sys::Uint8Array` while
 /// exercising the same installed-kernel envelope contract.
 pub fn dispatch_intent_cbor(intent_bytes: &[u8]) -> Vec<u8> {
-    encode_result_bytes(with_kernel(|k| k.dispatch_intent(intent_bytes)))
+    encode_result_bytes(with_kernel(|k| {
+        dispatch_application_intent(k, intent_bytes)
+    }))
 }
 
 /// Observe through the installed kernel and return a CBOR success/error
@@ -135,6 +138,55 @@ where
     })
 }
 
+fn validate_application_intent_bytes(intent_bytes: &[u8]) -> Result<(), AbiError> {
+    let (op_id, _) = unpack_intent_v1(intent_bytes).map_err(|error| AbiError {
+        code: kernel_port::error_codes::INVALID_INTENT,
+        message: format!(
+            "malformed EINT envelope ({} bytes): {error}",
+            intent_bytes.len()
+        ),
+    })?;
+
+    if op_id == CONTROL_INTENT_V1_OP_ID {
+        return Err(AbiError {
+            code: kernel_port::error_codes::FORBIDDEN_CONTROL_INTENT,
+            message: "application dispatch cannot carry scheduler control intents".into(),
+        });
+    }
+
+    Ok(())
+}
+
+fn dispatch_application_intent(
+    kernel: &mut dyn KernelPort,
+    intent_bytes: &[u8],
+) -> Result<kernel_port::DispatchResponse, AbiError> {
+    validate_application_intent_bytes(intent_bytes)?;
+    kernel.dispatch_intent(intent_bytes)
+}
+
+fn validate_application_optic_intent_request(
+    request: &DispatchOpticIntentRequest,
+) -> Result<(), AbiError> {
+    match &request.payload {
+        OpticIntentPayload::EintV1 { bytes } => validate_application_intent_bytes(bytes),
+    }
+}
+
+fn dispatch_optic_intent_result(
+    request_bytes: &[u8],
+) -> Result<kernel_port::IntentDispatchResult, AbiError> {
+    let request =
+        echo_wasm_abi::decode_cbor::<DispatchOpticIntentRequest>(request_bytes).map_err(|err| {
+            AbiError {
+                code: kernel_port::error_codes::INVALID_PAYLOAD,
+                message: format!("invalid optic dispatch request payload: {err}"),
+            }
+        })?;
+    validate_application_optic_intent_request(&request)?;
+    with_kernel(|k| k.dispatch_optic_intent(request))
+}
+
 // ---------------------------------------------------------------------------
 // CBOR envelope encoding
 // ---------------------------------------------------------------------------
@@ -295,7 +347,9 @@ pub fn init() -> Uint8Array {
 /// on success, or an error envelope.
 #[wasm_bindgen]
 pub fn dispatch_intent(intent_bytes: &[u8]) -> Uint8Array {
-    encode_result(with_kernel(|k| k.dispatch_intent(intent_bytes)))
+    encode_result(with_kernel(|k| {
+        dispatch_application_intent(k, intent_bytes)
+    }))
 }
 
 /// Propose an intent through an explicit optic dispatch request.
@@ -303,16 +357,7 @@ pub fn dispatch_intent(intent_bytes: &[u8]) -> Uint8Array {
 /// The request bytes must decode as canonical-CBOR `DispatchOpticIntentRequest`.
 #[wasm_bindgen]
 pub fn dispatch_optic_intent(request_bytes: &[u8]) -> Uint8Array {
-    let request = match echo_wasm_abi::decode_cbor::<DispatchOpticIntentRequest>(request_bytes) {
-        Ok(request) => request,
-        Err(err) => {
-            return encode_err(&AbiError {
-                code: kernel_port::error_codes::INVALID_PAYLOAD,
-                message: format!("invalid optic dispatch request payload: {err}"),
-            })
-        }
-    };
-    encode_result(with_kernel(|k| k.dispatch_optic_intent(request)))
+    encode_result(dispatch_optic_intent_result(request_bytes))
 }
 
 /// Observe through an explicit optic request.
@@ -696,12 +741,15 @@ mod schema_validation_tests {
 mod init_tests {
     use super::*;
     use echo_wasm_abi::kernel_port::{
-        AdmissionOutcomeKind, BaseRef, BuiltinObserverPlan, ConflictArtifactDraft, ConflictReason,
-        DispatchResponse, GlobalTick, HeadInfo, HeadObservation, NeighborhoodCore,
-        NeighborhoodParticipant, NeighborhoodParticipantRole, NeighborhoodPlurality,
-        NeighborhoodSite, NeighborhoodSiteId, ObservationArtifact, ObservationAt,
-        ObservationBasisPosture, ObservationFrame, ObservationPayload, ObservationProjection,
-        ParticipantRole, ProvenanceRef, ReadingBudgetPosture, ReadingEnvelope,
+        AdmissionLawId, AdmissionOutcomeKind, BaseRef, BuiltinObserverPlan, ConflictArtifactDraft,
+        ConflictReason, ControlIntentV1, CoordinateAt, DispatchOpticIntentRequest,
+        DispatchResponse, EchoCoordinate, GlobalTick, HeadInfo, HeadObservation, IntentFamilyId,
+        NeighborhoodCore, NeighborhoodParticipant, NeighborhoodParticipantRole,
+        NeighborhoodPlurality, NeighborhoodSite, NeighborhoodSiteId, ObservationArtifact,
+        ObservationAt, ObservationBasisPosture, ObservationFrame, ObservationPayload,
+        ObservationProjection, OpticActorId, OpticCapability, OpticCapabilityId, OpticCause,
+        OpticFocus, OpticId, OpticIntentPayload, OpticReadBudget, ParticipantRole,
+        ProjectionVersion, ProvenanceRef, ReadingBudgetPosture, ReadingEnvelope,
         ReadingObserverBasis, ReadingObserverPlan, ReadingResidualPosture, ReadingRightsPosture,
         ReadingWitnessRef, RegistryInfo, ResolvedObservationCoordinate, RunCompletion, RunId,
         SchedulerMode, SchedulerState, SchedulerStatus, SettlementBasisReport, SettlementDecision,
@@ -975,6 +1023,91 @@ mod init_tests {
         assert_eq!(err.code, kernel_port::error_codes::NOT_INITIALIZED);
     }
 
+    #[test]
+    fn public_dispatch_rejects_packed_control_intent_start() {
+        clear_kernel();
+        install_kernel(Box::new(StubKernel));
+        let control = echo_wasm_abi::pack_control_intent_v1(&ControlIntentV1::Start {
+            mode: SchedulerMode::UntilIdle {
+                cycle_limit: Some(1),
+            },
+        })
+        .unwrap();
+
+        let response = dispatch_intent_cbor(&control);
+        let err: ErrEnvelope = echo_wasm_abi::decode_cbor(&response).unwrap();
+
+        assert_eq!(err.code, kernel_port::error_codes::FORBIDDEN_CONTROL_INTENT);
+    }
+
+    #[test]
+    fn public_dispatch_rejects_hand_built_reserved_control_op_id() {
+        clear_kernel();
+        install_kernel(Box::new(StubKernel));
+        let mut bytes = Vec::new();
+        bytes.extend_from_slice(b"EINT");
+        bytes.extend_from_slice(&CONTROL_INTENT_V1_OP_ID.to_le_bytes());
+        bytes.extend_from_slice(&0u32.to_le_bytes());
+
+        let response = dispatch_intent_cbor(&bytes);
+        let err: ErrEnvelope = echo_wasm_abi::decode_cbor(&response).unwrap();
+
+        assert_eq!(err.code, kernel_port::error_codes::FORBIDDEN_CONTROL_INTENT);
+    }
+
+    #[test]
+    fn public_optic_dispatch_rejects_hand_built_reserved_control_op_id() {
+        clear_kernel();
+        install_kernel(Box::new(StubKernel));
+
+        let worldline_id = WorldlineId::from_bytes([3; 32]);
+        let focus = OpticFocus::Worldline { worldline_id };
+        let actor = OpticActorId::from_bytes([4; 32]);
+        let intent_family = IntentFamilyId::from_bytes([5; 32]);
+        let mut bytes = Vec::new();
+        bytes.extend_from_slice(b"EINT");
+        bytes.extend_from_slice(&CONTROL_INTENT_V1_OP_ID.to_le_bytes());
+        bytes.extend_from_slice(&0u32.to_le_bytes());
+
+        let request = DispatchOpticIntentRequest {
+            optic_id: OpticId::from_bytes([1; 32]),
+            base_coordinate: EchoCoordinate::Worldline {
+                worldline_id,
+                at: CoordinateAt::Frontier,
+            },
+            intent_family,
+            focus: focus.clone(),
+            cause: OpticCause {
+                actor,
+                cause_hash: vec![6; 32],
+                label: Some("optic dispatch".into()),
+            },
+            capability: OpticCapability {
+                capability_id: OpticCapabilityId::from_bytes([7; 32]),
+                actor,
+                issuer_ref: None,
+                policy_hash: vec![8; 32],
+                allowed_focus: focus,
+                projection_version: ProjectionVersion(1),
+                reducer_version: None,
+                allowed_intent_family: intent_family,
+                max_budget: OpticReadBudget {
+                    max_bytes: Some(4096),
+                    max_nodes: Some(64),
+                    max_ticks: Some(8),
+                    max_attachments: Some(0),
+                },
+            },
+            admission_law: AdmissionLawId::from_bytes([9; 32]),
+            payload: OpticIntentPayload::EintV1 { bytes },
+        };
+        let request_bytes = echo_wasm_abi::encode_cbor(&request).unwrap();
+
+        let err = dispatch_optic_intent_result(&request_bytes).unwrap_err();
+
+        assert_eq!(err.code, kernel_port::error_codes::FORBIDDEN_CONTROL_INTENT);
+    }
+
     #[test]
     fn init_failure_clears_preexisting_kernel() {
         clear_kernel();
diff --git a/crates/warp-wasm/src/warp_kernel.rs b/crates/warp-wasm/src/warp_kernel.rs
index c2f5fcfe..6c26ae83 100644
--- a/crates/warp-wasm/src/warp_kernel.rs
+++ b/crates/warp-wasm/src/warp_kernel.rs
@@ -32,11 +32,12 @@ use echo_wasm_abi::kernel_port::{
     RetainedReadingKey as AbiRetainedReadingKey, RunCompletion, RunId as AbiRunId, SchedulerMode,
     SchedulerState, SchedulerStatus, SettlementDelta as AbiSettlementDelta,
     SettlementPlan as AbiSettlementPlan, SettlementRequest as AbiSettlementRequest,
-    SettlementResult as AbiSettlementResult, WorkState, WorldlineId as AbiWorldlineId,
-    WorldlineTick as AbiWorldlineTick, WriterHeadKey as AbiWriterHeadKey, ABI_VERSION,
+    SettlementResult as AbiSettlementResult, TrustedKernelControlPort, WorkState,
+    WorldlineId as AbiWorldlineId, WorldlineTick as AbiWorldlineTick,
+    WriterHeadKey as AbiWriterHeadKey, ABI_VERSION,
 };
 use echo_wasm_abi::{
-    unpack_control_intent_v1, unpack_import_suffix_intent_v1, unpack_intent_v1,
+    pack_control_intent_v1, unpack_import_suffix_intent_v1, unpack_intent_v1,
     CONTROL_INTENT_V1_OP_ID, IMPORT_SUFFIX_INTENT_V1_OP_ID,
 };
 use warp_core::{
@@ -1043,41 +1044,23 @@ impl KernelPort for WarpKernel {
             ),
         })?;
 
-        let intent_id = if op_id == CONTROL_INTENT_V1_OP_ID {
-            IngressEnvelope::local_intent(
-                IngressTarget::DefaultWriter {
-                    worldline_id: self.default_worldline,
-                },
-                make_intent_kind("echo.control/eint-v1"),
-                intent_bytes.to_vec(),
-            )
-            .ingress_id()
-            .to_vec()
-        } else {
-            IngressEnvelope::local_intent(
-                IngressTarget::DefaultWriter {
-                    worldline_id: self.default_worldline,
-                },
-                make_intent_kind("echo.intent/eint-v1"),
-                intent_bytes.to_vec(),
-            )
-            .ingress_id()
-            .to_vec()
-        };
-
         if op_id == CONTROL_INTENT_V1_OP_ID {
-            let control = unpack_control_intent_v1(intent_bytes).map_err(|_| AbiError {
-                code: error_codes::INVALID_CONTROL,
-                message: "invalid control intent envelope".into(),
-            })?;
-            self.apply_control_intent(control)?;
-            return Ok(DispatchResponse {
-                accepted: true,
-                intent_id,
-                scheduler_status: self.scheduler_status()?,
+            return Err(AbiError {
+                code: error_codes::FORBIDDEN_CONTROL_INTENT,
+                message: "application dispatch cannot carry scheduler control intents".into(),
             });
         }
 
+        let intent_id = IngressEnvelope::local_intent(
+            IngressTarget::DefaultWriter {
+                worldline_id: self.default_worldline,
+            },
+            make_intent_kind("echo.intent/eint-v1"),
+            intent_bytes.to_vec(),
+        )
+        .ingress_id()
+        .to_vec();
+
         if op_id == IMPORT_SUFFIX_INTENT_V1_OP_ID {
             unpack_import_suffix_intent_v1(intent_bytes).map_err(|_| AbiError {
                 code: error_codes::INVALID_INTENT,
@@ -1229,6 +1212,34 @@ impl KernelPort for WarpKernel {
     }
 }
 
+impl TrustedKernelControlPort for WarpKernel {
+    fn dispatch_control_intent_trusted(
+        &mut self,
+        control: ControlIntentV1,
+    ) -> Result<DispatchResponse, AbiError> {
+        let intent_bytes = pack_control_intent_v1(&control).map_err(|error| AbiError {
+            code: error_codes::CODEC_ERROR,
+            message: format!("failed to pack trusted control intent: {error}"),
+        })?;
+        let intent_id = IngressEnvelope::local_intent(
+            IngressTarget::DefaultWriter {
+                worldline_id: self.default_worldline,
+            },
+            make_intent_kind("echo.control/eint-v1"),
+            intent_bytes,
+        )
+        .ingress_id()
+        .to_vec();
+
+        self.apply_control_intent(control)?;
+        Ok(DispatchResponse {
+            accepted: true,
+            intent_id,
+            scheduler_status: self.scheduler_status()?,
+        })
+    }
+}
+
 #[cfg(test)]
 #[allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
 mod tests {
@@ -1261,7 +1272,7 @@ mod tests {
             WriterHeadKey as AbiWriterHeadKey,
         },
         pack_control_intent_v1, pack_import_suffix_intent_v1, pack_intent_v1,
-        IMPORT_SUFFIX_INTENT_V1_OP_ID,
+        CONTROL_INTENT_V1_OP_ID, IMPORT_SUFFIX_INTENT_V1_OP_ID,
     };
     use warp_core::{
         compute_commit_hash_v2, make_edge_id, make_head_id, make_node_id, make_strand_id,
@@ -1290,11 +1301,9 @@ mod tests {
         kernel: &mut WarpKernel,
         cycle_limit: Option<u32>,
     ) -> Result<DispatchResponse, AbiError> {
-        let control = pack_control_intent_v1(&ControlIntentV1::Start {
+        kernel.dispatch_control_intent_trusted(ControlIntentV1::Start {
             mode: SchedulerMode::UntilIdle { cycle_limit },
         })
-        .unwrap();
-        kernel.dispatch_intent(&control)
     }
 
     fn abi_worldline_id(worldline_id: WorldlineId) -> AbiWorldlineId {
@@ -1986,8 +1995,9 @@ mod tests {
         let status_before = kernel.scheduler_status().unwrap();
         assert_eq!(status_before.state, SchedulerState::Inactive);
 
-        let stop = pack_control_intent_v1(&ControlIntentV1::Stop).unwrap();
-        let stop_response = kernel.dispatch_intent(&stop).unwrap();
+        let stop_response = kernel
+            .dispatch_control_intent_trusted(ControlIntentV1::Stop)
+            .unwrap();
 
         assert_eq!(
             stop_response.scheduler_status.state,
@@ -2033,18 +2043,56 @@ mod tests {
     #[test]
     fn set_head_eligibility_rejects_unknown_head_as_invalid_control() {
         let mut kernel = WarpKernel::new().unwrap();
-        let control = pack_control_intent_v1(&ControlIntentV1::SetHeadEligibility {
-            head: AbiWriterHeadKey {
-                worldline_id: abi_worldline_id(kernel.default_worldline),
-                head_id: abi_head_id(make_head_id("missing")),
+        let error = kernel
+            .dispatch_control_intent_trusted(ControlIntentV1::SetHeadEligibility {
+                head: AbiWriterHeadKey {
+                    worldline_id: abi_worldline_id(kernel.default_worldline),
+                    head_id: abi_head_id(make_head_id("missing")),
+                },
+                eligibility: AbiHeadEligibility::Dormant,
+            })
+            .unwrap_err();
+        assert_eq!(error.code, error_codes::INVALID_CONTROL);
+        assert!(error.message.contains("unknown writer head"));
+    }
+
+    #[test]
+    fn unprivileged_application_dispatch_rejects_control_intent_start() {
+        let mut kernel = WarpKernel::new().unwrap();
+        let control = pack_control_intent_v1(&ControlIntentV1::Start {
+            mode: SchedulerMode::UntilIdle {
+                cycle_limit: Some(1),
             },
-            eligibility: AbiHeadEligibility::Dormant,
         })
         .unwrap();
 
         let error = kernel.dispatch_intent(&control).unwrap_err();
-        assert_eq!(error.code, error_codes::INVALID_CONTROL);
-        assert!(error.message.contains("unknown writer head"));
+
+        assert_eq!(error.code, error_codes::FORBIDDEN_CONTROL_INTENT);
+        assert_eq!(
+            kernel.current_head().unwrap().worldline_tick,
+            AbiWorldlineTick(0)
+        );
+        let status = kernel.scheduler_status().unwrap();
+        assert_eq!(status.latest_cycle_global_tick, None);
+        assert_eq!(status.last_run_completion, None);
+    }
+
+    #[test]
+    fn unprivileged_application_dispatch_rejects_hand_built_control_op_id() {
+        let mut kernel = WarpKernel::new().unwrap();
+        let mut bytes = Vec::new();
+        bytes.extend_from_slice(b"EINT");
+        bytes.extend_from_slice(&CONTROL_INTENT_V1_OP_ID.to_le_bytes());
+        bytes.extend_from_slice(&0u32.to_le_bytes());
+
+        let error = kernel.dispatch_intent(&bytes).unwrap_err();
+
+        assert_eq!(error.code, error_codes::FORBIDDEN_CONTROL_INTENT);
+        assert_eq!(
+            kernel.current_head().unwrap().worldline_tick,
+            AbiWorldlineTick(0)
+        );
     }
 
     #[test]
@@ -2054,6 +2102,12 @@ mod tests {
         let resp = kernel.dispatch_intent(&intent).unwrap();
         assert!(resp.accepted);
         assert_eq!(resp.intent_id.len(), 32);
+        assert_eq!(
+            kernel.current_head().unwrap().worldline_tick,
+            AbiWorldlineTick(0)
+        );
+        assert_eq!(resp.scheduler_status.latest_cycle_global_tick, None);
+        assert_eq!(resp.scheduler_status.work_state, WorkState::RunnablePending);
     }
 
     #[test]
@@ -2520,15 +2574,15 @@ mod tests {
         let intent = pack_intent_v1(7, b"blocked").unwrap();
         kernel.dispatch_intent(&intent).unwrap();
 
-        let dormancy = pack_control_intent_v1(&ControlIntentV1::SetHeadEligibility {
-            head: AbiWriterHeadKey {
-                worldline_id: abi_worldline_id(kernel.default_worldline),
-                head_id: abi_head_id(make_head_id("default")),
-            },
-            eligibility: AbiHeadEligibility::Dormant,
-        })
-        .unwrap();
-        kernel.dispatch_intent(&dormancy).unwrap();
+        kernel
+            .dispatch_control_intent_trusted(ControlIntentV1::SetHeadEligibility {
+                head: AbiWriterHeadKey {
+                    worldline_id: abi_worldline_id(kernel.default_worldline),
+                    head_id: abi_head_id(make_head_id("default")),
+                },
+                eligibility: AbiHeadEligibility::Dormant,
+            })
+            .unwrap();
 
         let response = start_until_idle(&mut kernel, None);
         assert_eq!(
diff --git a/docs/architecture/application-contract-hosting.md b/docs/architecture/application-contract-hosting.md
index 115b236e..cfb6f7c9 100644
--- a/docs/architecture/application-contract-hosting.md
+++ b/docs/architecture/application-contract-hosting.md
@@ -198,8 +198,9 @@ meaning belongs to the generated contract layer and the application.
 fn dispatch_intent(&mut self, intent_bytes: &[u8]) -> Result<DispatchResponse, AbiError>
 ```
 
-Implementations such as `WarpKernel` may handle reserved control intents inline
-before returning a `DispatchResponse`.
+Application dispatch rejects reserved control intents. Runtime owners use a
+separate trusted control path for scheduler lifecycle requests before returning
+control evidence.
 
 Its semantic boundary is still:
 
diff --git a/docs/design/0013-wesley-compiled-contract-hosting-doctrine/design.md b/docs/design/0013-wesley-compiled-contract-hosting-doctrine/design.md
index ca269f8f..21bd398d 100644
--- a/docs/design/0013-wesley-compiled-contract-hosting-doctrine/design.md
+++ b/docs/design/0013-wesley-compiled-contract-hosting-doctrine/design.md
@@ -180,8 +180,9 @@ The current codebase already provides these pieces:
 
 - EINT v1 in `echo-wasm-abi`:
   `"EINT" || op_id:u32le || vars_len:u32le || vars`.
-- `dispatch_intent(...)` in `warp-wasm` as the write/control ingress.
-- `KernelPort::dispatch_intent(...)` as an app-agnostic byte boundary.
+- `dispatch_intent(...)` in `warp-wasm` as application EINT ingress.
+- `KernelPort::dispatch_intent(...)` as an app-agnostic application intent
+  boundary that rejects reserved scheduler/control envelopes.
 - `RegistryInfo` plus `get_registry_info`, `get_codec_id`,
   `get_registry_version`, and `get_schema_sha256_hex` exports.
 - `echo-registry-api::RegistryProvider` for app-supplied operation catalogs.
@@ -365,9 +366,10 @@ observe(bytes) -> bytes
 scheduler_status() -> bytes
 ```
 
-`dispatch_intent(bytes)` already accepts EINT v1. New work should prefer
-binding generated app contracts to that existing shape instead of creating a
-parallel contract envelope.
+`dispatch_intent(bytes)` already accepts application EINT v1 envelopes. New
+work should prefer binding generated app contracts to that existing shape
+instead of creating a parallel contract envelope. Scheduler lifecycle control
+must remain on a trusted runtime path, not on application dispatch.
 
 The installed runtime or app-level generated client should expose enough
 handshake metadata to name:
diff --git a/docs/design/optic-admission-ladder-checkpoint.md b/docs/design/optic-admission-ladder-checkpoint.md
index 394eb912..577b8383 100644
--- a/docs/design/optic-admission-ladder-checkpoint.md
+++ b/docs/design/optic-admission-ladder-checkpoint.md
@@ -3,8 +3,8 @@
 
 # Optic Admission Ladder Checkpoint
 
-Status: ApertureResolution v0 boundary checkpoint.
-Scope: refusal ladder with narrow controlled basis and aperture fixtures.
+Status: BudgetResolution v0 boundary checkpoint.
+Scope: refusal ladder with narrow controlled basis, aperture, and budget fixtures.
 
 ## Doctrine
 
@@ -19,6 +19,8 @@ not a validated grant. A basis request is not a resolved basis unless it matches
 the narrow deterministic BasisResolution v0 fixture. A resolved basis is not
 permission to act. An aperture request is not a resolved scope unless it matches
 the narrow deterministic ApertureResolution v0 fixture after basis resolution.
+Budget resolution exists only for the narrow deterministic BudgetResolution v0
+fixture after aperture resolution.
 A resolved aperture is not permission to act. A budget request is not spendable
 runtime capacity.
 
@@ -46,31 +48,33 @@ The current optic invocation admission path evaluates checks in this order:
 
 Presence checks come before resolution checks. Basis resolution gates aperture
 resolution. Aperture resolution gates budget evaluation and runtime support
-checks. BasisResolution v0 accepts exactly one deterministic fixture shape:
-`basis-request:resolved-fixture:v0`.
-ApertureResolution v0 accepts exactly one deterministic fixture shape:
-`aperture-request:resolved-fixture:v0`.
+checks. The current resolved fixture shapes are:
+
+- BasisResolution v0: `basis-request:resolved-fixture:v0`
+- ApertureResolution v0: `aperture-request:resolved-fixture:v0`
+- BudgetResolution v0: `budget-request:resolved-fixture:v0`
 
 ## Obstruction reachability
 
-| Obstruction                       | Reachability      | Meaning                                                                                                  |
-| :-------------------------------- | :---------------- | :------------------------------------------------------------------------------------------------------- |
-| `UnknownHandle`                   | Reachable today   | Echo cannot resolve the runtime-local artifact handle.                                                   |
-| `OperationMismatch`               | Reachable today   | The invocation operation does not match registered artifact metadata.                                    |
-| `MissingBasisRequest`             | Reachable today   | The caller did not provide basis request material.                                                       |
-| `MissingApertureRequest`          | Reachable today   | Basis material is present, but aperture request material is absent.                                      |
-| `MissingBudgetRequest`            | Reachable today   | Basis and aperture material are present, but budget request material is absent.                          |
-| `MissingCapability`               | Reachable today   | Required invocation context is present, but no capability presentation was supplied.                     |
-| `MalformedCapabilityPresentation` | Reachable today   | Capability presentation material is present but not structurally usable.                                 |
-| `UnboundCapabilityPresentation`   | Reachable today   | Capability presentation material is structurally usable but not bound to the invocation.                 |
-| `CapabilityValidationUnavailable` | Reachable today   | A bound presentation exists, but no successful validation or admission has occurred yet.                 |
-| `UnsupportedBasisResolution`      | Reachable today   | Identity-covered material reaches the basis boundary, but the basis shape is outside BasisResolution v0. |
-| `UnsupportedApertureResolution`   | Reachable today   | BasisResolution v0 succeeded, but the aperture shape is outside ApertureResolution v0.                   |
-| `UnsupportedBudgetResolution`     | Reachable today   | ApertureResolution v0 succeeded, but budget evaluation does not exist yet.                               |
-| `RuntimeSupportUnavailable`       | Future vocabulary | Must remain unreachable until lawful basis, aperture, and budget resolution exist.                       |
-
-`RuntimeSupportUnavailable` is deliberately defined but not lawfully reachable
-at this checkpoint.
+| Obstruction                       | Reachability    | Meaning                                                                                                  |
+| :-------------------------------- | :-------------- | :------------------------------------------------------------------------------------------------------- |
+| `UnknownHandle`                   | Reachable today | Echo cannot resolve the runtime-local artifact handle.                                                   |
+| `OperationMismatch`               | Reachable today | The invocation operation does not match registered artifact metadata.                                    |
+| `MissingBasisRequest`             | Reachable today | The caller did not provide basis request material.                                                       |
+| `MissingApertureRequest`          | Reachable today | Basis material is present, but aperture request material is absent.                                      |
+| `MissingBudgetRequest`            | Reachable today | Basis and aperture material are present, but budget request material is absent.                          |
+| `MissingCapability`               | Reachable today | Required invocation context is present, but no capability presentation was supplied.                     |
+| `MalformedCapabilityPresentation` | Reachable today | Capability presentation material is present but not structurally usable.                                 |
+| `UnboundCapabilityPresentation`   | Reachable today | Capability presentation material is structurally usable but not bound to the invocation.                 |
+| `CapabilityValidationUnavailable` | Reachable today | A bound presentation exists, but no successful validation or admission has occurred yet.                 |
+| `UnsupportedBasisResolution`      | Reachable today | Identity-covered material reaches the basis boundary, but the basis shape is outside BasisResolution v0. |
+| `UnsupportedApertureResolution`   | Reachable today | BasisResolution v0 succeeded, but the aperture shape is outside ApertureResolution v0.                   |
+| `UnsupportedBudgetResolution`     | Reachable today | ApertureResolution v0 succeeded, but the budget shape is outside BudgetResolution v0.                    |
+| `RuntimeSupportUnavailable`       | Reachable today | BudgetResolution v0 succeeded, but runtime support evaluation does not exist yet.                        |
+
+`RuntimeSupportUnavailable` is lawfully reachable after BasisResolution v0,
+ApertureResolution v0, and BudgetResolution v0 all resolve. It is the current
+terminal refusal before runtime support evaluation exists.
 
 `UnsupportedApertureResolution` is reachable only after the exact
 BasisResolution v0 fixture resolves. For identity-covered material, unsupported
@@ -125,9 +129,23 @@ Resolving that fixture establishes only the bounded observation/action window
 inside a resolved basis. It does not create authority, admission, budget
 capacity, runtime support, scheduler work, or execution.
 
+## BudgetResolution v0
+
+BudgetResolution v0 is not general budget resolution. It recognizes exactly one
+fixture after basis and aperture resolution both succeed:
+
+```text
+budget-request:resolved-fixture:v0
+```
+
+Budget resolution establishes a bounded resource envelope under consideration.
+It does not create permission to act, reserve spendable capacity, validate a
+grant, or admit an invocation. The only lawful next refusal in this slice is
+`RuntimeSupportUnavailable`.
+
 ## Next transition point
 
-The next transition point is BudgetResolution v0.
+The next transition point is RuntimeSupport v0.
 
 That transition must be narrow and explicit. It must not imply successful
 admission, budget spendability, runtime support, execution, or authority
@@ -142,4 +160,4 @@ If a future slice introduces a successful admission path before a resolved basis
 resolved aperture, evaluated budget, runtime support check, and validated grant
 exist, the admission ladder is wrong.
 
-ApertureResolution v0 is controlled resolved state, not admission.
+BudgetResolution v0 is controlled resolved state, not admission.
diff --git a/docs/method/backlog/bad-code/wasm-control-intent-authority-boundary.md b/docs/method/backlog/bad-code/wasm-control-intent-authority-boundary.md
new file mode 100644
index 00000000..86be1844
--- /dev/null
+++ b/docs/method/backlog/bad-code/wasm-control-intent-authority-boundary.md
@@ -0,0 +1,31 @@
+<!-- SPDX-License-Identifier: Apache-2.0 OR LicenseRef-MIND-UCAL-1.0 -->
+<!-- © James Ross Ω FLYING•ROBOTS <https://github.com/flyingrobots> -->
+
+# WASM control intent authority boundary is too implicit
+
+Status: active follow-up. The immediate bug was fixed by making public
+application dispatch reject `CONTROL_INTENT_V1_OP_ID` before the kernel can
+run scheduler control. `WarpKernel::dispatch_intent(...)` also rejects the
+reserved control op id directly, while trusted runtime control now uses a
+separate `TrustedKernelControlPort` Rust host/runtime-owner path.
+
+The remaining concern is product packaging and host architecture: browser
+adapters must not hand untrusted application code a raw privileged runtime
+owner. A worker or host adapter should own Echo's scheduler lifecycle and expose
+only application-safe intent ingress plus observation APIs.
+
+Fix direction:
+
+- Keep generated Wesley helpers unable to produce scheduler control envelopes.
+- Add browser/package integration tests proving untrusted application adapters
+  cannot reach trusted runtime control.
+- Document the worker/runtime-owner shape before publishing a high-level
+  browser application package.
+
+Acceptance criteria:
+
+- Browser package APIs expose application-safe dispatch and observation only.
+- Host/runtime control can start Echo deterministically without exposing the raw
+  control path to application code.
+- README, WASM ABI docs, and package docs describe the authority split
+  consistently.
diff --git a/docs/spec/SPEC-0009-wasm-abi-v3.md b/docs/spec/SPEC-0009-wasm-abi-v3.md
index fd43b07f..57ac1608 100644
--- a/docs/spec/SPEC-0009-wasm-abi-v3.md
+++ b/docs/spec/SPEC-0009-wasm-abi-v3.md
@@ -13,7 +13,7 @@ error protocol for Echo's deterministic simulation boundary.
 ABI v3 makes three boundaries explicit:
 
 - `observe(request)` is the canonical generic world-state read export; neighborhood-specific read exports expose bounded site/core views.
-- `dispatch_intent(...)` is the only public write and control ingress surface.
+- `dispatch_intent(...)` is the public application intent ingress surface.
 - `scheduler_status()` is the read-only scheduler metadata export.
 
 Echo internals do not consume wall-clock time. All clocks in this ABI are
@@ -36,9 +36,9 @@ represents `Option<...>::None`.
   `commit_global_tick = null` means the worldline is still at `U0` and has not
   committed anything yet.
 
-Scheduler lifecycle requests are carried as privileged control intents through
-the same EINT intake path as domain intents. There is no public `step(...)`,
-poll, or tick hook API in ABI v3.
+Scheduler lifecycle requests are privileged runtime control, not application
+intents. Public application dispatch rejects the reserved control op id. There
+is no public `step(...)`, poll, or tick hook API in ABI v3.
 
 ## Architecture
 
@@ -89,11 +89,11 @@ Removed before or by ABI v3:
 
 ## Intent Intake
 
-All external writes enter Echo through EINT envelopes.
+Application writes enter Echo through EINT envelopes.
 
 - Domain intents use their domain-specific `op_id`.
-- Privileged scheduler/control intents use reserved op id `u32::MAX`
-  (`CONTROL_INTENT_V1_OP_ID`).
+- The reserved scheduler/control op id `u32::MAX`
+  (`CONTROL_INTENT_V1_OP_ID`) is forbidden at application dispatch.
 
 The EINT v1 byte layout is:
 
@@ -104,8 +104,10 @@ The EINT v1 byte layout is:
 + vars (exactly vars_len bytes)
 ```
 
-For privileged control intents, `op_id` is always `0xffffffff` and `vars` are
-canonical-CBOR bytes that decode as `ControlIntentV1`.
+Trusted runtime control still uses `ControlIntentV1` internally. For those
+privileged control intents, `op_id` is always `0xffffffff` and `vars` are
+canonical-CBOR bytes that decode as `ControlIntentV1`; those bytes must not be
+accepted by public application dispatch.
 
 Canonical payload shapes:
 
@@ -316,8 +318,8 @@ Returned by `init()`.
 Current engine-backed behavior:
 
 - `init()` leaves the runtime inert.
-- `Start { mode: UntilIdle { ... } }` runs synchronously inside the control
-  intent handler and returns after the run completes.
+- Trusted `Start { mode: UntilIdle { ... } }` runs synchronously inside the
+  control intent handler and returns after the run completes.
 - `Stop` is a no-op when the scheduler is already inactive; it does not rewrite
   `last_run_completion` for a finished run.
 - Hosts normally observe `state = inactive` plus `last_run_completion`, not a
@@ -341,35 +343,44 @@ Current engine-backed behavior:
 
 ## Error Codes
 
-| Code | Name                           | Meaning                                                    |
-| ---- | ------------------------------ | ---------------------------------------------------------- |
-| 1    | `NOT_INITIALIZED`              | `init()` not called                                        |
-| 2    | `INVALID_INTENT`               | Malformed EINT intent envelope                             |
-| 3    | `ENGINE_ERROR`                 | Internal engine failure                                    |
-| 4    | `LEGACY_INVALID_TICK`          | Reserved for the removed v1 snapshot adapter               |
-| 5    | `NOT_SUPPORTED`                | Operation not implemented                                  |
-| 6    | `CODEC_ERROR`                  | CBOR encode/decode failure                                 |
-| 7    | `INVALID_PAYLOAD`              | Corrupted input bytes                                      |
-| 8    | `INVALID_WORLDLINE`            | Requested worldline missing                                |
-| 9    | `INVALID_TICK`                 | Requested observation tick missing                         |
-| 10   | `UNSUPPORTED_FRAME_PROJECTION` | Invalid frame/projection pair                              |
-| 11   | `UNSUPPORTED_QUERY`            | Query observation not yet implemented                      |
-| 12   | `OBSERVATION_UNAVAILABLE`      | Valid request but no observation exists at that coordinate |
-| 13   | `INVALID_CONTROL`              | Malformed or invalid control intent                        |
+| Code | Name                             | Meaning                                                    |
+| ---- | -------------------------------- | ---------------------------------------------------------- |
+| 1    | `NOT_INITIALIZED`                | `init()` not called                                        |
+| 2    | `INVALID_INTENT`                 | Malformed EINT intent envelope                             |
+| 3    | `ENGINE_ERROR`                   | Internal engine failure                                    |
+| 4    | `LEGACY_INVALID_TICK`            | Reserved for the removed v1 snapshot adapter               |
+| 5    | `NOT_SUPPORTED`                  | Operation not implemented                                  |
+| 6    | `CODEC_ERROR`                    | CBOR encode/decode failure                                 |
+| 7    | `INVALID_PAYLOAD`                | Corrupted input bytes                                      |
+| 8    | `INVALID_WORLDLINE`              | Requested worldline missing                                |
+| 9    | `INVALID_TICK`                   | Requested observation tick missing                         |
+| 10   | `UNSUPPORTED_FRAME_PROJECTION`   | Invalid frame/projection pair                              |
+| 11   | `UNSUPPORTED_QUERY`              | Query observation not yet implemented                      |
+| 12   | `OBSERVATION_UNAVAILABLE`        | Valid request but no observation exists at that coordinate |
+| 13   | `INVALID_CONTROL`                | Malformed or invalid control intent                        |
+| 14   | `INVALID_STRAND`                 | Requested strand is not registered                         |
+| 15   | `UNSUPPORTED_OBSERVER_PLAN`      | Requested observer plan is not available                   |
+| 16   | `UNSUPPORTED_OBSERVER_INSTANCE`  | Requested observer instance is not available               |
+| 17   | `UNSUPPORTED_OBSERVATION_RIGHTS` | Requested observation rights posture is not available      |
+| 18   | `OBSERVATION_BUDGET_EXCEEDED`    | Requested observation exceeded its explicit read budget    |
+| 19   | `FORBIDDEN_CONTROL_INTENT`       | Application dispatch rejected scheduler control            |
 
 ## Rust Boundary
 
-`KernelPort` is the Rust-side ABI contract for `warp-wasm`.
+`KernelPort` is the Rust-side ABI contract for `warp-wasm` application
+ingress, observation, status, and registry metadata.
 
 - `dispatch_intent(...)`
 - `observe(...)`
 - `scheduler_status()`
 - `registry_info()`
 
-The trait does not expose the removed v1 read adapters or a public step/pump
-surface. Implementors that need head or snapshot data must derive them from
-their own observation-backed internals rather than adding parallel public read
-methods.
+The trait does not expose the removed v1 read adapters, a public step/pump
+surface, or scheduler lifecycle control through application dispatch. Trusted
+runtime control is a separate `TrustedKernelControlPort` Rust host/runtime-owner
+path and is not part of the browser/application ingress surface. Implementors
+that need head or snapshot data must derive them from their own
+observation-backed internals rather than adding parallel public read methods.
 
 ## Migration Notes for Host Adapters
 
@@ -379,8 +390,9 @@ methods.
 2. Continue treating `observe(request)` as the canonical generic world-state
    read boundary, with neighborhood-specific read exports reserved for bounded
    site/core views.
-3. Route scheduler lifecycle and admission requests through
-   `dispatch_intent(...)` using `ControlIntentV1` packed into an EINT envelope.
+3. Route application admission requests through `dispatch_intent(...)`.
+   Scheduler lifecycle requests must use trusted runtime control, not public
+   application dispatch.
 4. Read `RegistryInfo.abi_version` and reject hosts that still expect the v2
    step surface.
 5. Rename host-side field access from bare `tick`-style fields to explicit
diff --git a/docs/spec/SPEC-0009-wasm-abi.md b/docs/spec/SPEC-0009-wasm-abi.md
index cc9d34bb..2cdbdab3 100644
--- a/docs/spec/SPEC-0009-wasm-abi.md
+++ b/docs/spec/SPEC-0009-wasm-abi.md
@@ -42,9 +42,14 @@ The hill: an agent can generate canonical CBOR, call one export, inspect an `ok`
 
 Removed exports stay removed: `step`, `snapshot_at`, `render_snapshot`, `execute_query`, `get_head`, and `drain_view_ops`.
 
-## Decision 2: All writes enter through EINT
+## Decision 2: Application writes enter through EINT
 
-`dispatch_intent(bytes)` accepts Echo intent envelopes: `"EINT" || op_id:u32le || vars_len:u32le || vars`.
+`dispatch_intent(bytes)` accepts application Echo intent envelopes:
+`"EINT" || op_id:u32le || vars_len:u32le || vars`.
+
+The reserved scheduler/control op id is not an application intent. Public
+application dispatch rejects it before the kernel can run scheduler control.
+Trusted host/runtime control uses a separate authority path.
 
 ## Decision 3: Observation is the only public world-state read
 
