Skip to content

v3.11 — sysadmin: read-only log diagnosis (discover → collect → synthesize) - #11

Merged
Kurtisone merged 24 commits into
mainfrom
v3.11-sysadmin
Aug 12, 2026
Merged

v3.11 — sysadmin: read-only log diagnosis (discover → collect → synthesize)#11
Kurtisone merged 24 commits into
mainfrom
v3.11-sysadmin

Conversation

@Kurtisone

Copy link
Copy Markdown
Owner

Summary

Adds a sysadmin tool: a deterministic three-step graph (discover → collect → synthesize, one call, no done:false) that diagnoses real service/system problems from actual logs. Read-only, always — there is no restart/stop/mutate path anywhere in the code, by design, same posture as the git tool.

Triggered from chat like any other tool ("le service X plante en boucle, tu peux regarder ?"), no new UI surface, consistent with the zero-tab constraint.

What's new
graphs/sysadmin.py — discover (active systemd units + podman containers) → collect (logs for a validated target, or kernel logs as fallback) → synthesize (one LLM call, plain-text diagnosis + proposed fix, never an executed action)
tools/sysadmin.py — chat-dispatchable JSON wrapper ({"target_hint": "...", "question": "..."}), same contract shape as review/research
Router entries — description + few-shot examples in router/prompt.py, including explicit contrast against files and shell to avoid the ambiguity classes hit in v3.9/v3.10
UI: expandable per-step detail (forge/subtrace.py) — any graph-based tool (review/research/sysadmin) can now publish its internal node steps to the Traces panel without widening the strict tool.run() -> str contract; a step pill with sub-steps gets a caret, click to expand
Read-only host access, three independent proxies (deploy/) — never the raw sockets:
journalctl — plain bind mount (/var/log/journal:ro), no daemon involved
systemctl/busctl — xdg-dbus-proxy (same tool Flatpak uses), deny-by-default, only 5 read-only method calls allowed
podman logs/ps — a small stdlib-only HTTP proxy (podman_ro_proxy.py), GET-only on two endpoints, 403 on everything else
deploy/setup-sysadmin-host-access.sh — one-shot idempotent installer (systemd --user units, persists across reboots)
deploy/ansible/sysadmin-proxies.yml — starting point for provisioning a new host
Why proxies instead of raw sockets + a confirmation step

A confirmation in Forge's own code only guards the path Forge chooses to take. Once a socket is reachable from inside the container, anything else running there (a future bug, a compromised dependency, any code-execution path) can reach it directly, bypassing the confirmation entirely. The three proxies make the mutating calls structurally unavailable regardless of what code runs in the container — full reasoning in deploy/README.md.

Real production bugs found and fixed this cycle

Left in as regression tests, not just fixed silently:

Prompt-injection-shaped example leak — the model copied the GOOD ANSWER few-shot example verbatim as a fabricated diagnosis for an unrelated question; fixed with a leak-fragment detector plus a rewritten, deliberately non-copiable placeholder example
Context overflow crash (4362 > 4096 tokens) — SYSADMIN_MAX_LOG_LINES alone didn't bound prompt size; added a hard character budget (SYSADMIN_LOG_CHARS_BUDGET) independent of line count
[error] ... strings parsed as real data — both a Python-exception path and a nonzero-exit-code path could slip a raw error message through as if it were a discovered unit/container name
systemctl silently ignoring DBUS_SYSTEM_BUS_ADDRESS — confirmed via SYSTEMD_LOG_LEVEL=debug that it hardcodes an attempt at /run/systemd/private first with no fallback; switched discovery to busctl, which has none of that quirk (--json=short, real parseable output)
Rootless podman not passing host supplementary groups — journalctl -u on root-owned services needs wheel/adm group membership inside the container; fixed with --group-add keep-groups (run.oci.keep_original_groups: "1" in compose)
Testing

389 tests passing (344 pre-existing + 45 new), ruff check/ruff format --check clean. Notably includes a mutation-tested security check (test_sysadmin_never_passes_unvalidated_name_to_subprocess) — verified to actually fail against a deliberately broken validation before being trusted as a real guard, not just a test that passes by construction.

Deployment
bash
./deploy/setup-sysadmin-host-access.sh # one-time, idempotent

then mount the proxies + journal and add sysadmin to ENABLED_TOOLS — see README.md and deploy/README.md for the full commands (podman run and podman-compose both covered).

Follow-ups

None blocking — the security audit discussed alongside this PR is tracked separately, not part of this scope.

@Kurtisone
Kurtisone merged commit bedafa9 into main Aug 12, 2026
2 checks passed
@Kurtisone
Kurtisone deleted the v3.11-sysadmin branch August 12, 2026 15:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant