Skip to content

Commit 848fa01

Browse files
RoyLinRoyLin
authored andcommitted
docs: add usage + custom-intervention examples to README
- Observe: example NDJSON output (LlmCall/ToolExec/SslContent) + a jq filter. - Intervene: 'bring your own policy' worked example — egress allow-list controller (observe → controller → deny-file → enforce on cgroup) + file/exec deny; deny-file formats.
1 parent 586503a commit 848fa01

1 file changed

Lines changed: 57 additions & 2 deletions

File tree

README.md

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,23 @@ Userspace enriches each event with **identity** (k8s cgroup→pod, `/proc` comm+
5555
in-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\nHost: 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

6077
The 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

Comments
 (0)