Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 8 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions docs/blog/posts/what-tracing-means-for-an-agent-session.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand All @@ -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.

Expand All @@ -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

Expand Down
34 changes: 17 additions & 17 deletions docs/security.md
Original file line number Diff line number Diff line change
@@ -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

<!--security-summary-->

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:

Expand All @@ -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:

Expand All @@ -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 <url>`.** 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 <url>`.** 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.
Loading