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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 49 additions & 1 deletion crates/ogar-action-handler/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -63,6 +67,22 @@ pub fn parse_capabilities(json: &str) -> Result<Vec<ConcreteCapability>, 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<BTreeMap<String, Vec<KausalSpec>>, 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.
Expand Down Expand Up @@ -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]
Expand Down
65 changes: 65 additions & 0 deletions crates/ogar-from-schema/src/registration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,41 @@ pub fn lift_applicability(filters: &[ModelFilter]) -> Vec<KausalSpec> {
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<ModelFilter>,
}

/// The `GET /applicabilities` response — a `MapOfApplicabilities` keyed by
/// handler id.
pub type RegisteredApplicabilities = BTreeMap<String, RegisteredApplicability>;

/// 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<String, Vec<KausalSpec>> {
apps.iter()
.map(|(handler, app)| (handler.clone(), lift_applicability(&app.model_filters)))
.collect()
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -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()],
}]
);
}
}
40 changes: 22 additions & 18 deletions docs/ARAGO-ACTIONHANDLER-PARITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<KausalSpec>`) — 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
Expand All @@ -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
Expand All @@ -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.

---

Expand All @@ -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<ClassRbac>`.
Expand Down
2 changes: 1 addition & 1 deletion docs/DISCOVERY-MAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
Loading