Skip to content

Commit a1bc96a

Browse files
committed
action handler: B2-lift — REST registration instance lift (capabilities)
Parse a deployed handler's REST registration into the concrete OGAR signatures the *schema* half cannot supply. The OGIT ontology declares only that a capability has mandatoryParameters/optionalParameters slots; the concrete (name, mandatory, default) tuples live in a deployed handler's config. B2-lift reads them from the live REST view, so the two halves compose: schema lift gives the contract shape, instance lift gives the deployed values. Producer stays parser-free (the crate-family split): - ogar-from-schema/src/registration.rs — typed REST DTOs (RegisteredCapability / RegisteredParam / ModelFilter, Deserialize behind the existing `serde` feature) + the pure lift: lift_registration -> ConcreteCapability (concrete ActionParam[]), and model_filter_to_guard (arago ModelFilter{Var,Mode,Value} -> StateGuard, field-for-field). No serde_json, no I/O. 5 tests incl. the lifted signature driving action_ws::bind_parameters. - ogar-action-handler — the runtime owns I/O: parse_capabilities does the serde_json read of a GET /capabilities body. Proven end-to-end by rest_registration_lifts_binds_and_runs (real JSON -> lift -> bind -> the NativeCommandExecutor runs the command). Same producer-defines-types / runtime-does-I/O split the whole crate family keeps: ogar-from-schema gains a REST front-end without gaining a parser dep. Remaining for full B2-lift: the GET /applicabilities MapOfApplicabilities JSON envelope read (the ModelFilter->StateGuard lift is already shipped). Docs: ARAGO-ACTIONHANDLER-PARITY (B2-lift §3 bullet + two scorecard rows + verdict + cross-refs), D-ACTIONHANDLER-B2LIFT discovery row, E-ACTIONHANDLER-B2LIFT epiphany (the parser-free producer / runtime-does-the-read split). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYvNjD8M8LMNYbRy3gq2FP
1 parent 13b4f1b commit a1bc96a

7 files changed

Lines changed: 437 additions & 8 deletions

File tree

.claude/board/EPIPHANIES.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,42 @@
77
88
---
99

10+
## 2026-06-24 — E-ACTIONHANDLER-B2LIFT — the producer stays parser-free even when lifting a JSON REST response: it defines the `Deserialize` DTOs + the pure lift, the runtime does the `from_str`
11+
12+
**Status:** FINDING (`[G]` for capabilities; `[H]` for the applicabilities envelope).
13+
14+
B2-lift (the REST registration instance lift) had to read a JSON `GET
15+
/capabilities` body — but `ogar-from-schema` is deliberately parser-free on its
16+
default path (a narrow line-oriented TTL walker; a hand-rolled JSON *encoder* in
17+
`action_ws`, never a decoder). The resolution kept the producer pure by splitting
18+
along the crate family's existing seam:
19+
20+
- **Producer (`ogar-from-schema::registration`) defines the typed REST DTOs**
21+
(`RegisteredCapability` / `RegisteredParam` / `ModelFilter`, `Deserialize` behind
22+
the already-present `serde` feature) **and the pure lift mapping**
23+
(`lift_registration → ConcreteCapability` with concrete `ActionParam[]`;
24+
`model_filter_to_guard`: arago `ModelFilter{Var,Mode,Value}``KausalSpec::StateGuard`
25+
field-for-field). No `serde_json`, no I/O.
26+
- **Runtime (`ogar-action-handler::parse_capabilities`) does the `serde_json::from_str`.**
27+
The runtime crate already owns I/O (it runs commands); reading a REST response is
28+
the same kind of work. `serde_json` lives there, never in the producer.
29+
30+
This is the same producer-defines-types / runtime-does-I/O split the whole crate
31+
family keeps (schema lift defines `Class`; source-AST producers fill behavior; the
32+
runtime executes). The payoff is concrete: `ogar-from-schema` gains a REST front-end
33+
without gaining a parser dependency.
34+
35+
**The lift fills a gap the schema cannot reach.** The OGIT ontology declares only
36+
*that* a capability has `mandatoryParameters` / `optionalParameters` slots
37+
(`CapabilitySlot`); the concrete `(name, mandatory, default)` tuples exist only in a
38+
*deployed* handler's config. B2-lift reads them from the live REST view — so the
39+
two halves compose: schema lift gives the contract shape, instance lift gives the
40+
deployed values, and the result drives `bind_parameters` → the executor. Proven by
41+
`rest_registration_lifts_binds_and_runs` (real JSON → lift → bind → run). The B2-lift
42+
rows in the parity scorecard + `D-ACTIONHANDLER-B2LIFT` in the discovery map.
43+
44+
---
45+
1046
## 2026-06-24 — E-ACTIONHANDLER-UPLINK — the hard gate is wired to the executor without OGAR ever taking a `lance-graph` dep: OGAR owns the executor, rs-graph-llm owns the gate, one seam crate joins them
1147

1248
**Status:** FINDING (`[G]`, 3 tests).

crates/ogar-action-handler/Cargo.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,10 @@ description = "OGAR-native HIRO ActionHandler runtime — CapabilityExecutor imp
1212
default = []
1313

1414
[dependencies]
15-
ogar-from-schema = { path = "../ogar-from-schema" }
15+
ogar-from-schema = { path = "../ogar-from-schema", features = ["serde"] }
1616
ogar-vocab = { path = "../ogar-vocab" }
17+
# The runtime owns I/O: it reads the REST Action API registration response
18+
# (`GET /capabilities`), so it carries the JSON reader. The producer crate
19+
# (`ogar-from-schema`) stays parser-free — it defines the `Deserialize` DTOs +
20+
# the pure lift; this crate does the `from_str`. (B2-lift.)
21+
serde_json = "1.0"

crates/ogar-action-handler/src/lib.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,27 @@
4141
use std::process::Command;
4242

4343
use ogar_from_schema::action_ws::CapabilityExecutor;
44+
use ogar_from_schema::registration::{
45+
ConcreteCapability, RegisteredCapabilities, lift_registration,
46+
};
47+
48+
/// Read a deployed handler's `GET /capabilities` REST response (a JSON
49+
/// `MapOfCapabilities`) and lift it into the concrete OGAR capability signatures
50+
/// — the **B2-lift** brick's I/O half (`ogar-from-schema::registration` owns the
51+
/// pure lift; this crate owns the JSON read, per the producer/runtime split).
52+
///
53+
/// Each [`ConcreteCapability`] carries the concrete `ActionParam[]` the schema
54+
/// half cannot supply — feed it to
55+
/// [`ogar_from_schema::action_ws::bind_parameters`] to validate an engine
56+
/// `submitAction`'s `parameters` before executing.
57+
///
58+
/// # Errors
59+
/// Returns the `serde_json` error message if the body is not a valid
60+
/// `MapOfCapabilities`.
61+
pub fn parse_capabilities(json: &str) -> Result<Vec<ConcreteCapability>, String> {
62+
let caps: RegisteredCapabilities = serde_json::from_str(json).map_err(|e| e.to_string())?;
63+
Ok(lift_registration(&caps))
64+
}
4465

4566
/// Reference executor for the **native** target: runs a capability's command via
4667
/// the local POSIX shell (`sh -c`). The concrete B1 impl that makes "OGAR
@@ -140,6 +161,54 @@ mod tests {
140161
assert!(err.contains("ExecuteCommand"), "got: {err}");
141162
}
142163

164+
/// B2-lift end-to-end: a real `GET /capabilities` REST response (JSON)
165+
/// parses → lifts to a concrete signature → the signature binds engine
166+
/// parameters → the native executor runs the command. The deployed config
167+
/// (not the schema) supplied the concrete `(name, mandatory, default)`.
168+
#[test]
169+
fn rest_registration_lifts_binds_and_runs() {
170+
use ogar_from_schema::action_ws::bind_parameters;
171+
172+
// A deployed handler's GET /capabilities body (MapOfCapabilities).
173+
let body = r#"{
174+
"ExecuteCommand": {
175+
"description": "run a command",
176+
"mandatoryParameters": {
177+
"command": { "description": "the command" }
178+
},
179+
"optionalParameters": {
180+
"shell": { "description": "the shell", "default": "sh" }
181+
}
182+
}
183+
}"#;
184+
185+
let lifted = parse_capabilities(body).expect("valid MapOfCapabilities");
186+
assert_eq!(lifted.len(), 1);
187+
let cap = &lifted[0];
188+
assert_eq!(cap.name, "ExecuteCommand");
189+
// The concrete signature the schema half could not produce:
190+
assert_eq!(cap.params.len(), 2);
191+
assert!(
192+
cap.params
193+
.iter()
194+
.any(|p| p.name == "command" && p.mandatory)
195+
);
196+
assert!(
197+
cap.params
198+
.iter()
199+
.any(|p| p.name == "shell" && !p.mandatory && p.default.as_deref() == Some("sh"))
200+
);
201+
202+
// The lifted signature drives bind (optional `shell` default filled),
203+
// and the bound command runs on the native executor.
204+
let supplied = vec![("command".to_owned(), "echo lifted".to_owned())];
205+
let bound = bind_parameters(&supplied, &cap.params).expect("binds");
206+
let result = NativeCommandExecutor
207+
.execute("ExecuteCommand", &bound)
208+
.expect("runs");
209+
assert_eq!(result[0], ("output".to_owned(), "lifted".to_owned()));
210+
}
211+
143212
/// End-to-end: the dispatch core + this executor run a real command and
144213
/// produce the `sendActionResult` — "OGAR running it here," native target.
145214
#[test]

crates/ogar-from-schema/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ use ogar_vocab::{Attribute, Class, EnumDecl, EnumSource, Language};
5959

6060
pub mod action_ws;
6161
pub mod do_arm;
62+
pub mod registration;
6263
pub mod sgo;
6364
pub mod ttl;
6465
pub mod ttl_emit;

0 commit comments

Comments
 (0)