net-mesh 0.23.0

High-performance, schema-agnostic, backend-agnostic event bus
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
# Net C SDK

One header, one shared library. This is the entire C SDK.

Unlocks every language that can call C: C++, Zig, Nim, Lua, Ruby, Java, C#, Dart, Swift, Kotlin, Haskell, Erlang, PHP.

Latest release: [v0.10 — "Killing Moon" Phase III](../docs/RELEASE_v0.10_KILLING_MOON_PHASE_III.md) is a hardening release. The FFI surface picked up several behavior changes that affect any C consumer; see [Behavior changes in v0.10 (FFI)](#behavior-changes-in-v010-ffi) below for the per-call summary.

## Files

- `net.h` — narrow event-bus surface (init / ingest / poll / stats / shutdown). Uses the `NET_SDK_H` include guard.
- `net.go.h` — broader mesh + compute surface (sessions, channels, capabilities, NAT, daemon dispatch, custom placement filters, daemon caps, predicate helpers, capability validation, predicate debug session). Pulls in `net_cortex.h` for the RedEX / CortEX / NetDb half. In-crate mirror of [`go/net.h`](../../../../go/net.h) at the repo root. Also uses `NET_SDK_H` and overlaps with the event-bus surface from `net.h`, but it is **not** a strict superset (`net_ingest_raw_ex`, `net_poll_ex`, and `net_stats_ex` are still `net.h`-only). Pick **one** of `net.h` or `net.go.h` per translation unit based on the symbols you need; if you need both surfaces in the same program, split across translation units.
- `net_cortex.h` — RedEX (append-only logs), CortEX (Tasks + Memories adapters), and NetDb (cross-adapter bundle + snapshot) surface. Self-contained — depends only on `<stdint.h>` and `<stddef.h>`, so C / Zig / Swift consumers who want just this slice of `libnet` can include it without dragging in the mesh / compute surface from `net.go.h`. Independent header guard (`NET_CORTEX_H`). Symbols resolve when the cdylib is built with `--features "netdb redex-disk"`.
- `net_rpc.h` — nRPC C SDK (request/response surface for the separate `libnet_rpc` cdylib). Independent header guard (`NET_RPC_H`); composes cleanly alongside whichever of `net.h` / `net.go.h` you chose.
- `net_meshdb.h` — MeshDB C SDK (federated query layer over the capability-query primitives + CortEX folds; lives in the separate `libnet_meshdb` cdylib). Independent header guard (`NET_MESHDB_H`); composes cleanly alongside the other headers.
- `net_meshos.h` — MeshOS daemon-author C SDK (operator-side handle + control-event channel for the separate `libnet_meshos` cdylib). Independent header guard (`NET_MESHOS_H`); composes cleanly alongside the other headers.
- Libraries: `libnet.{so,dylib,dll}` (main, also serves `net_cortex.h` symbols) + `libnet_compute.{so,dylib,dll}` (compute) + `libnet_rpc.{so,dylib,dll}` (nRPC) + `libnet_meshdb.{so,dylib,dll}` (MeshDB) + `libnet_meshos.{so,dylib,dll}` (MeshOS). Build with `cargo build --release --features ffi,net` for `libnet`; `-p net-compute-ffi`, `-p net-rpc-ffi`, `-p net-meshdb-ffi`, `-p net-meshos-ffi` for the others.
- Examples: `examples/basic.c` (event-bus quickstart) + `examples/capability.c` (stateless capability / predicate / where-header helpers) + `examples/meshdb.c` (MeshDB factory AST + runner + iterator + sentinel-envelope decoder).

## Build

```bash
# Build the shared library
cargo build --release --features ffi,net

# The library is at:
# Linux:  target/release/libnet.so
# macOS:  target/release/libnet.dylib
# Windows: target/release/net.dll
```

## Quick Start

```c
#include "net.h"
#include <stdio.h>
#include <string.h>

int main(void) {
    // Create a node
    net_handle_t node = net_init("{\"num_shards\": 4}");
    if (!node) return 1;

    // Ingest
    const char* event = "{\"token\": \"hello\"}";
    net_receipt_t receipt;
    net_ingest_raw_ex(node, event, strlen(event), &receipt);
    printf("shard=%d ts=%llu\n", receipt.shard_id, (unsigned long long)receipt.timestamp);

    // Flush
    net_flush(node);

    // Poll (structured, no JSON parsing needed)
    net_poll_result_t result;
    net_poll_ex(node, 100, NULL, &result);
    for (size_t i = 0; i < result.count; i++) {
        printf("%.*s\n", (int)result.events[i].raw_len, result.events[i].raw);
    }
    net_free_poll_result(&result);

    // Stats (structured)
    net_stats_t stats;
    net_stats_ex(node, &stats);
    printf("ingested=%llu dropped=%llu\n",
        (unsigned long long)stats.events_ingested,
        (unsigned long long)stats.events_dropped);

    // Shutdown
    net_shutdown(node);
    return 0;
}
```

## Compile and Link

```bash
# GCC
gcc -o app app.c -L target/release -lnet -lpthread -ldl -lm

# Run
LD_LIBRARY_PATH=target/release ./app       # Linux
DYLD_LIBRARY_PATH=target/release ./app     # macOS
```

## API

### Lifecycle

| Function | Description |
|----------|-------------|
| `net_init(config_json)` | Create a node. NULL config for defaults. Returns handle. |
| `net_shutdown(handle)` | Shut down and free resources. |
| `net_version()` | Library version string (static, do not free). |
| `net_num_shards(handle)` | Number of active shards. |

### Ingestion

| Function | Description |
|----------|-------------|
| `net_ingest_raw(handle, json, len)` | Ingest raw JSON (fastest). |
| `net_ingest_raw_ex(handle, json, len, &receipt)` | Ingest with receipt (shard_id, timestamp). |
| `net_ingest(handle, json, len)` | Ingest with JSON validation. |
| `net_ingest_raw_batch(handle, jsons, lens, count)` | Batch ingest. Returns count. |
| `net_ingest_batch(handle, json_array)` | Ingest from JSON array string. |

### Consumption

| Function | Description |
|----------|-------------|
| `net_poll(handle, request_json, out_buffer, buffer_len)` | Poll (JSON interface). |
| `net_poll_ex(handle, limit, cursor, &result)` | Poll (structured, no JSON). Free with `net_free_poll_result`. |
| `net_free_poll_result(&result)` | Free a structured poll result. |

### Statistics

| Function | Description |
|----------|-------------|
| `net_stats(handle, out_buffer, buffer_len)` | Stats (JSON interface). |
| `net_stats_ex(handle, &stats)` | Stats (structured, no JSON). |

### Utilities

| Function | Description |
|----------|-------------|
| `net_flush(handle)` | Flush pending batches. |
| `net_generate_keypair()` | Generate mesh keypair. Free with `net_free_string`. |
| `net_free_string(s)` | Free a string from `net_generate_keypair`. |

### Redis Streams dedup helper (`redis` feature)

The Redis adapter writes a stable `dedup_id` field on every XADD
entry (`{producer_nonce:hex}:{shard_id}:{sequence_start}:{i}`) so
duplicate stream entries from the producer-side `MULTI/EXEC`
timeout race can be filtered at consume time. Helper API:

| Function | Description |
|----------|-------------|
| `net_redis_dedup_new(capacity)` | Create a helper. `0` selects the default 4096. Never returns NULL. |
| `net_redis_dedup_free(handle)` | Free a helper handle. NULL is a no-op. |
| `net_redis_dedup_is_duplicate(handle, dedup_id)` | Test-and-insert. Returns 1 = duplicate, 0 = new, -1 = NULL, -2 = invalid UTF-8. |
| `net_redis_dedup_len(handle)` | Number of distinct ids tracked. |
| `net_redis_dedup_capacity(handle)` | Configured LRU capacity. |
| `net_redis_dedup_is_empty(handle)` | 1 = empty, 0 = non-empty, -1 = NULL. |
| `net_redis_dedup_clear(handle)` | Drop all tracked ids (e.g. on consumer-group rebalance). |

Canonical consumer loop:

```c
net_redis_dedup_t* dedup = net_redis_dedup_new(0);

/* For each XRANGE / XREAD entry, extract the `dedup_id` field
 * from the field map and probe the helper. */
const char* dedup_id = ...; /* from your Redis client */
int rc = net_redis_dedup_is_duplicate(dedup, dedup_id);
if (rc == 0) {
    process(entry);     /* new — process AND we're now marked seen */
} else if (rc == 1) {
    /* duplicate — skip */
}

net_redis_dedup_free(dedup);
```

The helper is transport-agnostic — bring your own `hiredis` /
`redis-rs` / equivalent client. Sizing: ~10k events/sec at a 1
min dedup window → capacity ~600,000. Default 4096 fits
low-throughput / short-window deployments.

## Types

```c
net_handle_t        // Opaque node handle (void*)
net_receipt_t       // { shard_id, timestamp }
net_event_t         // { id, id_len, raw, raw_len, insertion_ts, shard_id }
net_poll_result_t   // { events, count, next_id, has_more }
net_stats_t         // { events_ingested, events_dropped, batches_dispatched }
net_error_t         // NET_SUCCESS (0), NET_ERR_* (negative)
```

## Error Codes

| Code | Name | Value |
|------|------|-------|
| `NET_SUCCESS` | Success | 0 |
| `NET_ERR_NULL_POINTER` | Null pointer | -1 |
| `NET_ERR_INVALID_UTF8` | Invalid UTF-8 | -2 |
| `NET_ERR_INVALID_JSON` | Invalid JSON | -3 |
| `NET_ERR_INIT_FAILED` | Init failed | -4 |
| `NET_ERR_INGESTION_FAILED` | Ingestion failed | -5 |
| `NET_ERR_POLL_FAILED` | Poll failed | -6 |
| `NET_ERR_BUFFER_TOO_SMALL` | Buffer too small | -7 |
| `NET_ERR_SHUTTING_DOWN` | Shutting down | -8 |
| `NET_ERR_UNKNOWN` | Unknown error | -99 |

## Thread Safety

All functions are thread-safe. Handles can be shared across threads.

## Subscription Pattern

The C SDK does not manage threads. Use `net_poll_ex` in your own loop:

```c
char* cursor = NULL;
while (running) {
    net_poll_result_t result;
    int rc = net_poll_ex(node, 100, cursor, &result);
    if (rc < 0) break;

    for (size_t i = 0; i < result.count; i++) {
        process(&result.events[i]);
    }

    // Copy cursor before freeing the result.
    free(cursor);
    cursor = result.next_id ? strdup(result.next_id) : NULL;
    net_free_poll_result(&result);
}
free(cursor);
```

## Mesh transport

The header in this directory (`include/net.h`) is intentionally a
**narrow, public, event-bus-only** surface — every symbol declared
here is a stability commitment.

The mesh transport (encrypted peer sessions, channels, NAT
traversal, capability discovery) is implemented in the same
shared library but lives behind a **separate, broader header**:
[`go/net.h`](../../../../go/net.h) at the repo root, which the Go
cgo bindings cargo-include directly. That header is the
de-facto reference for C consumers who want the mesh API. Symbols
are stable in practice but not committed in the same way as
`include/net.h`. An identical-content mirror lives in this
directory at [`net.go.h`](./net.go.h) — it exists so the parity
test (`cr22_c_header_parity_with_rust_neterror`) can `include_str!`
both headers without escaping the crate root, and it's a
convenient drop-in for C consumers who want a copy that ships
with the crate.

**One header per translation unit.** All three files use the same
`#ifndef NET_SDK_H` include guard, so including more than one in
the same `.c` file silently drops the second include — symbols
only declared there will fail to compile. The narrow / broad
split is also **not a strict superset**:

- `include/net.h` declares `net_ingest_raw_ex`, `net_poll_ex`,
  `net_stats_ex` (structured no-JSON paths) that the broader
  mesh header does not.
- `go/net.h` (and its `net.go.h` mirror) declares the entire
  mesh surface (sessions, streams, channels, capabilities, NAT)
  that `include/net.h` does not.

Pick the header that matches the surface your translation unit
actually uses. If a single program needs both — the structured
`_ex` poll path *and* the mesh API — split them across translation
units: one `.c` file includes `include/net.h` and exposes a thin
internal API to the rest of your program, another includes the
mesh header. The resulting object files link against the same
`libnet.{so,dylib,dll}` regardless of which header declared each
symbol.

A mesh node is its own handle (`net_meshnode_t*`), created via
`net_mesh_new` and torn down via `net_mesh_shutdown` — independent
of the bus handle (`net_handle_t`). A single process can hold both
simultaneously regardless of how the headers are included.

The Go bindings (under repo-root `go/`) wrap this surface; their
README has runnable examples for every function family. The
section below is a function inventory — for usage prose, see
[`go/README.md`](../../../../go/README.md).

### Quick start (mesh)

```c
#include "net.go.h"   /* broader header — adjacent to net.h in this directory */

net_meshnode_t* mesh = NULL;
const char* cfg =
    "{\"bind_addr\":\"127.0.0.1:9000\",\"psk_hex\":\"42424242...\"}";
if (net_mesh_new(cfg, &mesh) != 0) return 1;
net_mesh_start(mesh);

/* Announce hardware/software/tag fingerprints. */
net_mesh_announce_capabilities(mesh, "{\"tags\":[\"gpu\",\"prod\"]}");

/* Query the local capability index. Result is a JSON array of
 * node ids; free with net_free_string. */
char* result = NULL;
size_t result_len = 0;
net_mesh_find_nodes(mesh, "{\"require_tags\":[\"gpu\"]}",
                    &result, &result_len);
printf("matches: %.*s\n", (int)result_len, result);
net_free_string(result);

net_mesh_shutdown(mesh);
```

### Mesh function families

| Family | Functions | Purpose |
|--------|-----------|---------|
| Lifecycle | `net_mesh_new`, `net_mesh_shutdown`, `net_mesh_start`, `net_mesh_public_key_hex`, `net_mesh_entity_id` | Create / start / tear down a mesh node. |
| Connections | `net_mesh_connect`, `net_mesh_accept`, `net_mesh_connect_direct` | Establish encrypted peer sessions. |
| Streams | `net_mesh_open_stream`, `net_mesh_send`, `net_mesh_send_with_retry`, `net_mesh_send_blocking`, `net_mesh_stream_stats`, `net_mesh_recv_shard` | Per-peer ordered byte streams. |
| Channels | `net_mesh_register_channel`, `net_mesh_subscribe_channel`, `net_mesh_subscribe_channel_with_token`, `net_mesh_unsubscribe_channel`, `net_mesh_publish` | Topic-based pub/sub over the mesh. |
| Capabilities | `net_mesh_announce_capabilities`, `net_mesh_find_nodes`, `net_mesh_find_nodes_scoped`, `net_mesh_find_best_node`, `net_mesh_find_best_node_scoped` | Capability discovery + scored placement. |
| Predicate evaluation | `net_predicate_evaluate` | Stateless local evaluator (Phase 9c). Returns `1` / `0` for a wire-format predicate against `(tags, metadata)`; same boolean every binding produces. Cross-binding contract pinned by `tests/cross_lang_capability/predicate_eval.json`. |
| Predicate `where:` header | `net_predicate_to_where_header` | Encode a predicate as the canonical `net-where:` request-header pair (Phase 9b). Mirror of the Go SDK's `WhereHeader`; pairs directly with the `*_with_headers` calls in `libnet_rpc` (`net_rpc_call_with_headers` / `net_rpc_call_service_with_headers` / `net_rpc_call_streaming_with_headers` — see the nRPC table below). Wire format pinned by `tests/cross_lang_capability/predicate_nrpc_envelope.json`. |
| Capability validation | `net_validate_capabilities` | Stateless `CapabilitySet` validator (Phase 9a). Wire-format caps in, JSON `ValidationReport` (`errors` + `warnings`) out; same shape every binding produces. Cross-binding contract pinned by `tests/cross_lang_capability/capability_validation.json`. |
| Predicate debug session | `net_predicate_evaluate_with_trace`, `net_predicate_aggregate_debug_report`, `net_predicate_redact_metadata_keys` | Stateless debug helpers (Phase 9d). Single-eval clause trace; corpus-wide per-clause aggregation; host-side label redaction. Cross-binding contracts pinned by `tests/cross_lang_capability/predicate_trace.json`, `predicate_debug_report.json`, `predicate_debug_report_redacted.json`. |
| Daemon capability authoring | `net_compute_set_daemon_caps_dispatcher` | Optional per-daemon `required` / `optional` `CapabilitySet` declaration; without it daemons advertise empty sets (back-compat). See "Daemon capability authoring (Phase 6)" below. |
| Custom placement filters | `net_compute_set_placement_filter_dispatcher`, `net_compute_register_placement_filter`, `net_compute_unregister_placement_filter`, `net_compute_has_placement_filter` | Plug a host-language predicate into `StandardPlacement.custom_filter_id` — substrate calls back per candidate. See "Custom placement-filter callback (Phase 7)" below. |
| NAT traversal | `net_mesh_nat_type`, `net_mesh_reflex_addr`, `net_mesh_peer_nat_type`, `net_mesh_probe_reflex`, `net_mesh_reclassify_nat`, `net_mesh_traversal_stats`, `net_mesh_set_reflex_override`, `net_mesh_clear_reflex_override` | Optional optimization — routed-handshake fallback always works. |

### Scoped capability discovery

`scope:*` reserved tags on a `CapabilitySet` narrow *who finds whom*
at query time. The wire format and forwarders are unchanged —
enforcement is purely query-side.

| Tag form               | Effect                                                          |
|------------------------|-----------------------------------------------------------------|
| _(none)_               | `Global` (default) — visible to every query that doesn't opt out. |
| `scope:subnet-local`   | Visible only under `{"kind":"same_subnet"}` queries.            |
| `scope:tenant:<id>`    | Visible to `{"kind":"tenant","tenant":"<id>"}` queries (and to permissive global queries). |
| `scope:region:<name>`  | Visible to `{"kind":"region","region":"<name>"}` queries.       |

```c
// GPU pool advertised to one tenant only.
net_mesh_announce_capabilities(mesh,
    "{\"tags\":[\"model:llama3-70b\",\"scope:tenant:oem-123\"]}");

// Tenant-scoped query.
char* result = NULL; size_t result_len = 0;
net_mesh_find_nodes_scoped(mesh,
    "{\"require_tags\":[\"model:llama3-70b\"]}",
    "{\"kind\":\"tenant\",\"tenant\":\"oem-123\"}",
    &result, &result_len);
net_free_string(result);

// Scored placement — pick the highest-scoring node within a scope.
uint64_t winner = 0;
int has_match = 0;
net_mesh_find_best_node_scoped(mesh,
    "{\"filter\":{\"require_gpu\":true},\"prefer_more_vram\":1.0}",
    "{\"kind\":\"tenant\",\"tenant\":\"oem-123\"}",
    &winner, &has_match);
if (has_match) printf("placement -> %llu\n", (unsigned long long)winner);
```

`scope.kind` accepts `any` (default) | `global_only` | `same_subnet`
| `tenant` (with `tenant`) | `tenants` (with `tenants`) | `region`
(with `region`) | `regions` (with `regions`). Both snake_case
(`global_only`) and camelCase (`globalOnly`) are accepted so
fixtures round-trip across SDKs. Strictest scope wins —
`scope:subnet-local` dominates tenant/region tags on the same set.

`net_mesh_find_best_node[_scoped]` use an out-param contract: the
return code is 0 on both hit and miss; `*out_has_match` is `1` on
hit (with `*out_node_id` populated) or `0` on miss. The boolean
disambiguates from `node_id == 0`, which is a valid id.

Full design + cross-SDK rationale:
[`docs/SCOPED_CAPABILITIES_PLAN.md`](../docs/SCOPED_CAPABILITIES_PLAN.md).

### Daemon capability authoring (Phase 6)

Optional per-daemon `requiredCapabilities` / `optionalCapabilities` declaration. Wires the substrate's `MeshDaemon::required_capabilities` / `optional_capabilities` (Phase G slice 2) through the C ABI so daemons spawned via the Go-style factory dispatcher can declare what hardware / region / runtime they need before placement decisions run.

Without this dispatcher installed, daemons advertise empty cap sets and `StandardPlacement` treats them as "runs anywhere" — back-compat with pre-Phase-6 consumers. Phase 6 of `docs/plans/CAPABILITY_SYSTEM_SDK_PLAN.md`.

**Lifecycle:**

```c
/* 1. At process init: install the dispatcher ONCE. First-call-wins. */
static int my_daemon_caps(
    uint64_t daemon_id,
    char** out_required_json, size_t* out_required_len,
    char** out_optional_json, size_t* out_optional_len)
{
    /* Look up your daemon by daemon_id. Allocate UTF-8 JSON
     * buffers via C.malloc / libc::malloc — Rust frees them via
     * libc::free after parsing. NULL / zero-length means "no
     * caps declared for this side" (either side may be omitted
     * independently).
     *
     * Wire shape: {"tags": ["hardware.gpu", ...],
     *              "metadata": {"intent": "ml-training", ...}} */
    const char* req = "{\"tags\":[\"hardware.gpu\"],\"metadata\":{}}";
    size_t req_len = strlen(req);
    char* req_buf = (char*)malloc(req_len);
    memcpy(req_buf, req, req_len);
    *out_required_json = req_buf;
    *out_required_len = req_len;

    *out_optional_json = NULL;  /* no optional caps declared */
    *out_optional_len = 0;
    return NET_COMPUTE_OK;
}

net_compute_set_daemon_caps_dispatcher(my_daemon_caps);

/* 2. Subsequent net_compute_spawn / migration reconstruction
 *    queries the dispatcher once per daemon construction; the
 *    bridge stores the parsed sets for the daemon's lifetime. */
```

The dispatcher is invoked at BOTH the initial-spawn path and the migration-target reconstruction path — same caps shape applies on every reincarnation. Idempotent: parsed once, stored on the bridge, never re-fetched on event processing.

`StandardPlacement` consumes the declared caps via the in-tree resource / intent / scope axes plus the hard-required check (artifact's required tags must be a subset of the candidate's tags). Combine this with Phase 7's custom-filter callback for full control over placement decisions.

### Custom placement-filter callback (Phase 7)

Path A (`StandardPlacement` config-driven scoring) is the default; this is the **escape hatch** when the in-tree axes don't capture the placement decision the operator needs. The substrate calls back into the consumer (C / language X) once per candidate when scoring; the consumer returns keep / drop. Phase 7 of `docs/plans/CAPABILITY_SYSTEM_SDK_PLAN.md` — full prose lives in the plan.

Symbols are in `libnet_compute` (separate cdylib), declared in `net.go.h` next to the existing daemon dispatcher.

**Lifecycle:**

```c
/* 1. At process init: install the trampoline ONCE. First-call-wins;
 *    subsequent calls are no-ops. */
static int my_placement_filter(
    const char* filter_id_ptr, size_t filter_id_len,
    uint64_t node_id,
    const char* candidate_json_ptr, size_t candidate_json_len);

net_compute_set_placement_filter_dispatcher(my_placement_filter);

/* 2. After the mesh node is live, register a filter id. The id must
 *    match what the daemon spec / `StandardPlacement.custom_filter_id`
 *    references on the substrate side. The mesh_arc is NOT consumed. */
const char* id = "pf-gpu-must-be-loaded";
int rc = net_compute_register_placement_filter(
    mesh_arc,            /* from net_mesh_arc_clone — caller still owns */
    id, strlen(id));
if (rc != NET_COMPUTE_OK) { /* handle error — see net.go.h for codes */ }

/* 3. Scoring fires the trampoline per candidate. Return:
 *      1 — keep candidate (placement-score 1.0 in Rust)
 *      0 — drop candidate (placement_score returns None)
 *      negative — error; treated as veto. Log the detail yourself.
 *
 *    Wire shape: candidate_json_ptr is a JSON string of length
 *    candidate_json_len:
 *      {"node_id": uint64, "tags": [string], "metadata": {key:value}}
 *    Buffers are owned by Rust for the call's duration; copy if needed.
 */
static int my_placement_filter(
    const char* filter_id_ptr, size_t filter_id_len,
    uint64_t node_id,
    const char* candidate_json_ptr, size_t candidate_json_len)
{
    /* parse candidate_json_ptr with your JSON library of choice
     * (cjson, jansson, RapidJSON, etc.) and apply your predicate */
    return /* 1 | 0 | negative */;
}

/* 4. On shutdown: drop the registration. Existing in-flight scoring
 *    calls already holding the Arc complete normally. */
net_compute_unregister_placement_filter(id, strlen(id));
```

**Counter:** every successful trampoline invocation increments `dataforts_placement_callback_invocations_total{binding}` on the substrate side, where `binding` is set per-language-SDK at register-time. C consumers see `binding="<your-binding-label>"` if you register through a language-specific SDK; raw C consumers calling `net_compute_register_placement_filter` directly inherit the default.

**Same JSON wire shape across all bindings.** The Node TSFN bridge marshals candidates natively; the Python `Py<PyAny>` bridge does the same; the Go cgo bridge uses this exact JSON. Cross-binding compat fixture: `tests/cross_lang_capability/predicate_eval.json` (each binding wraps the predicate as a placement filter and asserts the kept/vetoed verdict matches direct evaluation).

### Mesh types

```c
net_meshnode_t      // Opaque mesh-node handle (separate from net_handle_t).
net_mesh_stream_t   // Opaque per-peer stream handle.
```

### Where to look for full prose

- [`net.go.h`](./net.go.h) (or the repo-root [`go/net.h`](../../../../go/net.h)
  — identical content) — every function has a doc-comment
  with input shapes, error codes, and ownership rules.
- [`go/README.md`](../../../../go/README.md) — runnable
  examples for the full mesh surface (the Go bindings are a thin
  wrapper over `net.h`, so the example translation back to C is
  near-1:1).
- [`net/README.md`](../README.md) — architectural overview, NAT
  traversal design, channel visibility model.

## RedEX storage + cross-node replication

The `libnet` cdylib exposes a C ABI for the `Redex` storage
primitive — `Redex` lifecycle, `RedexFile::open` / `append` /
`tail` / `read_range`, and the cross-node replication operator
surface. Symbols live alongside the mesh / capability / nRPC
families in the same library; no separate `libnet_redex` is shipped.

```c
#include <stdint.h>
#include <stdlib.h>

typedef struct RedexHandle RedexHandle;
typedef struct RedexFileHandle RedexFileHandle;
typedef struct ArcMeshNode ArcMeshNode;

extern RedexHandle* net_redex_new(const char* persistent_dir);
extern void net_redex_free(RedexHandle* h);

typedef struct RedexTailHandle RedexTailHandle;

extern int net_redex_open_file(
    RedexHandle* redex,
    const char* name,
    const char* config_json,
    RedexFileHandle** out_handle
);
extern int net_redex_file_append(
    RedexFileHandle* file,
    const uint8_t* payload,
    size_t payload_len,
    uint64_t* out_seq
);
extern int net_redex_file_read_range(
    RedexFileHandle* file,
    uint64_t start,
    uint64_t end,
    char** out_json,
    size_t* out_len
);
extern int net_redex_file_sync(RedexFileHandle* file);
extern uint64_t net_redex_file_len(RedexFileHandle* file);
extern int net_redex_file_close(RedexFileHandle* file);
extern void net_redex_file_free(RedexFileHandle* file);

/* Tail cursor */
extern int net_redex_file_tail(
    RedexFileHandle* file,
    uint64_t from_seq,
    RedexTailHandle** out_cursor
);
extern int net_redex_tail_next(
    RedexTailHandle* cursor,
    uint32_t timeout_ms,
    char** out_json,
    size_t* out_len
);
extern void net_redex_tail_free(RedexTailHandle* cursor);

/* Replication (require `Arc<MeshNode>` from `net_mesh_arc_clone`) */
extern int net_redex_enable_replication(
    RedexHandle* redex,
    ArcMeshNode* mesh_arc
);
extern uint32_t net_redex_replication_runtime_count(const RedexHandle* redex);
extern char* net_redex_replication_prometheus_text(const RedexHandle* redex);
extern void net_free_string(char* s);
```

**Config wire shape.** `net_redex_open_file` consumes a JSON config
string. The replication opt-in is a nested `replication` field;
omit it for single-node behavior. Numeric fields default to the
core's defaults when omitted (`factor=3`, `heartbeat_ms=500`,
`replication_budget_fraction=0.5`).

```json
{
  "persistent": true,
  "retention_max_events": 1000000,
  "replication": {
    "factor": 3,
    "heartbeat_ms": 500,
    "placement": "standard",
    "on_under_capacity": "withdraw",
    "replication_budget_fraction": 0.5
  }
}
```

`placement` is `"standard"` (default; `PlacementFilter`-driven),
`"pinned"` (requires `pinned_nodes: [u64]`), or
`"colocation_strict"`. `on_under_capacity` is `"withdraw"`
(default) or `"evict_oldest"` (requires `retention_max_*` caps).

**Replication lifecycle.** Call `net_redex_enable_replication` on
each `Redex` that participates, passing an `ArcMeshNode*` obtained
from `net_mesh_arc_clone(mesh_handle)`. The call consumes the
`Arc<MeshNode>` pointer — DO NOT free it again. Idempotent on
repeated calls. After this returns, `net_redex_open_file` with a
populated `replication` field spawns one runtime task per channel;
single-node `open_file` calls keep their existing zero-wire-traffic
behavior.

Failover uses a deterministic nearest-RTT election with NodeId
tie-break — no broadcast, no epoch. Failure-detection window is
`3 × heartbeat_ms` (three-missed hysteresis); the election runs in
the same tick that detects silence (microseconds-scale window).

**Prometheus scrape.**
`net_redex_replication_prometheus_text(redex)` returns a
heap-allocated NUL-terminated string with the seven per-channel
metric shapes (`*_lag_seconds`, `*_sync_bytes_total`,
`*_leader_changes_total`, `*_under_capacity_total`,
`*_skip_ahead_total`, `*_election_thrash_total`,
`*_witness_withdrawals_total`). Returns an empty string (still
heap-allocated, still NUL-terminated) when replication isn't
enabled — pipe straight into an HTTP scrape body. Free with
`net_free_string`. Returns NULL only on a NULL `redex` handle.

Error codes: `0` = success, `-1` = NULL pointer, `-103`
(`NET_ERR_REDEX`) = generic Redex / replication failure (invalid
config, channel-name validation, `replication: {...}` set without
`enable_replication` having been called, etc.).

## Dataforts (greedy cache, gravity)

Dataforts is the compositional data plane on top of RedEX + the
capability index. The C FFI surfaces the two enable / disable pairs
needed to wire Phase 1 (greedy) and Phase 4 (gravity) into a Redex
handle; blob refs (Phase 3) and read-your-writes (Phase 5) — for
both the v0.15 external-hook adapter shape AND the v0.2 substrate-
owned `MeshBlobAdapter` — are higher-level than this FFI layer and
are exposed through the Python / Node / Go bindings on top (the
Python binding has a first slice of the v0.2 `MeshBlobAdapter`
surface; Node + Go land in follow-up slices). The v0.2 gravity
extensions (`BlobHeatRegistry`, `heat:blob:<hex>=<rate>` emission,
`drive_blob_migration_tick` consumer) live entirely above the FFI
boundary too.

The cdylib must be built with the `dataforts` Cargo feature for
these symbols to be live. Without the feature, the symbols still
link (unconditional `extern "C"` stubs) and return
`NET_ERR_FEATURE_NOT_BUILT` so a Go / cgo program against an older
build doesn't fail at module load.

```c
typedef struct ArcMeshNode ArcMeshNode;

/* Enable Phase 1 — greedy-LRU caching against `redex`, observing
 * inbound packets through `mesh_arc`'s shard dispatch. `config_json`
 * follows the GreedyConfig shape (scopes, per-channel + total caps,
 * bandwidth-budget fraction, intent / colocation policies). */
extern int net_redex_enable_greedy_dataforts(
    RedexHandle* redex,
    ArcMeshNode* mesh_arc,
    const char* config_json);
extern int net_redex_disable_greedy_dataforts(RedexHandle* redex);

/* Enable Phase 4 — data gravity. Requires greedy to be enabled
 * first; the runtime drives a periodic tick that decays the per-
 * chain heat counters and emits `heat:<hex>=<rate>` tags onto the
 * chain's existing capability announcement. */
extern int net_redex_enable_gravity_for_greedy(
    RedexHandle* redex,
    ArcMeshNode* mesh_arc,
    const char* config_json);
extern int net_redex_disable_gravity_for_greedy(RedexHandle* redex);

/* Diagnostics. */
extern uint32_t net_redex_greedy_cached_channel_count(const RedexHandle* redex);
extern char*    net_redex_greedy_prometheus_text(const RedexHandle* redex);
```

```c
RedexHandle* redex = net_redex_new();
ArcMeshNode* mesh_arc = /* obtained via the mesh enable path */;

const char* greedy_cfg =
    "{\"scopes\":[\"region:us\"],"
    " \"per_channel_cap_bytes\":67108864,"
    " \"total_cap_bytes\":1073741824}";
int rc = net_redex_enable_greedy_dataforts(redex, mesh_arc, greedy_cfg);
if (rc != 0) { /* NET_ERR_FEATURE_NOT_BUILT / NET_ERR_REDEX */ }

const char* gravity_cfg =
    "{\"emit_threshold_ratio\":1.5,"
    " \"decay_half_life_secs\":300}";
rc = net_redex_enable_gravity_for_greedy(redex, mesh_arc, gravity_cfg);

uint32_t cached = net_redex_greedy_cached_channel_count(redex);
char* metrics = net_redex_greedy_prometheus_text(redex);
/* ... pipe `metrics` into an HTTP scrape body ... */
net_free_string(metrics);
```

The canonical `channel_hash` returned by `net_channel_hash` is
`uint32_t` (substrate-wide ACL / config / storage / RYW key); the
per-packet wire `NetHeader::channel_hash` stays `uint16_t` (fast-
path filter hint). The `PermissionToken` wire form is 161 bytes
(channel-hash field 2 → 4 bytes during the canonical widening).

Error codes: `0` = success, `NET_ERR_NULL_POINTER` = NULL handle,
`NET_ERR_FEATURE_NOT_BUILT` = cdylib built without the `dataforts`
feature, `NET_ERR_REDEX` = generic Redex / config failure.

### Dataforts blob storage (`MeshBlobAdapter` + v0.3 overflow)

Substrate-owned blob CAS. Requires the cdylib to be built with
`dataforts,netdb,redex-disk` features enabled.

```c
typedef struct MeshBlobAdapterHandle MeshBlobAdapterHandle;

// Construct. `redex` is a `*RedexHandle` from `net_redex_new`.
// `persistent` 0/1 toggles disk-backed chunk files.
// `overflow_json` is optional null-terminated JSON for the v0.3
// active-overflow config; NULL or empty string keeps overflow off.
//
// Returns NULL on error (check feature gates + JSON validity).
extern MeshBlobAdapterHandle* net_mesh_blob_adapter_new(
    RedexHandle* redex,
    const char* adapter_id,
    int persistent,
    const char* overflow_json
);
extern void net_mesh_blob_adapter_free(MeshBlobAdapterHandle* handle);

// CRUD. `blob_ref_bytes` is a previously-encoded `BlobRef` wire
// payload. Substrate verifies BLAKE3 on store; fetch returns a
// caller-owned buffer (free with `net_blob_free_buffer`).
extern int net_mesh_blob_adapter_store(
    const MeshBlobAdapterHandle* handle,
    const uint8_t* blob_ref_bytes,
    size_t blob_ref_len,
    const uint8_t* data,
    size_t data_len
);
extern int net_mesh_blob_adapter_fetch(
    const MeshBlobAdapterHandle* handle,
    const uint8_t* blob_ref_bytes,
    size_t blob_ref_len,
    uint8_t** out_data,
    size_t* out_len
);
extern int net_mesh_blob_adapter_exists(
    const MeshBlobAdapterHandle* handle,
    const uint8_t* blob_ref_bytes,
    size_t blob_ref_len,
    int* out_exists
);

// Prometheus text body — includes v0.2 counters + v0.3 overflow
// counters. Free returned string with `net_free_string`.
extern char* net_mesh_blob_adapter_prometheus_text(
    const MeshBlobAdapterHandle* handle
);

// v0.3 active-overflow control surface.
extern int net_mesh_blob_adapter_overflow_enabled(
    const MeshBlobAdapterHandle* handle
);  // returns 0 / 1, or negative NET_ERR_*
extern int net_mesh_blob_adapter_overflow_active(
    const MeshBlobAdapterHandle* handle
);
extern char* net_mesh_blob_adapter_overflow_config(
    const MeshBlobAdapterHandle* handle
);  // JSON; free with net_free_string
extern int net_mesh_blob_adapter_set_overflow_enabled(
    const MeshBlobAdapterHandle* handle,
    int enabled
);
extern int net_mesh_blob_adapter_set_overflow_config(
    const MeshBlobAdapterHandle* handle,
    const char* config_json
);
```

Overflow config JSON shape (every key optional except
`enabled`):

```json
{
  "enabled": true,
  "high_water_ratio": 0.85,
  "low_water_ratio": 0.70,
  "max_pushes_per_tick": 16,
  "scope": "mesh",
  "tick_interval_ms": 30000
}
```

`scope` values: `"node"` / `"zone"` / `"region"` / `"mesh"`.
Malformed JSON / unknown scope token → `NET_ERR_*` from the
`InvalidJson` family.

Blob-specific error codes are in the `-110..` band — see
[`bindings/go/net/blob.go`](../bindings/go/net/blob.go) for the
Go wrapper that consumes this surface; the same `extern`
declarations work from any C / C++ consumer.

## nRPC (request / response over the mesh)

nRPC is the request/response convention layer (deadlines,
queue-group fan-out, response streaming, end-to-end cancellation)
riding on top of the pub/sub mesh. Lives in a separate cdylib at
[`bindings/go/rpc-ffi`](../bindings/go/rpc-ffi) — the Go binding
consumes it, but the ABI is callable from any C-ABI consumer.

**Library:** `libnet_rpc` (cdylib + staticlib). Build:

```bash
cargo build --release -p net-rpc-ffi
```

**Header:** [`net_rpc.h`](./net_rpc.h) — the canonical C SDK
header for nRPC. Drop-in for C / C++ / Zig / Swift / Java JNI /
etc.; identical declarations to the cgo block in
`bindings/go/net/mesh_rpc.go`. Same one-header-per-translation-
unit discipline as `net.h` / `net.go.h` (different `#ifndef`
guard — `NET_RPC_H` — so combining with the mesh headers in one
TU is fine).

```c
#include "net_rpc.h"
gcc -o app app.c -L target/release -lnet_rpc -lpthread -ldl -lm
```

**ABI version:** consumers SHOULD call `net_rpc_abi_version() ->
uint32_t` at process init and refuse to load on mismatch. Version
`0x0001` covers Phase B5 (lifecycle + unary call + serve +
service discovery) plus B6 (streaming + ABI version stamp).

**Entry-point families** (full per-function doc-comments in
`net_rpc.h`):

| Family | Functions |
|---|---|
| Lifecycle | `net_rpc_abi_version`, `net_rpc_new`, `net_rpc_free`, `net_rpc_id` |
| Free helpers | `net_rpc_free_cstring`, `net_rpc_response_free`, `net_rpc_find_service_nodes_free` |
| Cancellation | `net_rpc_reserve_cancel_token`, `net_rpc_cancel_call` |
| Handler dispatcher | `net_rpc_set_handler_dispatcher`, `net_rpc_reserve_handler_id`, `RpcHandlerFn` typedef |
| Unary calls | `net_rpc_call`, `net_rpc_call_service` |
| Header-bearing calls | `net_rpc_call_with_headers`, `net_rpc_call_service_with_headers`, `net_rpc_call_streaming_with_headers` (Phase 9b end-to-end — accept a `net_rpc_header_t[]`; pair with `net_predicate_to_where_header`) |
| Service discovery | `net_rpc_find_service_nodes` |
| Serve | `net_rpc_serve`, `net_rpc_serve_handle_id`, `net_rpc_serve_handle_close`, `net_rpc_serve_handle_free` |
| Streaming | `net_rpc_call_streaming`, `net_rpc_stream_next`, `net_rpc_stream_grant`, `net_rpc_stream_call_id`, `net_rpc_stream_close`, `net_rpc_stream_free` |

Ownership: every `uint8_t*` / `char*` / `uint64_t*` returned
out-of-band is freed via the matching
`net_rpc_response_free` / `net_rpc_free_cstring` /
`net_rpc_find_service_nodes_free`.

**Error codes (`int` return):**

| Code | Constant                       | Meaning                                              |
| ---- | ------------------------------ | ---------------------------------------------------- |
|  `0` | `NET_RPC_OK`                   | Success.                                             |
| `-1` | `NET_RPC_ERR_NULL`             | NULL pointer where a handle was expected.            |
| `-2` | `NET_RPC_ERR_CALL_FAILED`      | Generic — structured detail in `**out_err` CString.  |
| `-3` | `NET_RPC_ERR_ALREADY_SERVING`  | `serve` rejected — handler already registered.       |
| `-4` | `NET_RPC_ERR_NO_DISPATCHER`    | `set_handler_dispatcher` was never called.           |
| `-5` | `NET_RPC_ERR_INVALID_UTF8`     | Non-UTF-8 bytes where a string was expected.         |
| `-6` | `NET_RPC_ERR_STREAM_DONE`      | Stream produced its terminal item; release handle.   |

**Structured error format:** `format_rpc_error` emits
`<kind>: <detail>` (no `nrpc:` prefix; consumers add it). Kinds:
`no_route`, `timeout`, `server_error` (`status=0xNNNN`),
`transport`, `codec_encode`, `codec_decode`. Application-defined
status codes are in `0x8000..=0xFFFF`; the SDK stables
`NRPC_TYPED_BAD_REQUEST = 0x8000` and
`NRPC_TYPED_HANDLER_ERROR = 0x8001` for typed-handler decode /
runtime errors.

For the canonical cross-binding contract spec — including the
`cross_lang_echo_sum` service used by every binding's wire-format
compat test — see [`net/README.md#nrpc`](../README.md#nrpc).

## MeshDB (federated query layer)

MeshDB is the query layer above the capability-query primitives +
CortEX folds. The Python / Node / Go SDKs wrap this same FFI;
non-Go C consumers get an in-tree header at
[`net_meshdb.h`](./net_meshdb.h), backed by the separate
`libnet_meshdb.{so,dylib,dll}` cdylib built from
[`bindings/go/meshdb-ffi`](../bindings/go/meshdb-ffi).

**Library:** `libnet_meshdb`. Build:

```bash
cargo build --release -p net-meshdb-ffi
```

**Header:** [`net_meshdb.h`](./net_meshdb.h) — independent header
guard (`NET_MESHDB_H`); composes cleanly alongside the mesh /
event-bus / nRPC headers in the same translation unit. The Go
binding's `bindings/go/net/meshdb.go` cgo include block has been
the de-facto contract for non-Go consumers since the MeshDB SDK
landed; this header is the canonical drop-in.

```c
#include "net_meshdb.h"
gcc -o app app.c -L target/release -lnet_meshdb -lpthread -ldl -lm
```

### Quick start

```c
#include "net_meshdb.h"
#include <inttypes.h>
#include <stdio.h>
#include <string.h>

int main(void) {
    MeshDbReader* reader = net_meshdb_reader_new();
    net_meshdb_reader_append(reader, 0xAB, 1, (uint8_t*)"hello", 5);
    net_meshdb_reader_append(reader, 0xAB, 2, (uint8_t*)"world", 5);

    MeshDbRunner* runner = net_meshdb_runner_new(reader);
    MeshDbQuery* q = net_meshdb_query_latest(0xAB);
    MeshDbIter* it = net_meshdb_runner_execute(runner, q);

    uint64_t origin = 0, seq = 0;
    uint8_t* payload = NULL;
    size_t payload_len = 0;
    while (net_meshdb_iter_next(it, &origin, &seq, &payload, &payload_len)
           == NET_MESHDB_OK) {
        printf("origin=0x%" PRIx64 " seq=%" PRIu64 " payload=%.*s\n",
               origin, seq, (int)payload_len, (const char*)payload);
        net_meshdb_payload_free(payload, payload_len);
    }

    net_meshdb_iter_free(it);
    net_meshdb_query_free(q);
    net_meshdb_runner_free(runner);
    net_meshdb_reader_free(reader);
    return 0;
}
```

Full runnable example: [`examples/meshdb.c`](../examples/meshdb.c).

### Operator families

| Family | Functions |
|---|---|
| Reader | `net_meshdb_reader_new`, `net_meshdb_reader_free`, `net_meshdb_reader_append` |
| Atomic queries | `net_meshdb_query_at`, `net_meshdb_query_between`, `net_meshdb_query_latest`, `net_meshdb_query_lineage_emit` |
| Composite queries | `net_meshdb_query_window`, `net_meshdb_query_count`, `net_meshdb_query_numeric_agg`, `net_meshdb_query_percentile`, `net_meshdb_query_join`, `net_meshdb_query_filter_json` |
| Query lifecycle | `net_meshdb_query_free` |
| Runner | `net_meshdb_runner_new`, `net_meshdb_runner_new_cached`, `net_meshdb_runner_free`, `net_meshdb_runner_execute`, `net_meshdb_runner_execute_with` |
| Iterator | `net_meshdb_iter_next`, `net_meshdb_payload_free`, `net_meshdb_iter_free` |
| Sentinel decoder | `net_meshdb_decode_payload_json`, `net_meshdb_free_string` |
| Last error | `net_meshdb_last_error_message`, `net_meshdb_last_error_kind`, `net_meshdb_clear_last_error` |

### Error codes

| Code | Constant                       | Meaning                                              |
| ---- | ------------------------------ | ---------------------------------------------------- |
|  `0` | `NET_MESHDB_OK`                | Success.                                             |
|  `1` | `NET_MESHDB_END`               | Iterator drained; no more rows.                      |
|  `2` | `NET_MESHDB_INVALID_ARG`       | NULL handle or out-of-range input.                   |
|  `3` | `NET_MESHDB_RUNTIME_ERR`       | Planner / executor failure.                          |

Structured detail for the most recent failure is available on a
per-thread basis via `net_meshdb_last_error_message()` (a
human-readable detail string) and `net_meshdb_last_error_kind()`
(one of the `MeshError` variant tags such as `"planner_error"`,
`"executor_error"`, `"query_cancelled"`,
`"historical_range_unavailable"`, `"join_memory_exceeded"`,
`"runtime_panic"`, `"invalid_arg"`, …). Both return NULL when no
error has been recorded on the calling thread. Returned pointers
are valid until the next FFI call on the same thread touches the
thread-local; callers must NOT free them. Use
`net_meshdb_clear_last_error()` to reset state explicitly.

Panics from user-controlled operators (aggregate division by
zero, OOM inside a hash-join, etc.) are trapped via
`catch_unwind` on every entry point; instead of unwinding across
the C ABI (UB), the function returns its declared failure value
and the last-error pair is populated with kind `"runtime_panic"`.

### Cache options

`net_meshdb_runner_execute_with` accepts the Phase F cache
discriminator:

| Constant                       | Meaning                                              |
| ------------------------------ | ---------------------------------------------------- |
| `NET_MESHDB_CACHE_PERMANENT`   | Cache until LRU eviction (immutable results only).   |
| `NET_MESHDB_CACHE_TIME_BOUND`  | TTL expiry; `cache_ttl_secs` consulted (default 5.0). |

### Sentinel-envelope decoder

Atomic-operator rows (At / Between / Latest / LineageEmit) carry
raw event bytes. Composite-operator rows (Count / Sum / Avg / Min
/ Max / DistinctCount / Percentile / Join / Window) carry a
postcard-encoded sentinel envelope; pass the payload through
`net_meshdb_decode_payload_json` to get a tagged JSON string —
NULL means "not a sentinel" (i.e. it's a plain row body). Wire
JSON shapes:

```json
{"kind":"aggregate","group":{...|null},
 "value":{"kind":"count","value":N,"count":N}}

{"kind":"joined","left":{...|null},"right":{...|null}}

{"kind":"window","start":N,"end":N,"rows":[{...},...]}
```

Each nested row is `{"origin":N,"seq":N,"payload":[<byte>,...]}`
— JSON array of byte integers, no base64.

### Federated executor

Out of scope for the current C SDK surface — the FFI ships a
local in-memory executor only. The Phase B federated-executor
path will surface when the wire-subprotocol dispatch lands; until
then, cross-node fan-out is reachable only through the Python /
Node / Go SDK layers.

## Behavior changes in v0.10 (FFI)

### Panics no longer unwind across the FFI boundary

The cdylib is built with `panic = "abort"` and every `extern "C"`
body is wrapped in `catch_unwind`. A Rust panic that was previously
*partially* completing the call before unwinding (and silently
corrupting your process across the cgo / N-API / cffi boundary) now
either returns a defined error code or aborts the process cleanly.
Callers that depended on partial-completion no longer get it.

### Length validation on every wide-input entry point

Every FFI entry point that constructs a slice from a caller-supplied
`(ptr, len)` now rejects `len > isize::MAX as usize` (i.e. `SSIZE_MAX`
on 64-bit; `INT_MAX` on 32-bit) before calling `slice::from_raw_parts`.
A C caller passing a stray sign-extended `-1` previously triggered
immediate UB before any other validation ran — now it returns an
error.

Functions covered: `net_ingest`, `net_ingest_raw`, `net_ingest_raw_batch`,
`net_ingest_raw_ex`, `net_mesh_publish`, `net_redex_file_append`,
`net_netdb_open_from_snapshot`, `net_mesh_subscribe_channel_with_token`,
`net_identity_sign`, `net_identity_install_token`, `net_parse_token`,
`net_verify_token`, `net_token_is_expired`, `net_delegate_token`,
`net_blob_publish`, `net_blob_resolve`, `net_mesh_blob_adapter_store`,
`net_mesh_blob_adapter_fetch`, and `net_mesh_blob_adapter_exists`.

### Alignment checks on handle dereferences

Every FFI handle accessor now checks `is_aligned_to::<HandleType>()`
before dereferencing. A misaligned `*mut` returned from a wrapper
that allocated through a non-Rust allocator returns a defined error
instead of UB.

### `net_free_poll_result` is idempotent

After freeing, the function now nulls `result->events`,
`result->next_id`, and zeros `result->count` / `result->has_more`.
Subsequent calls on the same struct are no-ops; passing `NULL` is
also a no-op. Callers that ran their own field-nulling defensively
can drop it.

### `net_ingest_raw_batch` surfaces dropped indices

The function takes two new optional out-params:

```c
int net_ingest_raw_batch(
    net_handle_t handle,
    const char* const* jsons,
    const size_t* lens,
    size_t count,
    size_t* out_failed_indices,   /* nullable; up to `count` u32 indices */
    size_t* out_failed_len        /* nullable; written to with the count */
);
```

A null entry pointer or an invalid-UTF-8 entry no longer silently
disappears from the accepted count — the index is appended to
`out_failed_indices`. Callers passing `NULL` for both new params
keep the old "count returned" semantics, but should treat
`returned_count < count` as "drops happened, you don't know which."

### `net_poll` rejects undersized buffers up front

Buffers below `MIN_RESPONSE_BUFFER` (256 bytes) are now rejected
with `NET_ERR_BUFFER_TOO_SMALL` *before* the cursor is advanced.
Pre-fix the cursor was advanced first and then the response was
dropped — every event in the failed serialization was silently
lost. Sizing rule: `4 KB` is comfortable; the structured
`net_poll_ex` path is unaffected.

### Strict config parsing

`parse_config_json` (the JSON dialect every FFI `net_init`-shaped
call accepts) now errors instead of silently falling back:

- Unknown `backpressure_mode` strings (typos like `"DropOldset"`,
  retired names like `"FailProduce"`) return
  `NET_ERR_INVALID_JSON`. Pre-fix they silently selected
  `"drop_newest"` and you got a different durability profile with
  no signal.
- Zero values for `retention_max_events`, `retention_max_bytes`,
  `retention_max_age_ms` are rejected (they previously meant
  "evict everything immediately on first append" — almost always
  unintended). Use `null` or omit the field for "no limit."
- Zero values for `heartbeat_interval_ms`, `session_timeout_ms`
  (Net adapter), and mesh `heartbeat_ms` are rejected. A 0 ms
  heartbeat busy-loops a CPU.
- A new `Sample { rate }` arm is accepted on `backpressure_mode`
  with `rate` validation.

### `net_mesh_find_*` modality strings strictly validated

`parse_modality_cap` (called from `net_mesh_announce_capabilities`,
`net_mesh_find_nodes[_scoped]`, `net_mesh_find_best_node[_scoped]`)
now returns `NET_ERR_CHANNEL` on unknown modality strings instead of
silently falling back to `Modality::Text`. A typo in
`require_modalities` previously returned wrong nodes with no error.

### `net_generate_keypair` / `net_free_string` always linkable

Both symbols are now exported in builds without the `net` feature
(via no-op stubs) so consumers linking against a `net`-less cdylib
no longer hit load-time missing-symbol errors despite the header
promising the symbol.

### `MigrationError::NoTargetAvailable`

Auto-placement (the wrappers that take a capability filter and
pick a target node) returns the typed `NoTargetAvailable` variant
when the scheduler finds no candidate, instead of fabricating
`TargetUnavailable(0)` (which surfaced "target node 0x0 unavailable"
to operators). C consumers that string-matched on the rendered
error need to add the new arm.

### Concurrent `net_shutdown` is serialized

A second/third caller of `net_shutdown` no longer returns `Success`
while the first caller is still inside `runtime.block_on(bus.shutdown())`.
The shutdown is now atomic across concurrent callers; only one
caller observes the actual shutdown result, the others see a
defined "already shutting down" return.

## License

Apache-2.0