system_logs: first variant-based tool + OR match support

Adds system_logs with two variants:
- journalctl (systemd hosts): unit, since, lines, grep
- dmesg (any Linux): source, lines, grep

Both declare sudo: true — privilege escalation is handled by dispatch.

Also adds OR support in match conditions: array values mean any must
pass. Combined with AND across keys:
  {"binary": ["journalctl", "systemd-cat"], "os": "linux"}
  = (journalctl OR systemd-cat) AND linux
This commit is contained in:
Ollie Agent 2026-07-30 23:28:37 +02:00
parent 5c7df3a9fe
commit 5955562c1d
5 changed files with 164 additions and 41 deletions

View File

@ -0,0 +1,38 @@
{
"description": "Read system logs.",
"readOnly": true,
"tier": "cold",
"variants": [
{
"match": {"binary": "journalctl"},
"description": "Read system logs (journald).",
"prompt": "## system_logs\n\nRead systemd journal entries.\n\n**Args**: `lines` (default: 30), `unit`, `since`, `grep`\n\n```\nsystem_logs(unit=\"sshd\", since=\"1 hour ago\")\nsystem_logs(lines=\"50\", grep=\"error\")\n```",
"args": {
"type": "object",
"properties": {
"lines": {"type": "string", "description": "Number of lines (default: 30)"},
"unit": {"type": "string", "description": "Systemd unit name"},
"since": {"type": "string", "description": "Time filter (e.g. '1 hour ago', 'today')"},
"grep": {"type": "string", "description": "Filter output by pattern"}
}
},
"cmd": "system_logs_journald",
"sudo": true
},
{
"match": {"binary": "dmesg"},
"description": "Read system logs (dmesg + log files).",
"prompt": "## system_logs\n\nRead kernel ring buffer and log files.\n\n**Args**: `lines` (default: 30), `source` (dmesg or a log path), `grep`\n\n```\nsystem_logs(source=\"dmesg\", lines=\"50\")\nsystem_logs(source=\"/var/log/nginx/error.log\", grep=\"500\")\n```",
"args": {
"type": "object",
"properties": {
"lines": {"type": "string", "description": "Number of lines (default: 30)"},
"source": {"type": "string", "description": "Log source: 'dmesg' or absolute path (default: dmesg)"},
"grep": {"type": "string", "description": "Filter output by pattern"}
}
},
"cmd": "system_logs_dmesg",
"sudo": true
}
]
}

26
data/tools/system_logs_dmesg Executable file
View File

@ -0,0 +1,26 @@
#!/bin/sh
# system_logs_dmesg — read kernel ring buffer or log files
set -e
input=$(cat)
lines=$(echo "$input" | jq -r '.lines // "30"')
source=$(echo "$input" | jq -r '.source // "dmesg"')
grep_pat=$(echo "$input" | jq -r '.grep // empty')
if [ "$source" = "dmesg" ]; then
if [ -n "$grep_pat" ]; then
dmesg --time-format iso | tail -n "$lines" | grep -i "$grep_pat"
else
dmesg --time-format iso | tail -n "$lines"
fi
else
# Absolute path to a log file
if [ ! -f "$source" ]; then
echo "error: log file not found: $source" >&2
exit 1
fi
if [ -n "$grep_pat" ]; then
tail -n "$lines" "$source" | grep -i "$grep_pat"
else
tail -n "$lines" "$source"
fi
fi

15
data/tools/system_logs_journald Executable file
View File

@ -0,0 +1,15 @@
#!/bin/sh
# system_logs_journald — read systemd journal entries
set -e
input=$(cat)
lines=$(echo "$input" | jq -r '.lines // "30"')
unit=$(echo "$input" | jq -r '.unit // empty')
since=$(echo "$input" | jq -r '.since // empty')
grep_pat=$(echo "$input" | jq -r '.grep // empty')
args="--no-pager -n $lines --output=short-iso"
[ -n "$unit" ] && args="$args -u $unit"
[ -n "$since" ] && args="$args --since=$since"
[ -n "$grep_pat" ] && args="$args --grep=$grep_pat"
eval journalctl $args

View File

@ -17,6 +17,7 @@ type MetaFile struct {
Tier string `json:"tier,omitempty"`
ReadOnly bool `json:"readOnly,omitempty"`
Cmd string `json:"cmd,omitempty"`
Sudo bool `json:"sudo,omitempty"`
Variants []Variant `json:"variants,omitempty"`
}
@ -24,13 +25,14 @@ type MetaFile struct {
// match conditions are all satisfied determines the tool's schema,
// documentation, and executable.
type Variant struct {
Match map[string]string `json:"match"`
Description string `json:"description,omitempty"`
Prompt string `json:"prompt,omitempty"`
Args json.RawMessage `json:"args,omitempty"`
Tier string `json:"tier,omitempty"`
ReadOnly bool `json:"readOnly,omitempty"`
Cmd string `json:"cmd,omitempty"`
Match map[string]any `json:"match"`
Description string `json:"description,omitempty"`
Prompt string `json:"prompt,omitempty"`
Args json.RawMessage `json:"args,omitempty"`
Tier string `json:"tier,omitempty"`
ReadOnly bool `json:"readOnly,omitempty"`
Cmd string `json:"cmd,omitempty"`
Sudo bool `json:"sudo,omitempty"`
}
// LoadMetaFile reads and parses a .meta sidecar file for the given tool name.
@ -78,6 +80,9 @@ func (m *MetaFile) Resolve() *MetaFile {
if v.ReadOnly {
resolved.ReadOnly = true
}
if v.Sudo {
resolved.Sudo = true
}
if v.Cmd != "" {
resolved.Cmd = v.Cmd
}
@ -90,44 +95,66 @@ func (m *MetaFile) Resolve() *MetaFile {
// matchesMet returns true if all conditions in the match map are satisfied.
// Supported keys: binary, file, os, arch, env, nenv.
func matchesMet(m map[string]string) bool {
for key, val := range m {
switch key {
case "binary":
if filepath.IsAbs(val) {
if _, err := os.Stat(val); err != nil {
return false
}
} else {
if _, err := exec.LookPath(val); err != nil {
return false
}
}
case "file":
if _, err := os.Stat(val); err != nil {
return false
}
case "os":
if runtime.GOOS != val {
return false
}
case "arch":
if runtime.GOARCH != val {
return false
}
case "env":
if os.Getenv(val) == "" {
return false
}
case "nenv":
if os.Getenv(val) != "" {
return false
}
// Values can be a string (single check) or []string (any must pass = OR).
func matchesMet(m map[string]any) bool {
for key, raw := range m {
vals := toStrings(raw)
if !anyMatch(key, vals) {
return false
}
}
return true
}
func toStrings(v any) []string {
switch val := v.(type) {
case string:
return []string{val}
case []any:
out := make([]string, 0, len(val))
for _, item := range val {
if s, ok := item.(string); ok {
out = append(out, s)
}
}
return out
}
return nil
}
func anyMatch(key string, vals []string) bool {
for _, val := range vals {
if checkCondition(key, val) {
return true
}
}
return false
}
func checkCondition(key, val string) bool {
switch key {
case "binary":
if filepath.IsAbs(val) {
_, err := os.Stat(val)
return err == nil
}
_, err := exec.LookPath(val)
return err == nil
case "file":
_, err := os.Stat(val)
return err == nil
case "os":
return runtime.GOOS == val
case "arch":
return runtime.GOARCH == val
case "env":
return os.Getenv(val) != ""
case "nenv":
return os.Getenv(val) == ""
}
return false
}
// ToolInfoFromMeta builds a ToolInfo from a MetaFile.
// Applies variant resolution first. Returns zero ToolInfo if no variant matches.
func ToolInfoFromMeta(name string, m *MetaFile) ToolInfo {

View File

@ -319,12 +319,29 @@ func (e *Server) callPromotedTool(ctx context.Context, tool string, args json.Ra
}
}
// Check if tool requires sudo (from .meta).
needsSudo := false
if m, err := LoadMetaFile(tool); err == nil && m != nil {
resolved := m.Resolve()
if resolved != nil && resolved.Sudo {
needsSudo = true
elevated = true // sudo implies elevation
}
}
// The tool script receives JSON args on stdin.
code := path
stdinData := string(args)
var result string
if elevated {
if needsSudo {
e.wdMu.RLock()
workDir := e.cwd
e.wdMu.RUnlock()
// sudo -n: non-interactive (NOPASSWD). Full credential broker TBD.
sudoCode := fmt.Sprintf("cat <<'OLLIE_EOF' | sudo -n %s\n%s\nOLLIE_EOF", code, stdinData)
result, err = e.executeElevated(ctx, sudoCode, workDir, 30)
} else if elevated {
e.wdMu.RLock()
workDir := e.cwd
e.wdMu.RUnlock()