README
¶
Horizon
A Go-shaped DSL for writing kernel capabilities — verifier-aware checks, readable BPF C, typed Go bindings, and Continuum capability manifests, all in one workbench.
Horizon is not a Go compiler. It is a small Go-shaped DSL for writing verifier-friendly eBPF programs that lower to readable BPF C.
What Horizon solves
Verifier errors are cryptic. Horizon emits source maps with every
build. hzn diagnose remaps clang and verifier logs back through those
source maps and adds Horizon-specific remediation hints for nil checks,
ringbuf lifetimes, bounded loops, helper availability, and stack usage.
Common verifier messages route through a vendored catalog with stable
HZN31xx codes so editors and CI see consistent remediation text.
Resource lifetimes are implicit. Ringbuf reservations, map lookups,
and packet headers are first-class resource types with tracked states
(maybe_nil → live → consumed). Missing nil checks, double submits,
writes after submit, and live-on-return are rejected before clang runs.
Tracking spans intra-function aliasing, &&-chained nil checks, bounded
loop iterations, and user helpers that take resource handles as
parameters.
Build pipelines are scattered. hzn workbench produces readable BPF
C, source maps, typed Go bindings, a capability manifest, diagnostics, and
a report with provenance — in one command. hzn build adds clang
compilation.
Object files do not explain their power. Every program declares a
capability with explicit danger metadata. Danger is recorded as an axis
triple (mode × scope × reversibility); legacy flat words such as
observe, mutate, drop, block, and privileged map deterministically
onto the axes. The generated manifest carries the contract, including
helper side-effects per program.
The Workbench
hzn workbench is the authoring path. One command, every artifact:
hzn workbench ./examples/execwatch -o dist
produces:
- Readable BPF C (
exec.bpf.c). - Source map (
exec.hznmap.json) for clang and verifier diagnostics. - Typed Go bindings (
bindings.go) wrappingcilium/ebpfwith typed map, ringbuf, and attach helpers. - Capability manifest (
exec.cap.json) — see "Capability manifests" below. - Diagnostics (
exec.diagnostics.json) including source-line context and Horizon-specific remediation hints. - Report (
exec.report.json) with artifact hashes, sizes, generator provenance, and a summary of programs, maps, capability danger levels, and minimum kernel requirements.
Use -compile to also run clang and emit .bpf.o. Use -preflight to
run host readiness checks against the generated manifest.
A program end-to-end
The same program, expressed four ways.
Author it (examples/execwatch/exec.hzn):
package probes
import bpf "m31labs.dev/horizon/runtime/kernel"
type ExecEvent struct {
ts_ns u64
pid u32
ppid u32
uid u32
comm [16]u8
}
map ExecEvents ringbuf[ExecEvent]
capability ExecObserve danger observe = "kernel.process.exec.observe"
@capability(ExecObserve)
@tracepoint("sched:sched_process_exec")
func OnExec(ctx tracepoint.Exec) i32 {
event := ExecEvents.reserve()
if event == nil {
return 0
}
event.ts_ns = bpf.ktime_get_ns()
event.pid = bpf.current_pid()
event.ppid = bpf.current_ppid()
event.uid = bpf.current_uid()
bpf.current_comm(&event.comm)
ExecEvents.submit(event)
return 0
}
Generated BPF C (dist/exec.bpf.c, excerpt):
/* Code generated by hzn; DO NOT EDIT. */
/* ... */
struct hzn_type_ExecEvent {
__u64 ts_ns;
__u32 pid;
__u32 ppid;
__u32 uid;
__u8 comm[16];
};
_Static_assert(sizeof(struct hzn_type_ExecEvent) == 40, "horizon: struct ExecEvent size mismatch");
/* ... */
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 1 << 24);
} ExecEvents SEC(".maps");
static __always_inline struct hzn_type_ExecEvent *ExecEvents_reserve(void) {
return bpf_ringbuf_reserve(&ExecEvents, sizeof(struct hzn_type_ExecEvent), 0);
}
static __always_inline void ExecEvents_submit(struct hzn_type_ExecEvent *value) {
bpf_ringbuf_submit(value, 0);
}
SEC("tracepoint/sched/sched_process_exec")
int OnExec(struct trace_event_raw_sched_process_exec *ctx) {
(void)ctx;
struct hzn_type_ExecEvent *event = ExecEvents_reserve();
if (event == 0) {
return 0;
}
event->ts_ns = hzn_ktime_get_ns();
event->pid = hzn_current_pid();
event->ppid = hzn_current_ppid();
event->uid = hzn_current_uid();
hzn_current_comm(&event->comm, sizeof(event->comm));
ExecEvents_submit(event);
return 0;
}
Generated Go binding (dist/exec.bindings.go, excerpt):
// Code generated by hzn; DO NOT EDIT.
package bindings
// ...
type ExecEvent struct {
TsNs uint64
Pid uint32
Ppid uint32
Uid uint32
Comm [16]uint8
}
type Objects struct {
ExecEvents *ebpf.Map `ebpf:"ExecEvents"`
OnExec *ebpf.Program `ebpf:"OnExec"`
}
func LoadObjects(path string) (*Objects, error) {
return LoadObjectsWithOptions(path, LoadOptions{RemoveMemlock: true})
}
func LoadObjectsWithOptions(path string, opts LoadOptions) (*Objects, error) { /* ... */ }
func (o *Objects) ReadExecEvents(ctx context.Context, handle func(ExecEvent) error) error { /* ... */ }
func (o *Objects) AttachOnExec() (link.Link, error) {
if o == nil || o.OnExec == nil {
return nil, fmt.Errorf("OnExec program is not loaded")
}
return link.Tracepoint("sched", "sched_process_exec", o.OnExec, nil)
}
Capability manifest (dist/exec.cap.json, excerpt):
{
"schema": "m31labs.dev/horizon/capability/v1",
"package": "probes",
"programs": [
{
"name": "OnExec",
"kind": "tracepoint",
"attach": "sched:sched_process_exec",
"section": "tracepoint/sched:sched_process_exec",
"capabilities": [
"kernel.process.exec.observe"
]
}
],
"capabilities": [
{
"name": "kernel.process.exec.observe",
"kind": "source",
"danger": {
"mode": "observe",
"reversibility": "none",
"scope": "event"
},
"program": "OnExec",
"section": "tracepoint/sched:sched_process_exec",
"emits": "ExecEvent",
"maps": {
"read": [],
"write": [],
"events": ["ExecEvents"]
}
}
]
}
Install
Until the m31labs.dev/horizon vanity import metadata is live, install the CLI
from a clone:
git clone https://github.com/M31-Labs/horizon.git
cd horizon
go install ./cmd/hzn
After the vanity import path is serving Go metadata, tagged releases will be installable with:
go install m31labs.dev/horizon/cmd/hzn@latest
Language tour
.hzn -> gotreesitter parser -> AST -> BPF IR -> validation -> C -> clang -> .bpf.o -> bindings + capabilities
What Horizon keeps small
It keeps the kernel-side language deliberately small:
- tracepoint programs
- kprobe and kretprobe programs
- uprobe and uretprobe programs
- fentry and fexit programs
- raw tracepoint programs
- XDP programs
- TC classifier programs
- cgroup connect programs
- LSM programs
- sockops programs
- struct_ops programs
- typed structs and fixed arrays
- boolean literals and typed boolean expressions
- package-scoped declarations across multiple
.hznfiles - multi-file packages and cross-package imports resolved under
vendor/ - source-level scalar type aliases such as
type Port = u16 - grouped type declarations for related aliases and records
- integer constants with optional scalar widths
- grouped constants for related limits, flags, and action values
- signed integer literals such as
-1for signed scalar fields and helpers - scoped
if name := expr; conditiondeclarations for short-lived nullable resources - ringbuf event output
- hash, array, per-CPU, and LRU maps
- explicit
@max_entries(...)map sizing - nil-checked map lookups
- bounded counted loops
- expression
switchstatements with explicit non-fallthrough cases - explicit integer scalar conversions such as
u64(pid) - explicit local variable declarations such as
var pid u32 = bpf.current_pid() - explicitly typed constants such as
const Port u16 = 443 - signed constants such as
const Errno i32 = -1 - typed enum value groups for named integer actions and flags
- named capability aliases with explicit danger metadata such as
capability ExecObserve danger observe = "kernel.process.exec.observe" - scalar user helper functions that lower to
static __always_inlineC - compiler-known kernel helpers
- typed kprobe argument and kretprobe return helpers
- readable generated BPF C
- generated BPF C and Go bindings marked as generated artifacts
- embedded gotreesitter highlight, locals, and symbol queries for editor integrations
- source maps with declaration and function/section context for diagnostics
- typed Go bindings and Continuum capability manifests
Programs and capabilities
package probes
import bpf "m31labs.dev/horizon/runtime/kernel"
type ExecEvent struct {
ts_ns u64
pid u32
ppid u32
uid u32
comm [16]u8
}
map ExecEvents ringbuf[ExecEvent]
capability ExecObserve danger observe = "kernel.process.exec.observe"
@capability(ExecObserve)
@tracepoint("sched:sched_process_exec")
func OnExec(ctx tracepoint.Exec) i32 {
event := ExecEvents.reserve()
if event == nil {
return 0
}
event.ts_ns = bpf.ktime_get_ns()
event.pid = bpf.current_pid()
event.ppid = bpf.current_ppid()
event.uid = bpf.current_uid()
bpf.current_comm(&event.comm)
ExecEvents.submit(event)
return 0
}
Stateful programs can use typed maps. Lookup results are nullable and must be checked before dereference.
Capability strings can be named once at package scope with explicit danger
metadata and referenced from entrypoint attributes. This keeps the source
readable while preserving the manifest's stable Continuum capability name.
When explicit danger is omitted, Horizon infers it from the program body; when
it is present, the manifest uses the more dangerous of the declaration and the
program's inferred behavior. If a capability name ends in a known danger word,
such as .block or .drop, that suffix is also treated as a manifest floor.
capability ExecObserve danger observe = "kernel.process.exec.observe"
@capability(ExecObserve)
@tracepoint("sched:sched_process_exec")
func OnExec(ctx tracepoint.Exec) i32 {
return 0
}
Enums
Use enum for named integer value sets. Enum values are explicit, typed
constants; Horizon does not infer iota-like values or widen them through C's
usual implicit conversions.
enum Verdict i32 {
VerdictPass = 0
VerdictDrop = 1
}
capability ExecObserve danger observe = "kernel.process.exec.observe"
@capability(ExecObserve)
@tracepoint("sched:sched_process_exec")
func OnExec(ctx tracepoint.Exec) i32 {
if bpf.current_pid() == 0 {
return VerdictPass
}
return VerdictDrop
}
Types and constants
Use source-level type aliases for domain names over scalar and bool widths. Aliases are authoring names only: generated C uses the underlying BPF scalar type, and aliases cannot hide pointers, resource handles, context types, packet headers, maps, or structs.
Aliases and related records can be grouped when that makes the domain model easier to scan.
type (
Pid = u32
Port = u16
SocketEvent struct {
pid Pid
port Port
}
)
Related constants can be grouped without changing their C-facing type. This is useful for map sizes, packet ports, bit masks, and other named verifier-visible limits.
const (
CountEntries u32 = 4096
HTTPS Port = 443
BucketMask u32 = 0x0f
)
Var declarations
Use var when a local needs an explicit C-facing type. := remains the
preferred shape for nullable resources because Horizon tracks lookup, reserve,
and packet-header ownership from the helper call.
capability ExecObserve danger observe = "kernel.process.exec.observe"
@capability(ExecObserve)
@tracepoint("sched:sched_process_exec")
func OnExec(ctx tracepoint.Exec) i32 {
var pid u32 = bpf.current_pid()
var bucket u32 = pid & 0x0f
return i32(bucket)
}
Switch
Use switch for scalar dispatch that should become readable C switch.
Cases do not fall through, and case values must be compile-time constants:
integer or bool literals, package constants, enum values, or compiler-known
action/protocol constants.
capability WebDrop danger drop = "kernel.network.xdp.drop"
@capability(WebDrop)
@xdp
func DropWeb(ctx xdp.Context) i32 {
tcp := xdp.tcp(ctx)
if tcp == nil {
return xdp.Pass
}
switch xdp.ntohs(tcp.dst_port) {
case 80, 443:
return xdp.Drop
default:
return xdp.Pass
}
}
Maps
const FirstSeen u32 = 1
type Count struct {
seen u32
}
map Counts hash[u32, Count]
capability ExecCount danger observe = "kernel.process.exec.count.observe"
@capability(ExecCount)
@tracepoint("sched:sched_process_exec")
func OnExec(ctx tracepoint.Exec) i32 {
pid := bpf.current_pid()
if Counts.update(pid, Count{seen: FirstSeen}) != 0 {
return 0
}
if count := Counts.lookup(pid); count != nil {
count.seen = count.seen + 1
}
return 0
}
Helpers
Reusable logic belongs in small scalar helpers, not in raw C fragments. A
sectionless func is a Horizon user helper: it can be called from eBPF
entrypoints or other helpers, must be acyclic, and lowers to readable
static __always_inline C so clang and the verifier still see the final code.
Helper return values are scalar or bool. Helper parameters are scalar, bool,
or a nullable resource handle (ringbuf reservation, map lookup pointer, or
packet header); Horizon summarizes each helper's effect on the handle and
propagates resource state across the call, so callers see the transition
without having to re-derive it.
func should_count(pid u32) bool {
return pid != 0
}
func normalize_pid(pid u32) u32 {
if should_count(pid) {
return pid
}
return 1
}
capability ExecObserve danger observe = "kernel.process.exec.observe"
@capability(ExecObserve)
@tracepoint("sched:sched_process_exec")
func OnExec(ctx tracepoint.Exec) i32 {
pid := normalize_pid(bpf.current_pid())
if should_count(pid) {
return 0
}
return 0
}
Map sizing and shapes
Maps can be sized deliberately with @max_entries(...). Ringbuf sizes are byte
sizes and must be powers of two; hash and array sizes are entry counts. Use a
literal or an integer constant; Horizon resolves constants before emitting C map
definitions.
const CountEntries u32 = 4096
const EventBytes u32 = 262144
@max_entries(CountEntries)
map Counts hash[u32, Count]
@max_entries(EventBytes)
map ExecEvents ringbuf[ExecEvent]
Per-CPU maps are explicit when each CPU should get its own value slot. In
kernel-side .hzn, they use the same safe lookup/update/delete flow as ordinary
maps; generated Go bindings expose per-CPU values as typed slices.
type Count struct {
seen u64
}
map ExecCounts percpu_hash[u32, Count]
capability ExecCount danger observe = "kernel.process.exec.count.observe"
@capability(ExecCount)
@tracepoint("sched:sched_process_exec")
func CountExec(ctx tracepoint.Exec) i32 {
pid := bpf.current_pid()
if ExecCounts.update(pid, Count{seen: 1}) != 0 {
return 0
}
count := ExecCounts.lookup(pid)
if count == nil {
return 0
}
count.seen = count.seen + 1
return 0
}
LRU maps are explicit when state should stay bounded and let the kernel evict least-recently-used entries instead of surfacing map-full failures.
type Flow struct {
bytes u64
}
@max_entries(8192)
map Flows lru_hash[u64, Flow]
@max_entries(8192)
map CPUFlows lru_percpu_hash[u64, Flow]
Packet path
Packet-path programs use explicit section attributes, compiler-checked packet loads, nil-checked headers, and named action values instead of raw pointer arithmetic or integer returns.
package probes
capability DropCapability danger drop = "kernel.network.xdp.drop"
@capability(DropCapability)
@xdp
func DropTCP(ctx xdp.Context) i32 {
tcp := xdp.tcp(ctx)
if tcp == nil {
return xdp.Pass
}
port := xdp.ntohs(tcp.dst_port)
if (port == 443) && ((tcp.data_off & 0x0f) != 0) {
return xdp.Drop
}
return xdp.Pass
}
Probes
Tracing programs can also attach to kernel symbols with explicit kprobe and
kretprobe section attributes. Probe contexts stay opaque; use compiler-known
helpers such as kprobe.arg1(ctx) and kretprobe.ret(ctx) rather than raw
register arithmetic.
package probes
import bpf "m31labs.dev/horizon/runtime/kernel"
type OpenEvent struct {
pid u32
dfd i32
path [256]u8
}
map OpenEvents ringbuf[OpenEvent]
capability FileOpenObserve danger observe = "kernel.file.open.observe"
@capability(FileOpenObserve)
@kprobe("do_sys_openat2")
func OnOpen(ctx kprobe.Context) i32 {
event := OpenEvents.reserve()
if event == nil {
return 0
}
event.pid = bpf.current_pid()
event.dfd = i32(kprobe.arg1(ctx))
if bpf.probe_read_user_str(&event.path, kprobe.arg2(ctx)) < 0 {
OpenEvents.discard(event)
return 0
}
OpenEvents.submit(event)
return 0
}
@capability(FileOpenObserve)
@kretprobe("do_sys_openat2")
func OnOpenReturn(ctx kretprobe.Context) i32 {
rc := kretprobe.ret(ctx)
if rc < 0 {
return 0
}
return 0
}
TC
TC classifier programs are explicit about direction and return named TC actions, not raw integers.
package probes
capability TCObserve danger observe = "kernel.network.tc.observe"
@capability(TCObserve)
@tc("ingress")
func PassIngress(ctx tc.Context) i32 {
return tc.OK
}
Cgroup connect
Cgroup connect programs make policy decisions with named allow/deny actions.
Compiler-known helpers expose small, typed pieces of the kernel context instead
of making authors poke raw struct bpf_sock_addr fields.
package probes
capability ConnectBlock danger block = "kernel.network.connect.block"
@capability(ConnectBlock)
@cgroup("connect4")
func BlockSMTP(ctx cgroup.Connect) i32 {
if cgroup.family(ctx) != cgroup.FamilyIPv4 {
return cgroup.Allow
}
if cgroup.protocol(ctx) != cgroup.ProtocolTCP {
return cgroup.Allow
}
if cgroup.dst_port(ctx) == 25 && cgroup.dst_ip4(ctx) != cgroup.ip4(127, 0, 0, 1) {
return cgroup.Deny
}
return cgroup.Allow
}
LSM
LSM programs are policy hooks with opaque contexts. They return named allow/deny actions so authoring stays explicit about security impact.
package probes
capability FileOpenBlock danger block = "kernel.file.open.block"
@capability(FileOpenBlock)
@lsm("file_open")
func DenyFileOpen(ctx lsm.Context) i32 {
return lsm.Deny
}
Safety Model
Horizon makes verifier-sensitive behavior explicit before clang runs.
Resource typing
- ringbuf reservations must be nil-checked, submitted, or discarded exactly once
- scoped
if name := expr; conditiondeclarations lower to a C block before theif, so nullable lookup/header/reservation locals can be kept short-lived without leaking outside the branch - writes after ringbuf submit/discard are rejected
- map lookup results must be nil-checked before field access
- nullable map, packet, and ringbuf resource pointers cannot be copied or aliased
- raw address-taking and explicit pointer dereference are rejected; use compiler-known resource/header helpers and direct fixed-array helper operands instead
- source-authored pointer types such as
*u32are rejected; nullable pointers only come from compiler-known map lookup, ringbuf reserve, and packet helpers - ringbuf maps emit typed events and must use declared struct value types, not scalars or compiler-owned packet/header structs
- fixed array fields are address-only; pass
&event.commdirectly to helpers instead of copying arrays - packet headers returned by
xdp.eth(ctx),xdp.ipv4(ctx),xdp.tcp(ctx), andxdp.udp(ctx)must be nil-checked before field access
Type and width discipline
- conditions must be typed boolean expressions; integers and pointers need explicit comparison
- integer, bitwise, comparison, and boolean operators are typed before C emission
- integer width changes are explicit; write
u64(pid)oru16(port)instead of relying on implicit C coercions - integer literals, literal-backed constants, and literal-backed conversions are checked against their target scalar width before C emission
- unary negation is only allowed for signed scalar values or direct integer literals; unsigned values must be converted deliberately before signed arithmetic
- division and modulo by literal zero are rejected before C emission
- dynamic division and modulo require a divisor proven non-zero by a simple guard or non-zero constant
- literal shift counts must be non-negative and smaller than the left operand width
- dynamic shift counts must be proven non-negative and below the shifted value width with a simple guard
- constants can carry scalar widths, and generated C preserves those widths
- constants are immutable; use locals for values that change inside a program
- grouped constants are package-scoped constants with the same scalar, bool, and literal rules as standalone constants
- enum values are explicit typed integer constants; there is no implicit iota or untyped C enum widening
- type aliases are source-level names for scalar and bool widths only; they cannot target structs, pointer syntax, compiler-owned context/header types, or resource-bearing values
- grouped type declarations are package-scoped type declarations with the same alias and struct rules as standalone types
vardeclarations require an explicit scalar, bool, or declared struct type and cannot store nullable resources or compiler-owned packet/context typesswitchvalues must be scalar or bool, case values must be constant and type-compatible, and Horizon emits explicit Cbreakstatements so cases never fall through- short variable declarations introduce fresh local names only; use
=to update existing locals, and do not shadow maps or compiler namespaces
Capability discipline
hzn check,hzn emit-c,hzn bindgen,hzn workbench,hzn build, andhzn capabilitiesreject attachable programs without capability coverage- capability aliases can declare danger as a legacy flat word (
observe,mutate,drop,block,privileged) or as an explicit axis triple ("mode,scope,reversibility"); manifests emit the triple and never understate inferred program danger or a known danger suffix in the capability name - capability names must be unique across attachable programs, keeping generated manifests unambiguous for downstream policy and deployment tooling
- known
kernel.*capability namespaces must match known attach surfaces, so an XDP program cannot claim a process exec capability or a cgroup connect program cannot claim a file-open capability - capability manifests require each program's capability list to match the top-level capability entries, so Continuum consumers can trust either index
- capability manifest programs, maps, types, and type fields must have unique names; schema consumers never have to guess which duplicate wins
- default generated Go binding names must be valid and collision-free, so public APIs are checked as part of
hzn check
Verifier-aware constraints
- only bounded counted loops with numeric literal or integer const upper bounds are accepted
- helper availability is checked against the program kind
- compiler-known helpers have typed signatures and section availability rules before C emission
- every program must return an explicit
i32on every control-flow path - bare
returnis rejected; tracing programs should usereturn 0, while packet and policy programs should return named actions - tracepoints must use
category:eventattach strings, and kprobe, kretprobe, and LSM attach strings must be non-empty section tokens rather than path-like fragments - XDP programs must return named actions such as
xdp.Passandxdp.Drop, not raw integers - TC programs must declare
@tc("ingress")or@tc("egress")and return named actions such astc.OKandtc.Shot, not raw integers - cgroup connect programs must declare
@cgroup("connect4")or@cgroup("connect6"), use typed context helpers such ascgroup.protocol(ctx)andcgroup.dst_ip4(ctx), and return named actions such ascgroup.Allowandcgroup.Deny, not raw integers - cgroup context reads lower through typed generated wrappers, so generated C diagnostics map back to the authored
cgroup.*helper call - LSM programs must declare an explicit hook such as
@lsm("file_open")and return named actions such aslsm.Allowandlsm.Deny, not raw integers - kprobe arguments, safe user string reads, and kretprobe return registers are exposed through typed helper calls, not direct
pt_regsaccess - sectionless functions are user helpers, not eBPF programs; they are emitted as
static __always_inlineC, must be non-recursive, and currently accept and return only scalar or bool values - eBPF entrypoint functions cannot be called like helpers; share logic through sectionless helpers so attachable programs remain explicit
- struct fields must be unique, and structs are finite by-value records; recursive struct shapes are rejected before C emission
- stored data types for structs and keyed maps must be scalars, fixed arrays, or declared Horizon structs; compiler-owned context and packet header types stay helper-only
- package-scoped declarations cannot use compiler namespace names such as
bpf,xdp,tc,cgroup,lsm,kprobe, ortracepoint - map sizing is explicit through
@max_entries(...); integer constants are resolved before C emission, and ringbuf sizes must be powers of two - map update/delete results must be checked with an explicit comparison
Generated-artifact discipline
- generated BPF C and Go bindings include standard generated-file headers so downstream tools and reviewers can distinguish authored source from artifacts
- generated C validation only permits Horizon-owned
SEC(...)shapes for license, maps, and supported program sections - generated C validation permits raw
bpf_*calls only inside compiler-owned helper or map wrappers, never inside source-authored helper functions - generated map wrappers source-map back to the authored
lookup,update,delete,reserve,submit, ordiscardcall - generated BPF C is compiled with clang warnings treated as errors
- generated C stays readable so clang and verifier logs remain inspectable
- internal generated C constants and struct tags are prefixed to avoid collisions with kernel headers
- generated C emits only the helper and map wrappers the program actually uses
- parser failures are surfaced as stable diagnostics and never produce generated C
Commands
hzn check ./examples/execwatch
hzn check ./examples/execwatch -json
hzn new -list
hzn new ./scratch/execwatch
hzn new ./scratch/xdpdrop -template xdpdrop
hzn fmt ./examples/execwatch
hzn fmt ./examples -w
hzn fmt ./examples -check
hzn doctor
hzn doctor -capabilities dist/exec.cap.json
hzn version
hzn --version
hzn version -json
make setup-vmlinux
make ci-go
hzn workbench ./examples/execwatch -o dist
hzn workbench ./examples/execwatch -o dist -json
hzn workbench ./examples/execwatch -o dist -compile
hzn workbench ./examples/execwatch -o dist -preflight
hzn capabilities ./examples/execwatch -o dist/exec.cap.json
hzn build ./examples/cgroupconnect -o dist
sudo go run ./examples/cgroupconnect/cmd/cgroupconnect -obj dist/connect.bpf.o -cgroup /sys/fs/cgroup
hzn build ./examples/execwatch -o dist
go run ./examples/execwatch/cmd/execwatch -obj dist/exec.bpf.o
hzn build ./examples/execcount -o dist
sudo go run ./examples/execcount/cmd/execcount -obj dist/count.bpf.o -timeout 10s
hzn build ./examples/lsmfile -o dist
sudo go run ./examples/lsmfile/cmd/lsmfile -obj dist/file.bpf.o
hzn build ./examples/openwatch -o dist
sudo go run ./examples/openwatch/cmd/openwatch -obj dist/open.bpf.o
hzn build ./examples/tcpconnect -o dist
sudo go run ./examples/tcpconnect/cmd/tcpconnect -obj dist/tcp.bpf.o
hzn build ./examples/tcpass -o dist
sudo go run ./examples/tcpass/cmd/tcpass -obj dist/tc.bpf.o -iface lo
hzn build ./examples/xdpdrop -o dist
sudo go run ./examples/xdpdrop/cmd/xdpdrop -obj dist/xdp.bpf.o -iface lo
hzn diagnose dist/exec.verifier.log --map dist/exec.hznmap.json
hzn diagnose dist/exec.verifier.log --map dist/exec.hznmap.json -json -fail-on-error
hzn fmt gives .hzn files a canonical AST-based style for local editing and
CI. Use -w to update files in place and -check to fail when files need
formatting. The formatter preserves standalone and inline line comments.
hzn new creates an opinionated starter .hzn program that already follows
Horizon's safety rules: named capabilities, explicit danger, typed helpers,
nil-checked resources, and named packet actions. The default execwatch
template emits a tracepoint/ringbuf source program. -template also supports
execcount, openwatch, tcpconnect, kretprobe, xdpdrop, tcpass,
cgroupconnect, and lsmfile; run hzn new -list to inspect the current
catalog. Generated starters are meant to pass hzn check, hzn fmt -check,
and hzn workbench immediately.
hzn workbench is the authoring path: it validates source and writes readable
BPF C, a source map, typed Go bindings, a capability manifest, diagnostics, and
a report with source file hashes plus artifact kinds, byte sizes, and SHA-256
hashes. The report also includes a compact summary of source count, program
kinds, map kinds, capability danger levels, declared type count, and the
minimum kernel version implied by generated capabilities. Each run removes
stale artifacts for the target output base before writing new ones, records
replaced paths, and includes generator, version, Go toolchain, available VCS,
and timestamp provenance in the report.
Workbench artifact generation requires every attachable eBPF program to declare
at least one capability, so generated manifests cannot silently omit a program.
Use -preflight when workbench should run the same host readiness checks as
hzn doctor -capabilities against the generated manifest and include that
result in the report.
Invalid programs still produce
<name>.diagnostics.json and <name>.report.json, including parser failures
before typechecking or C emission can run. Clang failures are remapped into the
same diagnostics artifact, so editors and automation can show actionable
feedback without scraping terminal output. Diagnostics include source-line
context and markers when the authored file is available. Remapped diagnostics keep
the generated BPF C location plus source-map metadata such as Horizon function,
section, and AST node. Verifier diagnostics retain the recent verifier
instruction/source-comment context that led to the error. Common verifier
failures also carry Horizon-specific remediation hints for nil checks, ringbuf
lifetimes, bounded loops, helper availability, and stack usage. Use -compile
or hzn build when the local clang/BPF C toolchain should also produce a
.bpf.o. Both accept -clang-timeout=<duration> (default 30s, also
configurable via HZN_CLANG_TIMEOUT).
hzn diagnose remaps clang and verifier logs through an .hznmap.json source
map. By default it exits successfully when it can explain the log, even when the
log contains errors. Use -fail-on-error in CI or editor tasks that should exit
non-zero after emitting remapped diagnostics.
Generated Go bindings expose typed helpers around the loaded objects: ringbuf
maps get Read<Name>(context.Context, func(Event) error), hash maps get
Lookup<Name>, Update<Name>, ForEach<Name>, and Delete<Name>, array maps
get Lookup<Name>, Update<Name>, and ForEach<Name>, LRU hash maps use the
same typed hash helpers, per-CPU variants expose typed []Value slices for
lookup/update/iteration, and attachable programs get section-specific attach
methods. The raw *ebpf.Map and *ebpf.Program fields remain available for
advanced users, but ordinary consumers should not need to hand-roll cilium
loader, memlock, or map-access boilerplate. Bindings also embed
CapabilityManifestJSON and expose CapabilityManifest() so applications can
inspect the generated capability contract without manually locating the sidecar
manifest.
Ringbuf readers close themselves on context cancellation so blocking reads
unwind through the supplied context.
LoadObjects removes the memlock limit by default; use LoadObjectsWithOptions
when callers need explicit cilium collection options or custom rlimit behavior.
hzn check validates default generated binding names, so collisions with loader,
object, attach, map-helper, and reader APIs fail before artifacts are written.
make ci-go typechecks generated bindings for every example so attach helpers,
typed map helpers, and ringbuf readers stay valid against cilium/ebpf.
CI-oriented Make targets keep green logs compact and print captured command
output only when a gate fails.
Generated BPF C and generated Go bindings include scalar width, struct size, and
field offset assertions, so clang or go test fails early if an emitted type no
longer matches Horizon's ABI model.
Capability manifests include programs, map access, emitted event names, map key
and value types, and struct size/align/field-offset schemas for declared
Horizon types. They also include minimum kernel requirements for program types,
map kinds, and compiler-known helpers, plus deploy-time permission and host
feature requirements for attach surfaces such as tracefs, XDP, tc, cgroup v2,
and BPF LSM. Continuum consumers can inspect what a program observes, emits, and
needs from a target host without parsing BPF C.
Compiler-known helpers stay explicit: bpf.ktime_get_ns() lowers to
bpf_ktime_get_ns() and returns a typed u64 monotonic kernel timestamp.
bpf.current_ppid() lowers through a typed CO-RE task read, so the generated C
requires libbpf's bpf_core_read.h and a vmlinux.h that includes
struct task_struct layout.
hzn doctor checks the local eBPF C toolchain: clang BPF support, libbpf
headers including CO-RE helpers, bpftool/LLVM utilities, kernel BTF, and a
usable vmlinux.h. With -capabilities, it also reads a generated capability
manifest and checks the target host against the manifest's minimum kernel,
permission, and attach-feature requirements, including per-capability
requirements when a manifest carries them.
Use make setup-vmlinux on BTF-enabled Linux hosts to generate
/usr/local/include/vmlinux.h.
Capability manifests
Every Horizon build emits a capability manifest describing what the program observes, emits, mutates, and needs from a target host. The manifest is the contract Horizon hands to Continuum for governance and deployment.
The current manifest schema is m31labs.dev/horizon/capability/v1.
v0 manifests are rejected by capability.LoadManifest() with an
HZN3304 error; regenerate them as v1 (see
docs/migrations/v0-to-v1-manifest.md).
A manifest contains:
- Programs: each attachable program with its section, attach string, and declared capability name.
- Capabilities: each capability's danger axes (
mode×scope×reversibility), program/section, emitted event, map access, and per-programhelper_effectssummary of what the helpers observed, mutated, or required. - Maps: each map's kind (ringbuf, hash, array, percpu_hash,
lru_hash, ...), key/value type schemas,
@max_entries(...)sizing, and optional@steady_state_entries(...)/@access_freq(...)capacity-planning annotations. - Types: declared struct schemas with size, align, and field offsets.
- Emitted events: ringbuf event names linked to their struct schema.
- Minimum kernel requirements: feature versions implied by attach surfaces, map kinds, and compiler-known helpers.
- Deploy-time requirements: tracefs availability, XDP support, TC ingress/egress, cgroup v2, BPF LSM, plus required Linux capabilities per attach surface.
- Origin tagging: capabilities, maps, and types contributed by a
vendored imported package carry an
originfield naming the import alias they came from, so multi-package builds keep cross-package provenance visible.
hzn doctor -capabilities <manifest> checks the local host against a
manifest before deploy. Continuum consumes the same manifest to govern
which hosts may load which capabilities, what danger floor is acceptable
per environment, and how a capability composes with the rest of an
operator's policy surface.
What Horizon won't do
- Compile arbitrary Go. Horizon is a Go-shaped DSL, not a Go compiler.
- Hide the generated C. Generated BPF C is readable, source-mapped, and meant to be inspected by reviewers and the verifier alike.
- Replace clang or the verifier. Horizon checks what it can before clang runs; clang and the verifier remain the source of truth on the kernel side.
- Make unsafe kernel capabilities look safe. Every program declares explicit danger metadata; manifests never understate inferred danger.
- Silently emit a capability-free object.
hzn check,hzn emit-c,hzn bindgen,hzn workbench,hzn build, andhzn capabilitiesreject attachable programs without capability coverage. - Pretend the kernel is userspace. Verifier obligations, bounded loops, resource lifetimes, and helper availability stay visible in the source language.
Status
Latest tagged release: v0.1.2. v0.2 is in progress on main (Phase 0/1/2
shipped behind v0.2.0-phase{0,1,2} tags); the [Unreleased] section of
CHANGELOG.md tracks what has landed so far.
License
Apache-2.0. See LICENSE.
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
hzn
command
|
|
|
hzn-helpergen
command
hzn-helpergen walks a pinned libbpf source tree and produces candidate helper-registry entries for review against the hand-curated internal/registry/helpers-v1.json.
|
hzn-helpergen walks a pinned libbpf source tree and produces candidate helper-registry entries for review against the hand-curated internal/registry/helpers-v1.json. |
|
Package compiler — build matrix.
|
Package compiler — build matrix. |
|
examples
|
|
|
cgroupconnect/cmd/cgroupconnect
command
|
|
|
execcount/cmd/execcount
command
|
|
|
execdeny/cmd/execdeny
command
|
|
|
execwatch/cmd/execwatch
command
|
|
|
killwatch/cmd/killwatch
command
|
|
|
lsmfile/cmd/lsmfile
command
|
|
|
openwatch/cmd/openwatch
command
|
|
|
tcpass/cmd/tcpass
command
|
|
|
tcpconnect/cmd/tcpconnect
command
|
|
|
xdpdrop/cmd/xdpdrop
command
|
|
|
internal
|
|
|
registry
Package registry loads the canonical capability-namespace registry vendored from the Hyphae spec at ~/.hyphae/spaces/m31labs-horizon/specs/capability-namespaces-v1.json.
|
Package registry loads the canonical capability-namespace registry vendored from the Hyphae spec at ~/.hyphae/spaces/m31labs-horizon/specs/capability-namespaces-v1.json. |
|
Package parser — build-tag scanning.
|
Package parser — build-tag scanning. |
|
runtime
|
|
|
Package-level helper-effect summarization for cross-call resource tracking.
|
Package-level helper-effect summarization for cross-call resource tracking. |