Skip to content

Commit 89d421c

Browse files
committed
docs: record B2-transport — the transport-agnostic action daemon (ws edge)
The live action daemon shipped in rs-graph-llm's graph-flow-action-ogar::daemon: a transport-agnostic core (Daemon::react/serve) + a Transport trait + the action-ws WebSocket edge (WsTransport), proven by a mock-server roundtrip. HIRO distributes actions over both a WebSocket and an internal Kafka bus, so the dispatch is written once and the wire is a thin Transport shell — Kafka is the reserved second edge. Connection identity is an Auth type shaped after OGIT NTO/Auth/Configuration (auth_store 0x0B01): the principal that connects is the actor the gate authorizes. - ARAGO-ACTIONHANDLER-PARITY: B2-transport §3 bullet + two scorecard rows (daemon+ws SHIPPED, kafka edge remaining), verdict, cross-ref to daemon.rs. - DISCOVERY-MAP: D-ACTIONHANDLER-TRANSPORT (G core+ws / H kafka). - EPIPHANIES: E-ACTIONHANDLER-TRANSPORT — multi-wire ⇒ transport-agnostic core; OGIT Auth unifies connect-identity with gate-actor. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYvNjD8M8LMNYbRy3gq2FP
1 parent 149a73b commit 89d421c

3 files changed

Lines changed: 72 additions & 12 deletions

File tree

.claude/board/EPIPHANIES.md

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

10+
## 2026-06-24 — E-ACTIONHANDLER-TRANSPORT — the daemon is transport-agnostic because HIRO is multi-wire; and the OGIT Auth type unifies "who connects" with "who the gate authorizes"
11+
12+
**Status:** FINDING (`[G]` for the core + WebSocket edge; `[H]` for the Kafka edge).
13+
14+
Two design facts surfaced building B2-transport (the live action daemon, in
15+
rs-graph-llm `graph-flow-action-ogar::daemon`):
16+
17+
1. **HIRO distributes actions over more than one wire** — a handler-facing
18+
WebSocket (`action-ws`) AND an internal Kafka bus that legacy handlers consume
19+
directly (operator note, 2026-06-24). The wire differs; the dispatch doesn't.
20+
So the daemon is factored as: `Daemon::react` (the transport-agnostic core —
21+
one inbound `action-ws` frame → outbound frames, running the gate + executor,
22+
pure/no-I/O) + a `Transport` trait (`recv`/`send`, the swappable edge) +
23+
`Daemon::serve` (the loop, generic over `Transport`). The WebSocket edge
24+
(`WsTransport`) and a future Kafka edge (`rdkafka`) share `serve` verbatim —
25+
the gated dispatch is written once, the wire is a thin shell. This is the
26+
action-arm analogue of the codec stack's "one algebra, many carriers": one
27+
dispatch, many transports.
28+
29+
2. **The OGIT Auth type unifies the two identities that must be the same.** A
30+
handler's connection presents a credential; the gate authorizes an actor.
31+
These MUST be the same principal — and OGIT's `NTO/Auth/Configuration` (the
32+
`auth_store` class, OGAR `0x0B01`) already unifies them: it is keyed by
33+
`accountId` and maps `sub` → actor (`0x0104`), org/tenant → scope. So the
34+
daemon's `Auth` type is shaped after it: one value carries the `token` the
35+
transport presents (the `token-$TOKEN` subprotocol) AND the `account` the gate
36+
authorizes as (`accountId` → actor). `Daemon::new` takes `&Auth` and derives
37+
the gate actor from `auth.account`; `WsTransport::connect` takes `&Auth` and
38+
presents `auth.token`. The identity that connects IS the identity the RBAC
39+
grant is checked against — structurally, not by convention. (A future
40+
producer-side `auth_from_ogit(entity)` lift would populate `Auth` from a real
41+
`NTO/Auth/Configuration` node, the same way `assemble_action_handler` lifts the
42+
handler contract.)
43+
44+
Proven by `ws_roundtrip_against_a_mock_server` (engine `submitAction` → ack → real
45+
command → result over a live socket) + 10 pure-core tests. Scorecard: B2-transport
46+
WebSocket edge SHIPPED; Kafka edge reserved (`D-ACTIONHANDLER-TRANSPORT`).
47+
48+
---
49+
1050
## 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`
1151

1252
**Status:** FINDING (`[G]` for capabilities; `[H]` for the applicabilities envelope).

docs/ARAGO-ACTIONHANDLER-PARITY.md

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -201,10 +201,21 @@ downstream. The remaining bricks:
201201
unauthorized actor (`Denied`) or a MUL `Block` (`Escalated`) never reaches the
202202
OGAR executor. Three tests pin it; `NativeCommandExecutor` runs the real command
203203
only on the authorized path. OGAR owns the executor; rs-graph-llm owns the gate.
204-
- **B2-transport — the live WebSocket loop.** Wrap `handle_submit` in a
205-
`tokio-tungstenite` client (connect with the `token-$TOKEN` subprotocol, JSON-
206-
codec the six `action_ws` message types, drive the dispatch, retry-on-no-ack).
207-
All the message types, connection path, and auth are now pinned (§2a).
204+
- **B2-transport — the live daemon (SHIPPED, WebSocket edge).** Built in
205+
rs-graph-llm's `graph-flow-action-ogar::daemon` as a **transport-agnostic** core:
206+
`Daemon::react` turns one inbound `action-ws` JSON frame into the outbound frames
207+
it warrants (`acknowledged` + `sendActionResult`, or `negativeAcknowledged`),
208+
running the hard gate (`run_gated`) + the executor in between — pure, no I/O. A
209+
`Transport` trait is the swappable edge (`recv`/`send`); `Daemon::serve` is the
210+
loop. The **`WsTransport`** WebSocket edge (`feature = "ws"`) connects with the
211+
`token-$TOKEN` subprotocol and is proven by a mock-server roundtrip
212+
(`ws_roundtrip_against_a_mock_server`: engine `submitAction` → ack → run → result
213+
over a real socket). The connection identity is an `Auth` type shaped after OGIT
214+
`NTO/Auth/Configuration` (`auth_store` `0x0B01`) — the same principal the
215+
transport authenticates as (`accountId`) is the actor the gate authorizes.
216+
**HIRO also distributes actions over Kafka**; that edge (`rdkafka` over the same
217+
`Transport` trait) is reserved — the core is ready, it needs the topic/record
218+
shape pinned.
208219
- **B2-lift — the instance config lift (SHIPPED for capabilities).** Parse a
209220
deployed handler's REST registration → the concrete signatures the *schema*
210221
half cannot supply. `GET /capabilities` is **shipped**: `registration::{RegisteredCapability,
@@ -244,7 +255,8 @@ transport over them.
244255
| **Executor — native target (B1)** |`[G]` SHIPPED | `ogar-action-handler::NativeCommandExecutor` runs `ExecuteCommand` for real; `full_dispatch_runs_a_real_command` |
245256
| **Hard gate before executor (B1-uplink)** |`[G]` SHIPPED | rs-graph-llm `graph-flow-action-ogar::GatedOgarHandler``commit_via` (RBAC ∧ guard ∧ MUL) lands before `handle`; `take_result()` is `None` iff the gate refused (3 tests) |
246257
| **Executor — SSH/REST/WinRM (B1)** |`[H]` | further `CapabilityExecutor` impls (rs-graph-llm `graph-flow-action`) |
247-
| **Live WebSocket transport (B2-transport)** |`[H]` | wrap `handle_submit` in a `tokio-tungstenite` loop + JSON codec (all shapes pinned, §2a) |
258+
| **Live transport — daemon core + WebSocket (B2-transport)** |`[G]` SHIPPED | rs-graph-llm `graph-flow-action-ogar::daemon`: transport-agnostic `Daemon::react`/`serve` + `Transport` trait + `WsTransport` (action-ws), gate-driving; mock-server roundtrip. `Auth` ← OGIT `NTO/Auth/Configuration` |
259+
| **Live transport — Kafka edge (B2-transport)** |`[H]` | `rdkafka` over the same `Transport` trait (action topic → result topic); core ready, needs the topic/record shape pinned |
248260
| **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) |
249261
| **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 |
250262

@@ -259,13 +271,17 @@ MUL-blocked action never executes (proven structurally — `take_result()` is
259271
`None`). The **whole instance lift is shipped too** — real `GET /capabilities`
260272
and `GET /applicabilities` JSON bodies lift to concrete `ActionParam[]` (runs
261273
end-to-end, `rest_registration_lifts_binds_and_runs`) and per-handler `StateGuard`
262-
sets (`rest_applicabilities_lift_to_per_handler_guards`). What's left for
263-
a **live** drop-in replacement of arago's Python daemon: **B2-transport** (the
264-
WebSocket loop — all shapes/auth pinned) and the **non-native executor targets**
265-
(SSH/REST). Each is transport/runner glue over existing types — **no missing IR,
266-
no missing protocol mapping**. That is the honest state: OGAR *is* an ActionHandler
267-
that runs commands here, reads its own registration, and gates every action; a thin
268-
WebSocket transport away from connecting to a live HIRO engine.
274+
sets (`rest_applicabilities_lift_to_per_handler_guards`). And the **live daemon
275+
runs over a real socket**`graph-flow-action-ogar::daemon` drives the gated
276+
dispatch through a `Transport` trait, with the `action-ws` WebSocket edge proven
277+
by a mock-server roundtrip. What's left for a **live** drop-in replacement of
278+
arago's Python daemon: the **Kafka transport edge** (HIRO's internal bus —
279+
`rdkafka` over the same `Transport` trait, needs the topic/record shape pinned)
280+
and the **non-native executor targets** (SSH/REST). Each is a single edge/runner
281+
impl over existing types — **no missing IR, no missing protocol mapping**. That is
282+
the honest state: OGAR *is* an ActionHandler that reads its own registration,
283+
gates every action, runs commands, and speaks `action-ws` over a live socket;
284+
a Kafka consumer away from HIRO's internal bus.
269285

270286
---
271287

@@ -299,6 +315,9 @@ is replaceable; the parity claim is certified, not argued.
299315
- `rs-graph-llm/graph-flow-action-ogar` — the **uplink**: OGAR's
300316
`CapabilityExecutor` behind the hard gate (`GatedOgarHandler` / `run_gated`);
301317
`commit_via` lands before any execution.
318+
- `rs-graph-llm/graph-flow-action-ogar/src/daemon.rs`**B2-transport**: the
319+
transport-agnostic `Daemon` (`react`/`serve`) + the `Transport` trait +
320+
`WsTransport` (action-ws WebSocket edge) + the OGIT-`Auth`-derived identity.
302321
- arago: `github.com/arago/ActionHandlers`,
303322
`arago/python-hiro-stonebranch-actionhandler`, HIRO 7 Action API tutorial.
304323
- **HIRO 7 Action API machine-readable specs (the authoritative harvest, §2a):**

docs/DISCOVERY-MAP.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ two halves of a cell. ADR‑026 names the cascade that ties them.
213213
| 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 |
214214
| 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 |
215215
| 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 |
216+
| D‑ACTIONHANDLER‑TRANSPORT | The live action daemon (B2-transport), **transport-agnostic** by construction: rs‑graph‑llm `graph-flow-action-ogar::daemon::Daemon::react` turns one inbound `action-ws` JSON frame into the outbound frames it warrants (ack + `sendActionResult`, or nack), running the hard gate (`run_gated`) + executor in between — pure, no I/O. A `Transport` trait (`recv`/`send`) is the swappable edge; `Daemon::serve` is the loop; both the WebSocket and a future Kafka edge share it verbatim (HIRO distributes actions over BOTH wires — the wire differs, the dispatch doesn't). The `WsTransport` action-ws edge (`feature = "ws"`, tokio-tungstenite) presents the `token-$TOKEN` subprotocol and is proven by `ws_roundtrip_against_a_mock_server` (engine submitAction → ack → real command → result over a socket). Connection identity is an `Auth` type shaped after OGIT `NTO/Auth/Configuration` (`auth_store` 0x0B01): the principal the transport authenticates as (`accountId`) IS the actor the gate authorizes. Remaining: the Kafka edge (`rdkafka` over the same trait — core ready, needs topic/record shape) and SSH/REST executors | G (core + ws edge) / H (kafka edge) | CODED | `rs-graph-llm/graph-flow-action-ogar/src/daemon.rs` | D‑ACTIONHANDLER‑UPLINK, D‑ACTIONHANDLER‑B2LIFT |
216217
| D‑OSM | `ogar-from-osm-pbf` — Node/Way/Relation; quadkey NiblePath from resolved geometry | H | IDEA | (queued) | D‑VOCAB, `[per rt]` D‑OSM‑3 |
217218
| D‑PATTERN | `ogar-pattern` — recognition library + confidence (FMA‑D/FIBO/SKR/PROV‑O) | H | IDEA | (queued) | D‑TTL |
218219
| D‑ACTION | `ogar-actionable` — lifecycle → `ActionDef`/`KausalSpec` | H | IDEA | (queued) | D‑PATTERN |

0 commit comments

Comments
 (0)