@@ -55,6 +55,23 @@ Userspace enriches each event with **identity** (k8s cgroup→pod, `/proc` comm+
5555in-kernel ` comm ` fallback for short-lived processes), a ` (pid,fd)→peer ` ** correlation** , and
5656** provider** classification (SNI → 15 LLM providers); then exports ** NDJSON** (or a human log).
5757
58+ ** Example output** (` A3S_OBSERVER_JSON=1 ` , one event per line — wrapped here for readability):
59+
60+ ``` json
61+ {"identity" :{"agent" :" python3" ,"task" :" 1841" ,"session" :null },"provider" :" Anthropic" ,
62+ "event" :{"LlmCall" :{"pid" :1841 ,"sni" :" api.anthropic.com" ,"peer" :" 160.79.104.10" ,
63+ "req_bytes" :284 ,"resp_bytes" :3832 ,"latency" :{"secs" :1 ,"nanos" :210000000 },
64+ "ttft" :{"secs" :0 ,"nanos" :410000000 }}}}
65+ {"identity" :{"agent" :" python3" ,"task" :" 1903" ,"session" :null },"provider" :null ,
66+ "event" :{"ToolExec" :{"pid" :1903 ,"ppid" :1841 ,"argv" :[" /usr/bin/git" ],"cwd" :" " }}}
67+ {"identity" :{"agent" :" python3" ,"task" :" 1841" ,"session" :null },"provider" :null ,
68+ "event" :{"SslContent" :{"pid" :1841 ,"is_read" :false ,
69+ "content" :" POST /v1/messages HTTP/1.1\r\n Host: api.anthropic.com\r\n ..." }}}
70+ ```
71+
72+ Filter with ` jq ` , e.g. every LLM call and its provider:
73+ ` … | jq -c 'select(.event.LlmCall) | {agent:.identity.agent, provider, sni:.event.LlmCall.sni}' ` .
74+
5875## Intervene — egress / file / exec (opt-in)
5976
6077The same vantage point enforces an ** external policy** — a plain file any controller writes; the
@@ -66,8 +83,46 @@ kernel asks a guard allow/deny per action. The observe-only core is untouched. B
6683| ` a3s-observer-enforce ` | eBPF ` cgroup/connect4 ` | ` connect() ` to policy IPs/hosts — cgroup-scoped, fail-open, DNS-re-resolved |
6784| ` a3s-observer-fileguard ` | fanotify ` FAN_OPEN_PERM ` + ` FAN_OPEN_EXEC_PERM ` | ` open() ` ** and** ` exec ` of policy-listed files |
6885
69- Drive it in-process (the ` Policy ` trait) or out-of-process — ` scripts/example-controller.py `
70- turns observed events into a deny-list. See [ ` docs/enforcement.md ` ] ( docs/enforcement.md ) .
86+ ### Bring your own policy
87+
88+ The decision logic is ** yours** , in any language. a3s-observer gives you the signal (events)
89+ and the enforcement primitive (a guard that reads a deny-file); you write the policy in between:
90+
91+ ```
92+ events (NDJSON) → your controller (your rules) → deny-file → guard → kernel denies (EPERM)
93+ ```
94+
95+ ** Egress allow-list** — an agent may only reach approved LLM providers; everything else is cut:
96+
97+ ``` bash
98+ # 1. observe → 2. your controller writes the deny-list → 3. enforce on the agent's cgroup
99+ A3S_OBSERVER_JSON=1 sudo -E a3s-observer-collector \
100+ | ./scripts/example-controller.py egress-deny.txt &
101+ sudo a3s-observer-enforce /sys/fs/cgroup/< agent> egress-deny.txt
102+ ```
103+
104+ The controller is ~ 10 lines — the ` if ` is the part you own (` scripts/example-controller.py ` ):
105+
106+ ``` python
107+ ALLOWED = {" Anthropic" , " OpenAi" , " Gemini" } # your rule
108+ for line in sys.stdin: # the NDJSON event stream
109+ ev = json.loads(line); call = ev.get(" event" , {}).get(" Egress" )
110+ if call and call.get(" sni" ) and ev.get(" provider" ) not in ALLOWED :
111+ denied.add(call[" peer" ]) # → write egress-deny.txt (hot-reloaded)
112+ open (sys.argv[1 ], " w" ).write(" \n " .join(sorted (denied)) + " \n " )
113+ ```
114+
115+ ** File / exec deny** — no event stream needed, just a path list (also hot-reloaded):
116+
117+ ``` bash
118+ printf ' %s\n' /etc/shadow /usr/bin/curl > deny.txt # deny open(/etc/shadow) + exec(curl)
119+ sudo a3s-observer-fileguard deny.txt # edit deny.txt → applies within ~2s
120+ ```
121+
122+ Deny-file formats: ** egress** = one IPv4 or hostname per line (hostnames are DNS-re-resolved
123+ each reload); ** file/exec** = one path per line. Prefer in-process (Rust) embedding? Implement
124+ the ` Policy ` trait (` egress ` / ` file_write ` / ` exec ` → ` Verdict ` ). Full design + both paths:
125+ [ ` docs/enforcement.md ` ] ( docs/enforcement.md ) .
71126
72127## Why eBPF, and the boundary
73128
0 commit comments