Skip to content

Commit 9e356f4

Browse files
RoyLinRoyLin
authored andcommitted
release: v0.7.0 — richer tool observability (full argv + cwd)
The exec probe captures argv[0..] in-kernel (up to 12x64B) — the agent's intent (curl <url>, sh -c '<cmd>'), not just the binary — and the collector fills cwd from /proc/<pid>/cwd. Host-validated: a marker exec surfaced its full argv + cwd. Folds in the heartbeat-write warning. 0.6.0 -> 0.7.0.
1 parent 848fa01 commit 9e356f4

10 files changed

Lines changed: 64 additions & 18 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@
22

33
All notable changes to a3s-observer will be documented in this file.
44

5-
## [Unreleased]
5+
## [0.7.0] — richer tool observability (full argv + cwd)
66

7-
### Changed
7+
### Added / Changed
88

9+
- **Full command line on `ToolExec`** — the exec probe captures `argv[0..]` in-kernel (up to 12
10+
args × 64 bytes), not just the binary, so an agent's *intent* is visible
11+
(`curl --url=… "two words"`, `sh -c "<cmd>"`); the working directory (`cwd`) is filled from
12+
`/proc/<pid>/cwd`. Host-validated: a marker exec surfaced its full argv + cwd.
913
- The collector warns if the liveness heartbeat write fails — an unwritable
1014
`A3S_OBSERVER_HEARTBEAT` path would otherwise cause a silent livenessProbe restart-loop.
1115

Cargo.lock

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "a3s-observer"
3-
version = "0.6.0"
3+
version = "0.7.0"
44
edition = "2021"
55
license = "MIT"
66
description = "General-purpose, language-agnostic eBPF observability for AI agents (LLM calls, tools, files, network egress)."

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ latency / TTFT, or plaintext) / **where** (peer IP / hostname).
4343

4444
| signal | kernel hook | event |
4545
|---|---|---|
46-
| `exec` | `sys_enter_execve` | `ToolExec` — tool / subprocess (argv, comm, uid) |
46+
| `exec` | `sys_enter_execve` | `ToolExec` — tool / subprocess: **full argv** + cwd, comm, uid |
4747
| `connect` | `sys_enter_connect` | `Egress` — peer IP:port |
4848
| `sni` | TLS ClientHello (plaintext `server_name`) | LLM **provider** + endpoint |
4949
| `dns` | `sendto` / `sendmsg` / `sendmmsg` to :53 | `Dns` — resolved hostname |
@@ -63,7 +63,8 @@ in-kernel `comm` fallback for short-lived processes), a `(pid,fd)→peer` **corr
6363
"req_bytes":284,"resp_bytes":3832,"latency":{"secs":1,"nanos":210000000},
6464
"ttft":{"secs":0,"nanos":410000000}}}}
6565
{"identity":{"agent":"python3","task":"1903","session":null},"provider":null,
66-
"event":{"ToolExec":{"pid":1903,"ppid":1841,"argv":["/usr/bin/git"],"cwd":""}}}
66+
"event":{"ToolExec":{"pid":1903,"ppid":1841,
67+
"argv":["git","clone","https://github.com/acme/repo"],"cwd":"/home/agent/work"}}}
6768
{"identity":{"agent":"python3","task":"1841","session":null},"provider":null,
6869
"event":{"SslContent":{"pid":1841,"is_read":false,
6970
"content":"POST /v1/messages HTTP/1.1\r\nHost: api.anthropic.com\r\n..."}}}

a3s-observer-collector/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "a3s-observer-collector"
3-
version = "0.6.0"
3+
version = "0.7.0"
44
edition = "2021"
55
license = "MIT"
66
description = "a3s-observer collector: loads the eBPF probes and exports enriched events."

a3s-observer-collector/src/main.rs

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -216,8 +216,8 @@ async fn main() -> anyhow::Result<()> {
216216
event: AgentEvent::ToolExec {
217217
pid: ev.pid,
218218
ppid: read_ppid(ev.pid),
219-
argv: argv_of(&ev.filename),
220-
cwd: String::new(),
219+
argv: argv_of(&ev),
220+
cwd: read_cwd(ev.pid),
221221
},
222222
});
223223
}
@@ -387,8 +387,26 @@ fn read_pod<T: Copy>(item: &[u8]) -> Option<T> {
387387
.then(|| unsafe { core::ptr::read_unaligned(item.as_ptr() as *const T) })
388388
}
389389

390-
fn argv_of(filename: &[u8; 128]) -> Vec<String> {
391-
vec![cstr(filename)]
390+
/// Full argv (argv[0..argc]) captured in-kernel; falls back to the binary path if none.
391+
fn argv_of(ev: &ExecEvent) -> Vec<String> {
392+
let n = (ev.argc as usize).min(ev.args.len());
393+
let argv: Vec<String> = ev.args[..n]
394+
.iter()
395+
.map(|a| cstr(a))
396+
.filter(|s| !s.is_empty())
397+
.collect();
398+
if argv.is_empty() {
399+
vec![cstr(&ev.filename)]
400+
} else {
401+
argv
402+
}
403+
}
404+
405+
/// The process's current working directory (≈ exec-time for a fresh process).
406+
fn read_cwd(pid: u32) -> String {
407+
std::fs::read_link(format!("/proc/{pid}/cwd"))
408+
.map(|p| p.to_string_lossy().into_owned())
409+
.unwrap_or_default()
392410
}
393411

394412
/// A NUL-terminated byte buffer (from a kernel copy) as a lossy String.

a3s-observer-common/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "a3s-observer-common"
3-
version = "0.6.0"
3+
version = "0.7.0"
44
edition = "2021"
55
license = "MIT"
66
description = "Shared no_std types crossing the eBPF <-> userspace boundary for a3s-observer."

a3s-observer-common/src/lib.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,21 @@
88
///
99
/// This is the language-agnostic "tool ran" signal — AgentSight's flagship is catching
1010
/// subprocesses that bypass app-level instrumentation, and this is how we see them.
11+
/// Up to `ARGV_SLOTS` arguments, each truncated to `ARG_LEN` bytes, are captured per exec —
12+
/// the args carry the agent's intent (`curl <url>`, `sh -c "<cmd>"`), not just the binary.
13+
pub const ARGV_SLOTS: usize = 12;
14+
pub const ARG_LEN: usize = 64;
15+
1116
#[repr(C)]
1217
#[derive(Clone, Copy)]
1318
pub struct ExecEvent {
1419
pub pid: u32,
1520
pub ppid: u32,
1621
pub uid: u32,
22+
pub argc: u32, // number of args captured into `args` (may be < the real argc)
1723
pub comm: [u8; 16],
1824
pub filename: [u8; 128],
25+
pub args: [[u8; ARG_LEN]; ARGV_SLOTS], // argv[0..argc], each NUL-terminated
1926
}
2027

2128
/// The leading bytes of an outbound TLS ClientHello, captured at the send syscall.

a3s-observer-ebpf/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "a3s-observer-ebpf"
3-
version = "0.6.0"
3+
version = "0.7.0"
44
edition = "2021"
55
license = "MIT"
66
publish = false

a3s-observer-ebpf/src/main.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
#![no_main]
33

44
use a3s_observer_common::{
5-
ConnectEvent, DnsEvent, ExecEvent, FileEvent, LlmEvent, SslEvent, TlsEvent, DNS_SNAP_LEN,
6-
PATH_SNAP_LEN, SSL_SNAP_LEN, TLS_SNAP_LEN,
5+
ConnectEvent, DnsEvent, ExecEvent, FileEvent, LlmEvent, SslEvent, TlsEvent, ARGV_SLOTS,
6+
ARG_LEN, DNS_SNAP_LEN, PATH_SNAP_LEN, SSL_SNAP_LEN, TLS_SNAP_LEN,
77
};
88
use aya_ebpf::{
99
helpers::gen::bpf_probe_read_user,
@@ -122,6 +122,22 @@ fn try_exec(ctx: &TracePointContext) -> Result<u32, i64> {
122122
if let Ok(filename_ptr) = ctx.read_at::<*const u8>(16) {
123123
let _ = bpf_probe_read_user_str_bytes(filename_ptr, &mut (*ev).filename);
124124
}
125+
// `const char *const *argv` at offset 24 — capture up to ARGV_SLOTS args (the intent:
126+
// which URL / which command, not just the binary). Bounded for the verifier.
127+
(*ev).argc = 0;
128+
(*ev).args = [[0u8; ARG_LEN]; ARGV_SLOTS];
129+
if let Ok(argv) = ctx.read_at::<*const u8>(24) {
130+
for i in 0..ARGV_SLOTS {
131+
let Some(argp) = read_user_u64(argv.add(i * 8)) else {
132+
break;
133+
};
134+
if argp == 0 {
135+
break; // end of argv
136+
}
137+
let _ = bpf_probe_read_user_str_bytes(argp as *const u8, &mut (*ev).args[i]);
138+
(*ev).argc += 1;
139+
}
140+
}
125141
}
126142
entry.submit(0);
127143
Ok(0)

0 commit comments

Comments
 (0)