Add token expiry, edit notice, log allowlist, local-zone test; merge PLAN.md into SPECS.md
get_logs needs a unit from an allowlist (router-*, kea, unbound, networkd, nftables, ddclient, acme, sshd, kernel). API tokens are valid for 1, 7, 30 (default) or 90 days, or without limit. The overview and the Changes page say which token made the unapplied changes. options.json (router://options) no longer holds the options' source paths: they differ between builds of a release, so a router rebuilding itself offline had to build the file and failed (found by the update test). Web UI: panels flow into columns on wide screens and the width is no longer capped; on phones the navigation is a menu button; the Advanced page uses panels; VLAN dropdowns show their chosen names; the duplicate WireGuard heading is gone. basic: a subtest for wan -> local rules. SPECS.md is now the only document (decisions, how each part works, and what is not done yet); PLAN.md is gone. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AeFqV9dZrK6nipHkPfyM4F
This commit is contained in:
@@ -13,9 +13,15 @@ import (
|
||||
// an option.
|
||||
|
||||
var (
|
||||
unitRe = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9@:._-]{0,79}$`)
|
||||
timeRe = regexp.MustCompile(`^[0-9A-Za-z :.+/_,-]{1,40}$`)
|
||||
prioRe = regexp.MustCompile(`^([0-7]|emerg|alert|crit|err|warning|notice|info|debug)$`)
|
||||
unitRe = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9@:._-]{0,79}$`)
|
||||
timeRe = regexp.MustCompile(`^[0-9A-Za-z :.+/_,-]{1,40}$`)
|
||||
prioRe = regexp.MustCompile(`^([0-7]|emerg|alert|crit|err|warning|notice|info|debug)$`)
|
||||
// The journal as a whole is not offered, only the units that run the router
|
||||
// (and the kernel): a read token should not read everything on the box.
|
||||
logUnits = []string{
|
||||
"router-*", "kea-dhcp4-server", "unbound", "systemd-networkd", "systemd-resolved",
|
||||
"systemd-timesyncd", "nftables", "ddclient", "acme-*", "sshd",
|
||||
}
|
||||
maxLogs = 1000
|
||||
logSize = 256 << 10
|
||||
)
|
||||
@@ -45,10 +51,13 @@ func journalArgs(q logQuery) ([]string, error) {
|
||||
switch {
|
||||
case q.Unit == "kernel":
|
||||
args = append(args, "-k")
|
||||
case q.Unit != "":
|
||||
if !unitRe.MatchString(q.Unit) {
|
||||
return nil, argError("unit: a unit name like router-webui or router-pppoe.service, or kernel")
|
||||
}
|
||||
case q.Unit == "":
|
||||
return nil, argError("unit is required: kernel or one of " + strings.Join(logUnits, ", "))
|
||||
case !unitRe.MatchString(q.Unit):
|
||||
return nil, argError("unit: a unit name like router-webui or router-pppoe.service, or kernel")
|
||||
case !logUnitAllowed(q.Unit):
|
||||
return nil, argError("unit " + q.Unit + " is not offered; use kernel or one of " + strings.Join(logUnits, ", "))
|
||||
default:
|
||||
args = append(args, "--unit="+q.Unit)
|
||||
}
|
||||
for _, t := range []struct{ flag, v string }{{"--since=", q.Since}, {"--until=", q.Until}} {
|
||||
@@ -81,13 +90,27 @@ func journalArgs(q logQuery) ([]string, error) {
|
||||
return args, nil
|
||||
}
|
||||
|
||||
func logUnitAllowed(unit string) bool {
|
||||
unit = strings.TrimSuffix(unit, ".service")
|
||||
for _, p := range logUnits {
|
||||
if prefix, ok := strings.CutSuffix(p, "*"); ok {
|
||||
if strings.HasPrefix(unit, prefix) {
|
||||
return true
|
||||
}
|
||||
} else if unit == p {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var logsTool = mcpTool{
|
||||
name: "get_logs", scope: scopeRead,
|
||||
description: "Lines of the system journal, oldest first (the last `lines` that match). Without `unit` all units. Useful units: router-pppoe (the PPPoE connection: connects and disconnects), router-webui, router-apply, systemd-networkd, kea-dhcp4-server, nftables, wireguard-*; `kernel` for the kernel log. Search a text with `grep`, narrow by `since` (\"1 hour ago\", \"2026-10-31 10:00:00\"), `priority` (err = errors and worse) or `boot` (0 this boot, -1 the previous).",
|
||||
description: "Lines of the system journal, oldest first (the last `lines` that match). The unit is required: `kernel` or one of router-* (router-pppoe is the PPPoE connection: connects and disconnects; router-webui, router-apply, router-update, ...), kea-dhcp4-server, unbound, systemd-networkd, systemd-resolved, systemd-timesyncd, nftables, ddclient, acme-*, sshd. Search a text with `grep`, narrow by `since` (\"1 hour ago\", \"2026-10-31 10:00:00\"), `priority` (err = errors and worse) or `boot` (0 this boot, -1 the previous).",
|
||||
schema: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"unit": map[string]any{"type": "string", "description": "A systemd unit (router-pppoe, router-webui.service, ...) or `kernel`."},
|
||||
"unit": map[string]any{"type": "string", "description": "Required: `kernel` or a unit: router-pppoe, router-webui.service, kea-dhcp4-server, unbound, systemd-networkd, nftables, ddclient, ..."},
|
||||
"lines": map[string]any{"type": "integer", "minimum": 1, "maximum": maxLogs, "default": 100, "description": "How many lines at most."},
|
||||
"since": map[string]any{"type": "string", "description": "Only entries since then: \"1 hour ago\", \"yesterday\", \"2026-10-31 10:00:00\"."},
|
||||
"until": map[string]any{"type": "string", "description": "Only entries until then."},
|
||||
@@ -95,6 +118,7 @@ var logsTool = mcpTool{
|
||||
"grep": map[string]any{"type": "string", "description": "Only entries matching this regular expression (case insensitive)."},
|
||||
"boot": map[string]any{"type": "integer", "maximum": 0, "description": "0 is the current boot, -1 the one before, ... Without it all boots the journal holds."},
|
||||
},
|
||||
"required": []string{"unit"},
|
||||
},
|
||||
run: func(a *app, raw json.RawMessage) (any, error) {
|
||||
var q logQuery
|
||||
|
||||
Reference in New Issue
Block a user