diff --git a/README.md b/README.md index 6dc8fcb..94a64fc 100644 --- a/README.md +++ b/README.md @@ -478,17 +478,16 @@ can disagree on cost and how to reconcile them, is ## Security -agent-top reads a developer's transcripts and process table, so the dependency -tree and the code are both part of the security story. Three checks run in -[the Security workflow](https://github.com/kannandreams/agent-top/actions/workflows/security.yml): -`cargo-deny` over advisories, licences and sources; CodeQL over the code; and a -check that the vulnerability count published on the site still matches a fresh -scan. The supply-chain rules are in [`deny.toml`](deny.toml): crates.io only, -no git dependencies, and a permissive-licence allowlist. Run them yourself with -`mise run security`. +Three checks run in +[the Security workflow](https://github.com/kannandreams/agent-top/actions/workflows/security.yml), +on every push and pull request and daily: `cargo-deny` over advisories, licences +and sources; CodeQL over the code; and a check that the vulnerability count +published on the site still matches a fresh scan. The supply-chain rules are in +[`deny.toml`](deny.toml): crates.io only, no git dependencies, and a +permissive-licence allowlist. Run them yourself with `mise run security`. The current count, with the advisory database revision it was measured against, -is [on the docs site](https://agenttop.dev/security/), and the same figures are +is [on the docs site](https://agenttop.dev/security/); the same figures are published as [JSON](https://agenttop.dev/data/security.json). ## Development diff --git a/docs/blog/posts/what-tracing-means-for-an-agent-session.md b/docs/blog/posts/what-tracing-means-for-an-agent-session.md index 2b8a737..d564c3f 100644 --- a/docs/blog/posts/what-tracing-means-for-an-agent-session.md +++ b/docs/blog/posts/what-tracing-means-for-an-agent-session.md @@ -20,7 +20,7 @@ The usual way to get a trace is instrumentation: you put OpenTelemetry calls aro Take Claude Code's JSONL. A tool call is a `tool_use` block; the answer is a `tool_result` a few lines later carrying the same `tool_use_id`. Codex pairs `function_call` and `function_call_output` by `call_id`. Both harnesses stamp the surrounding lines with a timestamp. That's a span: an id to pair begin and end, a start time, an end time once the pair completes. The file was never trying to be a trace — it's an append-only record of a conversation — but a trace is just what you get once you fold matching begin/end pairs into intervals. -This has one consequence worth dwelling on: the trace is retroactive. You don't decide to trace a session before it starts. You point at a transcript, today, for a session that ended last Tuesday, and get the same trace you'd have gotten if OpenTelemetry had been wired in from the first line. Nothing has to be switched on in the harness, which also means it works the same for every harness that writes a transcript with paired call/result records and a timestamp — Claude Code, Codex, Gemini CLI, OpenCode — without four different SDKs to integrate. +One consequence follows: the trace is retroactive. You don't decide to trace a session before it starts. You point at a transcript, today, for a session that ended last Tuesday, and get the same trace you'd have gotten if OpenTelemetry had been wired in from the first line. Nothing has to be switched on in the harness, which also means it works the same for every harness that writes a transcript with paired call/result records and a timestamp — Claude Code, Codex, Gemini CLI, OpenCode — without four different SDKs to integrate. ## Building the tree @@ -53,7 +53,7 @@ fn parent_turn<'a>(spans: &[&'a ToolSpan], i: usize) -> Option<&'a ToolSpan> { Subagent spans prefer a subagent turn of their own if the transcript carries one, and fall back to the main turn otherwise — which is the whole rule for keeping a subagent's fan-out on its own branch of the tree instead of flattening it into the parent's. -The other thing worth being honest about is spans that never close. A tool call issued right as the transcript ends, or a session that's still running when you export it, has no end timestamp. Inventing one — "assume it took as long as the last call" — would make a trace that lies. So an open span keeps its start and gets no end: in Chrome trace format that's a begin event (`"ph": "B"`) with no matching end, which Perfetto draws as a slice with no closing edge instead of a slice with a fabricated width; in OTLP, where an end time is mandatory, it gets one equal to its start plus an `agent_top.open` attribute, so a reader can tell "zero duration" from "we don't know." +The other case is a span that never closes. A tool call issued right as the transcript ends, or a session that's still running when you export it, has no end timestamp. Inventing one — "assume it took as long as the last call" — would make a trace that lies. So an open span keeps its start and gets no end: in Chrome trace format that's a begin event (`"ph": "B"`) with no matching end, which Perfetto draws as a slice with no closing edge instead of a slice with a fabricated width; in OTLP, where an end time is mandatory, it gets one equal to its start plus an `agent_top.open` attribute, so a reader can tell "zero duration" from "we don't know." ## Two shapes, two audiences @@ -76,7 +76,7 @@ You don't need Perfetto to get the first read. `Tab` in the detail pane switches The header line above the waterfall is the one number that answers the question people actually ask, which is "why has this agent been busy for eight minutes": the share of the window that was tool time versus inference time, with overlapping tool calls merged rather than summed so five parallel calls that each took four seconds don't get counted as twenty seconds of tool time. If that share is mostly tool, one call is the long pole and it's usually the widest bar on the tools track. If it's mostly the gap, the model was thinking — and no span log fixes a slow model, but at least you've ruled out the tool. -## A debugging session, not a demo +## Debugging a slow turn Say a turn took nine minutes and you want to know why before you decide whether to file it as a bug against the harness, the MCP server, or your own prompt. @@ -92,7 +92,7 @@ Open it in Perfetto. Three things to check, roughly in order of how often each o **Did the subagent track run serially when it should have run in parallel?** Subagent spans sit on their own tracks specifically so a fan-out that should overlap and doesn't is visible as five bars stacked with gaps between them instead of five bars stacked on top of each other. That shape — sequential when the intent was parallel — is easy to miss in a transcript and impossible to miss in a waterfall. -None of this requires you to have decided, ahead of time, that this session might need tracing. That's the actual point: the same file that exists because the harness needed to remember what happened is the input, so the debugging tool is available after the fact, for every session you already ran, not just the ones you had the foresight to watch live. +None of this requires you to have decided, ahead of time, that this session might need tracing. The input is the same file the harness wrote to remember what happened, so the trace is available after the fact, for every session you have already run. ## Try it diff --git a/docs/security.md b/docs/security.md index bb4101d..532063f 100644 --- a/docs/security.md +++ b/docs/security.md @@ -1,16 +1,16 @@ --- -description: "The security model behind agent-top: what it reads, what it never does, and the two network calls it can make, enumerated exactly." +description: "The security model behind agent-top: what it reads, what it never does, and the network calls it can make." --- # Security -agent-top's security model is a consequence of its design, not a policy bolted on afterward: it is read-only and local-only, so it never needs more trust than any other unprivileged process reading files you could already read yourself. +agent-top is read-only and local-only. It reads the transcripts a harness already wrote and the process table the OS already keeps, and changes neither. ## What the scans say -That figure is not typed into the page. `scripts/security_report.py` runs [cargo-audit](https://github.com/rustsec/rustsec) against the workspace's `Cargo.lock`, writes what it found to `docs/data/security.json`, and this page and the sidebar are rendered from that file at build time. CI re-runs the scan and fails if the published number disagrees with a fresh one, so it cannot go stale while still reading as current. +`scripts/security_report.py` runs [cargo-audit](https://github.com/rustsec/rustsec) against the workspace's `Cargo.lock` and writes the result to `docs/data/security.json`. This page and the sidebar are rendered from that file at build time, and CI fails if the published number disagrees with a fresh scan. Three checks run in [the Security workflow](https://github.com/kannandreams/agent-top/actions/workflows/security.yml), each answering a different question: @@ -20,9 +20,9 @@ Three checks run in [the Security workflow](https://github.com/kannandreams/agen | [CodeQL](https://codeql.github.com) | Does the code itself contain a pattern recognised as a vulnerability? | every push and pull request, and daily | | `security_report.py --check` | Does the count published above still match a fresh scan? | every push and pull request, and daily | -The daily schedule is the one that matters most. An advisory published against an unchanged `Cargo.lock` is a new vulnerability in a release that has already shipped, and nothing triggered by a push would ever notice it. +The daily run catches what a push cannot: an advisory published against an unchanged `Cargo.lock` is a new vulnerability in a release that has already shipped. -The supply-chain rules live in [`deny.toml`](https://github.com/kannandreams/agent-top/blob/main/deny.toml) and are stricter than the default: every crate must come from crates.io — no git dependencies, no alternative registries — and carry a licence from an explicit permissive allowlist, so a new dependency under an unexpected licence fails the build rather than shipping quietly inside the binary. Wildcard version requirements are refused outright. +The supply-chain rules are in [`deny.toml`](https://github.com/kannandreams/agent-top/blob/main/deny.toml). Every crate must come from crates.io — no git dependencies, no alternative registries — and carry a licence from an explicit permissive allowlist. A wildcard version requirement fails the build. To run the same checks yourself: @@ -34,35 +34,35 @@ mise run security # both, the way CI does ## What it reads -The process table (through `sysinfo` and, on macOS, `libproc` — not by shelling out to `ps`) and the transcript files each harness already writes. From a transcript it takes metadata: usage records (token counts), tool and MCP call names, ids, timestamps, session id, model, working directory. It does not read prompt text or tool output. [Accounting](accounting.md) is the arithmetic that follows from that boundary; [Context by source](accounting.md#context-by-source) is the clearest case of it — what a tool result added to the prompt is priced from the token counts alone, never from the result itself. +The process table (through `sysinfo` and, on macOS, `libproc` — not by shelling out to `ps`) and the transcript files each harness already writes. From a transcript it takes metadata: usage records (token counts), tool and MCP call names, ids, timestamps, session id, model, working directory. It does not read prompt text or tool output. [Context by source](accounting.md#context-by-source) works within that boundary: what a tool result added to the prompt is priced from the token counts alone, not from the result. [Accounting](accounting.md) has the arithmetic. ## What it never does - **Never writes to a transcript.** The files a harness owns are never touched. - **Never signals or kills a process.** An orphaned MCP server is reported — pid, memory, age, the agent it came from — and the `kill` is yours. See [what agent-top is not](vision.md#what-agent-top-is-not). -- **No shell-outs for discovery.** Process and file information comes from library calls (`sysinfo`, `libproc`), never from parsing the output of `ps`, `lsof`, or any other command. -- **No credentials, no provider.** agent-top does not hold an API key or talk to a model. It has nothing to leak on that front because it was never given anything to hold. -- **No `unsafe`.** Both crates are safe Rust; nothing in agent-top reaches past what the compiler checks. +- **No shell-outs for discovery.** Process and file information comes from library calls (`sysinfo`, `libproc`), not from parsing the output of `ps`, `lsof`, or any other command. +- **No credentials, no provider.** agent-top does not hold an API key or talk to a model. +- **No `unsafe`.** There is no `unsafe` block in either crate. -## The network, enumerated exactly +## Network calls -Two calls, both narrow enough to name in full: +There are two. -1. **A daily version check.** `GET https://crates.io/api/v1/crates/agent-top` with a fixed `User-Agent` and nothing else — no session data, no machine identifier, no query string. Cached to run at most once a day, in `~/.cache/agent-top/update-check.json` (or `$XDG_CACHE_HOME/agent-top/update-check.json`), which holds only when it last checked, what it found, and which version you dismissed. `AGENT_TOP_NO_UPDATE_CHECK=1` turns it off. -2. **`trace --endpoint `.** Posts the OTLP document to the exact address typed on the command line, once, only when typed. No default endpoint, no config key, no environment variable that turns it on silently. +1. **A daily version check.** `GET https://crates.io/api/v1/crates/agent-top` with a fixed `User-Agent` and nothing else — no session data, no machine identifier, no query string. Cached to run at most once a day, in `~/.cache/agent-top/update-check.json` (or `$XDG_CACHE_HOME/agent-top/update-check.json`), which holds when it last checked, what it found, and which version you dismissed. `AGENT_TOP_NO_UPDATE_CHECK=1` turns it off. +2. **`trace --endpoint `.** Posts the OTLP document to the address typed on the command line, once. No default endpoint, no config key, no environment variable that turns it on. -Nothing else opens a connection. `--json` and `--replay` never touch the network — replay reads only the file you give it. +Nothing else opens a connection. `--json` and `--replay` never touch the network; replay reads only the file you give it. ## The one command that changes the machine -Accepting the upgrade prompt (`u`) runs a fixed command line chosen by detecting how the running binary was installed — `brew update && brew upgrade agent-top`, `cargo binstall -y agent-top`, or `cargo install --locked agent-top` — never a string built from anything you typed, never through a shell. It is printed before it runs, it happens only on that keypress, and it is the only thing agent-top ever changes outside of itself. +Accepting the upgrade prompt (`u`) runs a fixed command line chosen by detecting how the running binary was installed — `brew update && brew upgrade agent-top`, `cargo binstall -y agent-top`, or `cargo install --locked agent-top` — never a string built from input, and never through a shell. The command is printed before it runs, and it runs only on that keypress. ## Local files it touches **Reads:** the transcripts, the process table, and — if it exists — `~/.config/agent-top/prices.toml`. A malformed price file is reported on stderr and ignored; the built-in prices still apply. See [Prices](prices.md). -**Writes:** the one cache file above, and nothing else on its own. `--replay` and `--json` are read/print only; neither touches disk beyond the file you point at. +**Writes:** the one cache file above. `--replay` and `--json` read and print; neither touches disk beyond the file you point at. ## Reporting an issue -agent-top is MIT-licensed and the source is the whole story: [github.com/kannandreams/agent-top](https://github.com/kannandreams/agent-top). Open an issue, or, for anything that should not be public first, a private report through GitHub's Security tab. +agent-top is MIT-licensed: [github.com/kannandreams/agent-top](https://github.com/kannandreams/agent-top). Open an issue, or, for anything that should not be public first, a private report through GitHub's Security tab.