read_text_head cannot reach the recent end of a log, which is the only end that answers an incident #19

Open
opened 2026-08-13 12:39:56 +00:00 by coilyco-ops · 0 comments
Owner

Filed by Olaf (OPS) from a live investigation this MCP almost solved. Small, concrete, and it would permanently unblock a class of question that currently needs an interactive operator.

The concrete case

I was establishing why node-stats-exporter on ser8 dies roughly every 2.3 days with nothing in its own logs — coilyco-bridge/deploy#436. The one thing that settles it is the kernel's own verdict, and this MCP can see that the evidence is right there:

stat_path /var/log/kern.log  ->  exists: true, 121,911,304 bytes, mtime 30s ago
stat_path /var/log/syslog    ->  exists: true, 158,719,128 bytes, mtime 20s ago

Both files are live and both certainly contain the answer. And read_text_head cannot get to it, because a head read of a 121 MB append-only log returns entries from whenever that file was created — months of boot noise — while the death I care about happened in the last hour. The default cap is 64 KiB, so the reader can see roughly the first 0.05% of the file, which is the least useful 0.05%.

That is not a bug. read_text_head does exactly what it says. The gap is that logs are read from the end, and this server can only read from the beginning.

What I tried, so nobody repeats it

  • /dev/kmsg — the kernel ring buffer, whose "head" would have been recent by construction. Correctly refused: path '/dev/kmsg' is outside the readable-root allowlist.
  • SigNoz — no k8s.* metrics are published at all, so container restart reasons are not there either.
  • get_k3s_pods — reports image but not imageID, and no termination reason or lastState.

So the estate has the data in three places and exposes it in none.

What would fix it

Either of these, smallest first:

  1. read_text_tail, mirroring read_text_head exactly — same allowlist, same cap, same refusals, reading the last max_bytes instead of the first. This is the whole ask and it is a seek(-n, SEEK_END).
  2. An offset parameter on read_text_head, which is strictly more general and lets a caller page. Slightly larger surface.

I would take (1). It is symmetric with what exists, the safety properties are identical because the allowlist and cap are unchanged, and it does not invite a caller to walk an entire file in 64 KiB slices.

Explicitly not asking for grep or filtering server-side. That turns a bounded reader into a query engine and expands the trust surface. Returning the tail bytes is enough — the caller can search them.

Why it is worth the change

This server's stated purpose is "bounded host introspection" for diagnostics. Right now it can tell you a log exists, how big it is, and when it last changed — everything about a log except what it just said. For a read-only diagnostic MCP the recent end of a log file is close to the highest-value thing on a host, and it is the one thing currently out of reach.

The alternative path for these questions is granting kubectl or shell access to the node, which is a far larger authority grant than "let the existing allowlisted reader read the other end of a file it can already open."

Acceptance

  • The last N bytes of an allowlisted text file can be read, honouring the same allowlist, the same byte cap, and the same refusals as read_text_head.
  • A path outside the allowlist is refused identically.
  • Reading a file smaller than max_bytes returns the whole file rather than erroring.

No urgency. Nothing is on fire; coilyco-bridge/deploy#436 stays labelled for an interactive operator meanwhile, and I have recorded there that this is the dead end rather than leaving it to be re-derived.

**Filed by Olaf (OPS)** from a live investigation this MCP *almost* solved. Small, concrete, and it would permanently unblock a class of question that currently needs an interactive operator. ## The concrete case I was establishing why `node-stats-exporter` on ser8 dies roughly every 2.3 days with nothing in its own logs — https://forgejo.coilysiren.me/coilyco-bridge/deploy/issues/436. The one thing that settles it is the kernel's own verdict, and this MCP can see that the evidence is right there: ``` stat_path /var/log/kern.log -> exists: true, 121,911,304 bytes, mtime 30s ago stat_path /var/log/syslog -> exists: true, 158,719,128 bytes, mtime 20s ago ``` Both files are live and both certainly contain the answer. And `read_text_head` cannot get to it, because a **head** read of a 121 MB append-only log returns entries from whenever that file was created — months of boot noise — while the death I care about happened in the last hour. The default cap is 64 KiB, so the reader can see roughly the first 0.05% of the file, which is the least useful 0.05%. That is not a bug. `read_text_head` does exactly what it says. The gap is that **logs are read from the end, and this server can only read from the beginning.** ## What I tried, so nobody repeats it - `/dev/kmsg` — the kernel ring buffer, whose "head" would have been recent by construction. Correctly refused: `path '/dev/kmsg' is outside the readable-root allowlist`. - SigNoz — no `k8s.*` metrics are published at all, so container restart reasons are not there either. - `get_k3s_pods` — reports `image` but not `imageID`, and no termination reason or `lastState`. So the estate has the data in three places and exposes it in none. ## What would fix it Either of these, smallest first: 1. **`read_text_tail`**, mirroring `read_text_head` exactly — same allowlist, same cap, same refusals, reading the last `max_bytes` instead of the first. This is the whole ask and it is a `seek(-n, SEEK_END)`. 2. **An `offset` parameter on `read_text_head`**, which is strictly more general and lets a caller page. Slightly larger surface. I would take (1). It is symmetric with what exists, the safety properties are identical because the allowlist and cap are unchanged, and it does not invite a caller to walk an entire file in 64 KiB slices. **Explicitly not asking for grep or filtering server-side.** That turns a bounded reader into a query engine and expands the trust surface. Returning the tail bytes is enough — the caller can search them. ## Why it is worth the change This server's stated purpose is *"bounded host introspection"* for diagnostics. Right now it can tell you a log exists, how big it is, and when it last changed — everything about a log except **what it just said**. For a read-only diagnostic MCP the recent end of a log file is close to the highest-value thing on a host, and it is the one thing currently out of reach. The alternative path for these questions is granting kubectl or shell access to the node, which is a far larger authority grant than "let the existing allowlisted reader read the other end of a file it can already open." ## Acceptance - The last N bytes of an allowlisted text file can be read, honouring the same allowlist, the same byte cap, and the same refusals as `read_text_head`. - A path outside the allowlist is refused identically. - Reading a file smaller than `max_bytes` returns the whole file rather than erroring. No urgency. Nothing is on fire; https://forgejo.coilysiren.me/coilyco-bridge/deploy/issues/436 stays labelled for an interactive operator meanwhile, and I have recorded there that this is the dead end rather than leaving it to be re-derived.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coilyco-flight-deck/node-stats-mcp#19
No description provided.