@@ -138,6 +138,11 @@ Zig 0.15.2. Produces `libzig_api.so` (shared) and `libzig_api.a` (static).
138138| HTTP/1.1 server pool. Replaces v-grpc + v-rest. Handles `/render`,
139139 `/context`, `/health`, `/`, and gRPC-style `/gnosis.GnosisService/*` paths.
140140 `uapi_gnosis_{create,start,stop,destroy,state,health}`.
141+ **Edge hook API (2026-04-17):** `uapi_gnosis_set_handler(handle, fn_ptr)` and
142+ `uapi_gnosis_write_response` support single-port + path-routed edge dispatch;
143+ see §"Canonical single-port shape" below. When a hook is registered, gnosis
144+ calls the edge fn for every request instead of the built-in routes — no
145+ separate accept loop needed in the edge repo.
141146
142147| `connector.zig`
143148| 64-slot HTTP connector pool for 11 hyperpolymath services.
@@ -187,7 +192,79 @@ main = do
187192
188193=== From another Zig project
189194
190- Option A — link the shared library and `@cImport` the header:
195+ ==== Canonical single-port shape (2026-04-17)
196+
197+ The standard pattern for Zig edge repos is one gnosis pool slot plus
198+ `uapi_gnosis_set_handler`. The edge repo *does not* bind its own sockets or
199+ run its own accept loops — gnosis owns the listener; the edge supplies the
200+ dispatch function.
201+
202+ [source,zig]
203+ ----
204+ const c = @cImport(@cInclude("zig_api.h"));
205+
206+ // GnosisRequest and GnosisResponse are defined in zig_api.h.
207+ // @cImport translates C pointer types to [*c] in Zig.
208+ // Convert to regular pointers at the top of the handler for ergonomic field
209+ // access:
210+ //
211+ // const resp: *c.GnosisResponse = @ptrCast(resp_c);
212+ // const req: *const c.GnosisRequest = @ptrCast(req_c);
213+ //
214+ export fn myEdgeHandler(
215+ req_c: [*c]const c.GnosisRequest,
216+ resp_c: [*c]c.GnosisResponse,
217+ ) callconv(.c) void {
218+ const resp: *c.GnosisResponse = @ptrCast(resp_c);
219+ const req: *const c.GnosisRequest = @ptrCast(req_c);
220+
221+ const path = std.mem.span(req.path);
222+
223+ if (std.mem.startsWith(u8, path, "/api/v1/")) {
224+ const body = "{\"status\":\"ok\"}";
225+ c.uapi_gnosis_write_response(resp_c, 200, "application/json",
226+ body, body.len);
227+ } else {
228+ const e = "{\"error\":\"not found\"}";
229+ c.uapi_gnosis_write_response(resp_c, 404, "application/json",
230+ e, e.len);
231+ }
232+ _ = resp; _ = req; // suppress unused-var warnings if not needed
233+ }
234+
235+ pub fn main() !void {
236+ _ = c.uapi_init();
237+ defer c.uapi_teardown();
238+
239+ const handle = c.uapi_gnosis_create(4000);
240+ // Register the edge handler BEFORE uapi_gnosis_start.
241+ _ = c.uapi_gnosis_set_handler(handle, &myEdgeHandler);
242+ _ = c.uapi_gnosis_start(handle);
243+ defer c.uapi_gnosis_stop(handle);
244+ defer c.uapi_gnosis_destroy(handle);
245+
246+ while (c.uapi_gnosis_state(handle) == c.UAPI_SERVER_LISTENING) {
247+ std.Thread.sleep(std.time.ns_per_s);
248+ }
249+ }
250+ ----
251+
252+ Key rules for the edge hook:
253+
254+ * Call `uapi_gnosis_set_handler` *after* `uapi_gnosis_create` and *before*
255+ `uapi_gnosis_start`. Calling after start returns `UAPI_ERR`.
256+ * Pass `NULL` as `handler_fn` to revert to built-in gnosis routes.
257+ * `GnosisRequest` fields (`method`, `path`, `body_ptr`, `body_len`) are valid
258+ only for the duration of the handler call — copy any data you need to keep.
259+ * `GnosisResponse` fields must be fully populated before the handler returns.
260+ Use `uapi_gnosis_write_response` as a convenience or fill the struct directly.
261+ * `body_ptr` in `GnosisResponse` must remain valid until `uapi_gnosis_write_response`
262+ returns — a module-level or stack buffer that outlives the call is fine;
263+ an arena-allocated slice freed before return is not.
264+
265+ ==== Option B — connector usage only
266+
267+ Link the shared library and `@cImport` the header:
191268
192269[source,zig]
193270----
@@ -203,7 +280,7 @@ pub fn main() !void {
203280}
204281----
205282
206- Option B — add `libzig_api` as a `build.zig` dependency and import symbols
283+ Option C — add `libzig_api` as a `build.zig` dependency and import symbols
207284directly from the Zig modules (internal use only; C ABI is the stable surface).
208285
209286=== From Rust
@@ -242,6 +319,16 @@ const zig_api = b.addSharedLibraryFromPath(
242319exe.linkLibrary(zig_api);
243320exe.addIncludePath(.{ .cwd_relative = PATH_TO_GENERATED_ABI });
244321----
322+ . *Use the canonical single-port + set_handler shape* (see §"Canonical
323+ single-port shape" above) for all HTTP-serving edge repos. **Do not** bind
324+ your own sockets or run your own accept loops — gnosis owns the listener.
325+ Register your dispatch function with `uapi_gnosis_set_handler` before
326+ calling `uapi_gnosis_start`. Path-route all protocols (REST, GraphQL,
327+ gRPC-JSON) inside a single edge handler. Reference implementations:
328+ - `standards/lol/api/zig-gateway/src/main.zig` — LoL gateway (v0.2.0,
329+ single-port, path-routed, 3-protocol)
330+ - `aerie/src/api/zig/main.zig` — Aerie gateway (single-port, path-routed,
331+ REST + GraphQL + gRPC-JSON)
245332. *Place repo-specific Idris2 ABI proofs in `src/abi/` of the edge repo.*
246333 Import `ZigApi.ABI.Types` and `ZigApi.ABI.Connector` from `zig-api.ipkg`
247334 (add it as a dependency in the edge repo's `.ipkg`). Extend, do not fork.
@@ -316,6 +403,14 @@ The trigger was the estate-wide V-lang ban on 2026-04-10 (V-lang detected via
316403| Idris2 ABI fully proved; Zig runtime builds and passes unit tests;
317404 integration tests require `gnosis` binary on `$PATH`.
318405
406+ | `uapi_gnosis_set_handler` + `uapi_gnosis_write_response`
407+ | ACTIVE (2026-04-17)
408+ | Edge hook API. Enables single-port + path-routed dispatch for all edge
409+ repos. `set_handler` registers a C-ABI `GnosisHandlerFn` before start;
410+ `write_response` is a convenience fill helper. Idris2 ABI:
411+ `ZigApi.ABI.Foreign.prim__gnosisSetHandler`. Header: `zig_api.h` lines
412+ 197–212.
413+
319414| `zig-groove-bridge`
320415| PARTIAL
321416| Dodeca-API + discovery logic written; attachment / full Groove manifest
0 commit comments