From 50e7f582613b0336ff967a0edd17669cdc8d34c0 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 24 Jun 2026 09:59:12 +0000 Subject: [PATCH] =?UTF-8?q?action=20handler:=20B2-lift=20applicabilities?= =?UTF-8?q?=20envelope=20=E2=80=94=20GET=20/applicabilities=20read?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes B2-lift (the REST registration instance lift). The capabilities half shipped in #122; this adds the applicabilities half the same way. - ogar-from-schema/src/registration.rs (producer, parser-free): RegisteredApplicability DTO + RegisteredApplicabilities (MapOfApplicabilities, keyed by handler id) + lift_applicabilities -> BTreeMap>. The inner filter-list field name isn't pinned by the harvested spec, so the DTO accepts modelFilters / model / filters via serde aliases (defaults empty) — a real response binds without a code change once the name is confirmed. The element mapping (ModelFilter{Var,Mode,Value} -> StateGuard) is the documented, exact part. - ogar-action-handler::parse_applicabilities — the serde_json read of a GET /applicabilities body (runtime owns I/O). Proven by rest_applicabilities_lift_to_per_handler_guards (real JSON -> per-handler StateGuard sets, alias accepted). The lifted guards are the same StateGuards the hard gate's commit_via evaluates before executing — so the instance lift now feeds both halves of an ActionDef: capabilities -> ActionParam[] signature, applicabilities -> StateGuard. Docs: ARAGO-ACTIONHANDLER-PARITY (applicabilities row → SHIPPED, §3 bullet, verdict, cross-refs); D-ACTIONHANDLER-B2LIFT regraded G with a cited canon-pass correction (append-only — original body kept, "Remaining" note superseded). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01EYvNjD8M8LMNYbRy3gq2FP --- crates/ogar-action-handler/src/lib.rs | 50 +++++++++++++++- crates/ogar-from-schema/src/registration.rs | 65 +++++++++++++++++++++ docs/ARAGO-ACTIONHANDLER-PARITY.md | 40 +++++++------ docs/DISCOVERY-MAP.md | 2 +- 4 files changed, 137 insertions(+), 20 deletions(-) diff --git a/crates/ogar-action-handler/src/lib.rs b/crates/ogar-action-handler/src/lib.rs index d89869e..205fdaf 100644 --- a/crates/ogar-action-handler/src/lib.rs +++ b/crates/ogar-action-handler/src/lib.rs @@ -40,10 +40,14 @@ use std::process::Command; +use std::collections::BTreeMap; + use ogar_from_schema::action_ws::CapabilityExecutor; use ogar_from_schema::registration::{ - ConcreteCapability, RegisteredCapabilities, lift_registration, + ConcreteCapability, RegisteredApplicabilities, RegisteredCapabilities, lift_applicabilities, + lift_registration, }; +use ogar_vocab::KausalSpec; /// Read a deployed handler's `GET /capabilities` REST response (a JSON /// `MapOfCapabilities`) and lift it into the concrete OGAR capability signatures @@ -63,6 +67,22 @@ pub fn parse_capabilities(json: &str) -> Result, String> Ok(lift_registration(&caps)) } +/// Read a deployed handler's `GET /applicabilities` REST response (a JSON +/// `MapOfApplicabilities`) and lift it into the per-handler guard sets — handler +/// id → its [`KausalSpec::StateGuard`] list (the B2-lift applicabilities half). +/// +/// Each handler's guards are the OGAR form of arago's node-match `ModelFilter`s: +/// the action applies where they all match. The same `commit_via` state-guard the +/// hard gate evaluates before executing. +/// +/// # Errors +/// Returns the `serde_json` error message if the body is not a valid +/// `MapOfApplicabilities`. +pub fn parse_applicabilities(json: &str) -> Result>, String> { + let apps: RegisteredApplicabilities = serde_json::from_str(json).map_err(|e| e.to_string())?; + Ok(lift_applicabilities(&apps)) +} + /// Reference executor for the **native** target: runs a capability's command via /// the local POSIX shell (`sh -c`). The concrete B1 impl that makes "OGAR /// running it here" real for local execution. @@ -209,6 +229,34 @@ mod tests { assert_eq!(result[0], ("output".to_owned(), "lifted".to_owned())); } + /// B2-lift applicabilities: a real `GET /applicabilities` REST response + /// (JSON `MapOfApplicabilities`) parses → lifts to per-handler guard sets, + /// the `ModelFilter`s becoming `StateGuard`s the hard gate evaluates. + #[test] + fn rest_applicabilities_lift_to_per_handler_guards() { + // `modelFilters` is the primary field name; `model` / `filters` are + // accepted aliases (the inner field name isn't pinned by the spec). + let body = r#"{ + "ssh-handler": { + "modelFilters": [ + { "var": "ogit/_type", "mode": "equals", "value": "ogit/Network/Machine" } + ] + }, + "any-handler": { "model": [] } + }"#; + + let lifted = parse_applicabilities(body).expect("valid MapOfApplicabilities"); + assert_eq!(lifted.len(), 2); + assert!(lifted["any-handler"].is_empty()); + assert_eq!( + lifted["ssh-handler"], + vec![KausalSpec::StateGuard { + guard_field: "ogit/_type".to_owned(), + guard_values: vec!["ogit/Network/Machine".to_owned()], + }] + ); + } + /// End-to-end: the dispatch core + this executor run a real command and /// produce the `sendActionResult` — "OGAR running it here," native target. #[test] diff --git a/crates/ogar-from-schema/src/registration.rs b/crates/ogar-from-schema/src/registration.rs index de2b6bd..e9518e3 100644 --- a/crates/ogar-from-schema/src/registration.rs +++ b/crates/ogar-from-schema/src/registration.rs @@ -161,6 +161,41 @@ pub fn lift_applicability(filters: &[ModelFilter]) -> Vec { filters.iter().map(model_filter_to_guard).collect() } +/// One applicability as `GET /applicabilities` reports it — its node-match +/// `ModelFilter`s. +/// +/// The harvested spec (`docs/ARAGO-ACTIONHANDLER-PARITY.md` §2a) pins the +/// *outer* shape (a `MapOfApplicabilities` keyed by handler id) and the +/// `ModelFilter{Var,Mode,Value}` element, but not the exact name of the field +/// that carries the filter list. This DTO accepts the plausible names +/// (`modelFilters`, `model`, `filters`) via serde aliases and defaults to an +/// empty list, so a real response binds without a code change once the field +/// name is confirmed. The element mapping itself (`ModelFilter → StateGuard`) is +/// the documented, `[G]` part. +#[derive(Debug, Clone, PartialEq, Eq, Default)] +#[cfg_attr(feature = "serde", derive(Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))] +pub struct RegisteredApplicability { + /// The node-match filters (an applicability applies where they all match). + #[cfg_attr(feature = "serde", serde(default, alias = "model", alias = "filters"))] + pub model_filters: Vec, +} + +/// The `GET /applicabilities` response — a `MapOfApplicabilities` keyed by +/// handler id. +pub type RegisteredApplicabilities = BTreeMap; + +/// Lift a whole `GET /applicabilities` response into the per-handler guard sets: +/// handler id → its [`KausalSpec::StateGuard`] list (each handler applies where +/// **all** of its guards match). Deterministic order (the source is a +/// [`BTreeMap`], sorted by handler id). +#[must_use] +pub fn lift_applicabilities(apps: &RegisteredApplicabilities) -> BTreeMap> { + apps.iter() + .map(|(handler, app)| (handler.clone(), lift_applicability(&app.model_filters))) + .collect() +} + #[cfg(test)] mod tests { use super::*; @@ -294,4 +329,34 @@ mod tests { } ); } + + #[test] + fn applicabilities_map_lifts_per_handler_guard_sets() { + let mut apps: RegisteredApplicabilities = BTreeMap::new(); + apps.insert( + "ssh-handler".to_owned(), + RegisteredApplicability { + model_filters: vec![ModelFilter { + var: "ogit/_type".to_owned(), + mode: Some("equals".to_owned()), + value: "ogit/Network/Machine".to_owned(), + }], + }, + ); + apps.insert( + "noop-handler".to_owned(), + RegisteredApplicability::default(), + ); + let lifted = lift_applicabilities(&apps); + // Keyed by handler id (BTreeMap → sorted): noop-handler, ssh-handler. + assert_eq!(lifted.len(), 2); + assert!(lifted["noop-handler"].is_empty()); + assert_eq!( + lifted["ssh-handler"], + vec![KausalSpec::StateGuard { + guard_field: "ogit/_type".to_owned(), + guard_values: vec!["ogit/Network/Machine".to_owned()], + }] + ); + } } diff --git a/docs/ARAGO-ACTIONHANDLER-PARITY.md b/docs/ARAGO-ACTIONHANDLER-PARITY.md index 36682ec..7c647c3 100644 --- a/docs/ARAGO-ACTIONHANDLER-PARITY.md +++ b/docs/ARAGO-ACTIONHANDLER-PARITY.md @@ -213,12 +213,15 @@ downstream. The remaining bricks: turn a real `MapOfCapabilities` JSON body into `ConcreteCapability` — `ActionParam[]` with concrete `(name, mandatory, default)` — proven end-to-end by `rest_registration_lifts_binds_and_runs` (JSON → lift → `bind_parameters` → - `NativeCommandExecutor` runs the command). The applicability side is the - documented field-for-field lift: `ModelFilter{Var,Mode,Value}` → - `KausalSpec::StateGuard` (`model_filter_to_guard` / `lift_applicability`, shipped); - what remains is wiring the `GET /applicabilities` JSON envelope read (the - `MapOfApplicabilities` outer shape) the same way `parse_capabilities` reads - capabilities. + `NativeCommandExecutor` runs the command). The applicability side is **shipped + too**: `GET /applicabilities` → `registration::{RegisteredApplicability, + lift_applicabilities}` + `ogar-action-handler::parse_applicabilities` turn a real + `MapOfApplicabilities` JSON body into per-handler `StateGuard` sets (handler id → + `Vec`) — the documented field-for-field `ModelFilter{Var,Mode,Value}` + → `KausalSpec::StateGuard` lift, proven by `rest_applicabilities_lift_to_per_handler_guards`. + The only residual is cosmetic: the inner filter-list field name is alias-flexible + (`modelFilters` / `model` / `filters`) pending confirmation against a live + response — the lift itself is exact. What remains is **glue over existing types** (a socket loop + a JSON codec + a registration parser) plus the non-native executor targets — not new IR. The @@ -243,7 +246,7 @@ transport over them. | **Executor — SSH/REST/WinRM (B1)** | ⛔ `[H]` | further `CapabilityExecutor` impls (rs-graph-llm `graph-flow-action`) | | **Live WebSocket transport (B2-transport)** | ⛔ `[H]` | wrap `handle_submit` in a `tokio-tungstenite` loop + JSON codec (all shapes pinned, §2a) | | **Instance config lift — capabilities (B2-lift)** | ✅ `[G]` SHIPPED | `registration::lift_registration` + `ogar-action-handler::parse_capabilities`: real `GET /capabilities` JSON → `ConcreteCapability` (`ActionParam[]`); `rest_registration_lifts_binds_and_runs` (JSON → lift → bind → run) | -| **Instance config lift — applicabilities (B2-lift)** | 🟡 `[G]` lift / `[H]` envelope | `ModelFilter{Var,Mode,Value}` → `StateGuard` shipped (`model_filter_to_guard`/`lift_applicability`); remaining: the `GET /applicabilities` `MapOfApplicabilities` JSON envelope read | +| **Instance config lift — applicabilities (B2-lift)** | ✅ `[G]` SHIPPED | `registration::lift_applicabilities` + `ogar-action-handler::parse_applicabilities`: real `GET /applicabilities` JSON → per-handler `StateGuard` sets; `rest_applicabilities_lift_to_per_handler_guards`. Residual: inner filter-list field name is alias-flexible pending a live response | **Verdict:** OGAR is at **full contract + lifecycle + protocol-binding + reactive-dispatch parity**, and **a working native executor runs real commands @@ -253,16 +256,16 @@ binding, the dispatch, and native execution are real and tested — and the gate now **wired to the executor**: rs-graph-llm's `graph-flow-action-ogar` runs OGAR's `CapabilityExecutor` only after `commit_via` commits, so an unauthorized or MUL-blocked action never executes (proven structurally — `take_result()` is -`None`). The **capabilities instance lift is shipped too** — a real `GET -/capabilities` JSON body lifts to concrete `ActionParam[]` and runs end-to-end -(`rest_registration_lifts_binds_and_runs`). What's left for +`None`). The **whole instance lift is shipped too** — real `GET /capabilities` +and `GET /applicabilities` JSON bodies lift to concrete `ActionParam[]` (runs +end-to-end, `rest_registration_lifts_binds_and_runs`) and per-handler `StateGuard` +sets (`rest_applicabilities_lift_to_per_handler_guards`). What's left for a **live** drop-in replacement of arago's Python daemon: **B2-transport** (the -WebSocket loop — all shapes/auth pinned), the **B2-lift applicabilities envelope** -(`GET /applicabilities` JSON read; the `ModelFilter→StateGuard` lift is done), and -the **non-native executor targets** (SSH/REST). Each is -transport/parser/runner glue over existing types — **no missing IR, no missing -protocol mapping**. That is the honest state: OGAR *is* an ActionHandler that -runs commands here; a thin transport away from connecting to a live HIRO engine. +WebSocket loop — all shapes/auth pinned) and the **non-native executor targets** +(SSH/REST). Each is transport/runner glue over existing types — **no missing IR, +no missing protocol mapping**. That is the honest state: OGAR *is* an ActionHandler +that runs commands here, reads its own registration, and gates every action; a thin +WebSocket transport away from connecting to a live HIRO engine. --- @@ -285,8 +288,9 @@ is replaceable; the parity claim is certified, not argued. - `crates/ogar-from-schema/src/registration.rs` — B2-lift: the REST registration DTOs (`RegisteredCapability` / `ModelFilter`) + the pure lift (`lift_registration` → `ConcreteCapability`; `model_filter_to_guard`). -- `crates/ogar-action-handler/src/lib.rs` — `parse_capabilities` (the `serde_json` - read of a `GET /capabilities` body; the B2-lift I/O half). +- `crates/ogar-action-handler/src/lib.rs` — `parse_capabilities` / + `parse_applicabilities` (the `serde_json` read of the `GET /capabilities` and + `GET /applicabilities` bodies; the B2-lift I/O half). - `docs/HIRO-DO-ARM-LIFT.md` — the lossless-DO rule (the body is pointed-to). - `docs/ACTIONHANDLER-TURSTEHER.md` — RBAC-as-`const`, the cold-path gate, Rung. - `lance-graph-contract::action` — `ActionInvocation` / `commit_via`. diff --git a/docs/DISCOVERY-MAP.md b/docs/DISCOVERY-MAP.md index 719016c..a4c2bd3 100644 --- a/docs/DISCOVERY-MAP.md +++ b/docs/DISCOVERY-MAP.md @@ -212,7 +212,7 @@ two halves of a cell. ADR‑026 names the cascade that ties them. | D‑MARS‑CLASSID | MARS/Automation classids MINTED: `ConceptDomain::Automation` (0x0C), 9 concepts (`mars_application/resource/software/machine` 0x0C01‑04, `knowledge_item` 05, `mars_node_template` 06, `action_handler` 07, `action_applicability` 08, `automation_trigger` 09) — one domain spanning MARS structural CMDB + Automation DO‑arm (Auth precedent); resolves MARS‑TRANSCODING §1 deferral; passed 5+3 hardening (theorem/doctrine/integration/runtime savants + drift‑guards). Reserves the speculative rest | G | CODED | `ogar-vocab/src/lib.rs`, `ogar-class-view/src/lib.rs` | D‑VOCAB, D‑HIRO‑DO | | D‑ACTIONHANDLER‑PARITY | arago HIRO ActionHandler ⟷ OGAR: `assemble_action_handler` walks the OGIT `provides` graph (`ActionHandler→ActionApplicability→ActionCapability`) into `ActionHandlerSpec`/`CapabilitySlot`/`ApplicabilitySlot`/`ActionParam`. Config+ontology+`action-ws` protocol all map to OGAR types: arago `ModelFilter{Var,Mode,Value}`→`StateGuard`; `Capability.Name`→`predicate`; `resultParameters`→output sig; `action-ws` `submitAction→ack→sendActionResult` ⟷ `ActionInvocation` `Pending→Committed` (`commit_via` is the gate). **B2 protocol core SHIPPED + spec-faithful** (`action_ws`: all 6 `action-ws.yaml` message types — submitAction/sendActionResult/acknowledged/negativeAcknowledged/configChanged/error — + `submit_to_invocation`/`bind_parameters`/`invocation_to_result` (result=JSON string ≤1 MiB per spec) + connection consts (`ACTION_WS_PATH`, `auth_subprotocol`, `validate_id`); socket-free, `full_action_ws_roundtrip` proven; harvested from the HIRO 7.0 dev-portal specs §2a). **Reactive dispatch + B1 native executor SHIPPED**: `action_ws::handle_submit` (validate→ack/nack→bind→execute→result) over the `CapabilityExecutor` trait (the B1 seam); `ogar-action-handler::NativeCommandExecutor` runs `ExecuteCommand` for real (`full_dispatch_runs_a_real_command` — OGAR runs a command end-to-end). Remaining for a live drop-in: B2-transport (WS loop), B2-lift (REST registration parse), SSH/REST executor targets; gated on `PROBE‑OGAR‑ACTIONHANDLER‑RUN` | G (contract+protocol+native exec) / H (live socket) | CODED | `ogar-from-schema/src/{do_arm,action_ws}.rs`, `ogar-action-handler/`, `docs/ARAGO-ACTIONHANDLER-PARITY.md` | D‑HIRO‑DO, D‑MARS‑CLASSID | | D‑ACTIONHANDLER‑UPLINK | The hard gate wired to the OGAR executor (cross‑repo seam): rs‑graph‑llm `graph-flow-action-ogar::GatedOgarHandler` wraps an OGAR `CapabilityExecutor` as a `graph-flow-action::ActionHandler`, so `dispatch_via`'s cold floor (`commit_via`: def‑match → RBAC `ClassRbac` → `StateGuard` → MUL) lands **before** the executor's `handle`. Structural proof the contract lands first: `take_result()`/`run_gated` returns `None` whenever the gate refused — unauthorized actor → `Denied` (executor never runs), MUL `Block` → `Escalated` (executor never runs); only the authorized path reaches `NativeCommandExecutor` and runs the real command (3 tests). Dependency hygiene held: `graph-flow-action` stays contract‑only (`I‑ACTIONHANDLER‑IS‑KGV‑NOT‑CHOKEPOINT`); `ogar-from-schema` carries no `lance-graph` dep — the two sides meet only at this crate's API (one `lance-graph-contract`). rs‑graph‑llm pinned to toolchain 1.95.0 to match the AdaWorldAPI stack | G | CODED | `rs-graph-llm/graph-flow-action-ogar/src/lib.rs` | D‑ACTIONHANDLER‑PARITY | -| D‑ACTIONHANDLER‑B2LIFT | REST registration **instance lift** (B2-lift) — turns a deployed handler's `GET /capabilities` JSON (`MapOfCapabilities`) into the concrete signatures the *schema* half can't supply: `registration::{RegisteredCapability,ModelFilter}` (typed DTOs, `Deserialize` behind the `serde` feature) + the pure lift `lift_registration → ConcreteCapability` (concrete `ActionParam[]` with `(name,mandatory,default)`) and `model_filter_to_guard` (arago `ModelFilter{Var,Mode,Value}`→`KausalSpec::StateGuard`, field‑for‑field). Producer stays parser‑free; the runtime `ogar-action-handler::parse_capabilities` does the `serde_json` read (producer‑defines‑types / runtime‑does‑I/O split). Proven end‑to‑end: `rest_registration_lifts_binds_and_runs` (real JSON → lift → `bind_parameters` → `NativeCommandExecutor` runs the command). Remaining: the `GET /applicabilities` `MapOfApplicabilities` envelope read (the `ModelFilter→StateGuard` lift is done) | G (capabilities) / H (applicabilities envelope) | CODED | `ogar-from-schema/src/registration.rs`, `ogar-action-handler/src/lib.rs` | D‑ACTIONHANDLER‑PARITY | +| D‑ACTIONHANDLER‑B2LIFT | REST registration **instance lift** (B2-lift) — turns a deployed handler's `GET /capabilities` JSON (`MapOfCapabilities`) into the concrete signatures the *schema* half can't supply: `registration::{RegisteredCapability,ModelFilter}` (typed DTOs, `Deserialize` behind the `serde` feature) + the pure lift `lift_registration → ConcreteCapability` (concrete `ActionParam[]` with `(name,mandatory,default)`) and `model_filter_to_guard` (arago `ModelFilter{Var,Mode,Value}`→`KausalSpec::StateGuard`, field‑for‑field). Producer stays parser‑free; the runtime `ogar-action-handler::parse_capabilities` does the `serde_json` read (producer‑defines‑types / runtime‑does‑I/O split). Proven end‑to‑end: `rest_registration_lifts_binds_and_runs` (real JSON → lift → `bind_parameters` → `NativeCommandExecutor` runs the command). Remaining: the `GET /applicabilities` `MapOfApplicabilities` envelope read (the `ModelFilter→StateGuard` lift is done). **CORRECTION (canon‑pass 2026‑06‑24): applicabilities envelope now SHIPPED** — `registration::{RegisteredApplicability,lift_applicabilities}` + `ogar-action-handler::parse_applicabilities` lift a real `GET /applicabilities` JSON body into per‑handler `StateGuard` sets (`rest_applicabilities_lift_to_per_handler_guards`); inner filter‑list field name alias‑flexible (`modelFilters`/`model`/`filters`) pending a live response. Supersedes the "Remaining" note | G | CODED | `ogar-from-schema/src/registration.rs`, `ogar-action-handler/src/lib.rs` | D‑ACTIONHANDLER‑PARITY | | D‑OSM | `ogar-from-osm-pbf` — Node/Way/Relation; quadkey NiblePath from resolved geometry | H | IDEA | (queued) | D‑VOCAB, `[per rt]` D‑OSM‑3 | | D‑PATTERN | `ogar-pattern` — recognition library + confidence (FMA‑D/FIBO/SKR/PROV‑O) | H | IDEA | (queued) | D‑TTL | | D‑ACTION | `ogar-actionable` — lifecycle → `ActionDef`/`KausalSpec` | H | IDEA | (queued) | D‑PATTERN |