fix: transport escalation exempts log reads, get_entity accepts slug alias, add restart_service + push_file tools
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
ci / web (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled

P0: Transport escalation in classifyAndGate (server.go:745) now exempts
    log-inspection commands (tail/head/cat/journalctl on *.log or /logs/)
    from the /opt//etc//var/lib/ gating on LXC targets. Fixes the
    'tail -3 /opt/seanime/data/logs/seanime.log queued for approval' bug.

P1: queryEntity returns actionable error when slug_or_id param is empty
    ('slug_or_id is required') instead of silent 'entity not found: '.

P2: Added slugArg() helper (server.go) so get_entity, get_relations,
    get_blast_radius, and explain accept 'slug' as an alias for their
    declared param name. Solves the discoverability inconsistency where
    every tool used a different param name for the same concept.

P3: Two new MCP tools:
    - restart_service(target, service) — systemctl restart wrapper,
      correctly classified config_mutation (requires approval)
    - push_file(target, source_path, dest_path, backup=true) — pct push
      from Proxmox host into LXC, with optional backup. Classified
      config_mutation. LXC-only for now.

P4: Updated homelab-lxc-ops skill with MCP tools preference table.

Plus: Wails desktop build now uses build-tag approach for frontend embed
      (assets_embed.go + assets_stub.go), so go build ./... works on
      clean checkout without the frontend built first.

Version: 0.31.0 → 0.32.0
This commit is contained in:
2026-08-15 17:38:26 +02:00
parent 7160eee1e1
commit 8fbe39cf2a
11 changed files with 162 additions and 12 deletions

View File

@@ -300,6 +300,22 @@ func argsMap(req *mcp.CallToolRequest) map[string]any {
return m
}
// slugArg returns the first non-empty string value among the given keys.
// Tools historically used inconsistent param names for "the entity slug"
// (slug_or_id, entity_id, service_slug, lxc_slug, target). This lets a single
// handler accept any of them, so an agent guessing "slug" still works.
func slugArg(m map[string]any, keys ...string) string {
if m == nil {
return ""
}
for _, k := range keys {
if s, ok := m[k].(string); ok && strings.TrimSpace(s) != "" {
return strings.TrimSpace(s)
}
}
return ""
}
func getFloat(m map[string]any, key string, def float64) float64 {
if m == nil {
return def
@@ -353,6 +369,9 @@ func jsonErr(format string, args ...any) []byte {
}
func queryEntity(ctx context.Context, pool *db.Pool, idOrSlug string) *mcp.CallToolResult {
if strings.TrimSpace(idOrSlug) == "" {
return textResult("slug_or_id is required — expected an entity slug (e.g. lxc:seanime) or UUID")
}
var id uuid.UUID
if u, err := uuid.Parse(idOrSlug); err == nil {
id = u
@@ -742,9 +761,15 @@ func classifyAndGate(ctx context.Context, pool *db.Pool, agentID, targetID uuid.
// the same command locally on the Proxmox host via pct exec.
// Caught live: "cat /etc/hostname" on lxc:dns queued as config_mutation
// while "pct exec 107 -- cat /etc/hostname" on host:hubris auto-ran.
//
// EXEMPTION: tail/head/cat/less/journalctl on log files (*.log, */logs/*)
// are always read-only — caught live 2026-08-15 session f6a7d4e:
// "tail -3 /opt/seanime/data/logs/seanime.log" on lxc:seanime was gated.
if riskClass == policy.RiskReadOnly && strings.HasPrefix(targetSlug, "lxc:") {
if strings.Contains(command, "/opt/") || strings.Contains(command, "/etc/") || strings.Contains(command, "/var/lib/") {
riskClass = policy.RiskConfigMutation
if !isLogInspectionRead(command) {
if strings.Contains(command, "/opt/") || strings.Contains(command, "/etc/") || strings.Contains(command, "/var/lib/") {
riskClass = policy.RiskConfigMutation
}
}
}
@@ -1134,6 +1159,22 @@ func hostLxcCommand(cmd string) (string, bool) {
return first, hostLxcCommands[first]
}
// isLogInspectionRead returns true if the command is a safe read-only operation
// on a log file — tail, head, cat, less, or journalctl with a .log or /logs/ path.
// Used by the transport-aware escalation check to avoid gating the most common
// debugging action (e.g. "tail -3 /opt/seanime/data/logs/seanime.log" on lxc:seanime).
func isLogInspectionRead(cmd string) bool {
trimmed := strings.TrimSpace(cmd)
for _, prefix := range []string{"tail ", "head ", "cat ", "less ", "journalctl "} {
if strings.HasPrefix(trimmed, prefix) {
if strings.Contains(trimmed, ".log") || strings.Contains(trimmed, "/logs/") {
return true
}
}
}
return false
}
// validateCommandSyntax checks for common LLM-generated bash errors that always
// fail at the shell. Returns an error message or "" if the command looks valid.
func validateCommandSyntax(cmd string) string {