Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ jobs:
- 'packages/messaging/**'
- 'packages/github-app-auth/**'
- 'tools/github/**'
ssh-tool:
- 'package.json'
- 'package-lock.json'
- 'packages/messaging/**'
- 'tools/ssh/**'
core-controller:
- 'controllers/core-controller/**'
localtool-executor:
Expand All @@ -125,6 +130,7 @@ jobs:
KUBECTL_READONLY: ${{ steps.filter.outputs.kubectl-readonly }}
SIGNOZ_QUERY: ${{ steps.filter.outputs.signoz-query }}
GITHUB_TOOL: ${{ steps.filter.outputs.github-tool }}
SSH_TOOL: ${{ steps.filter.outputs.ssh-tool }}
CORE_CONTROLLER: ${{ steps.filter.outputs.core-controller }}
LOCALTOOL_EXECUTOR: ${{ steps.filter.outputs.localtool-executor }}
run: |
Expand All @@ -141,6 +147,7 @@ jobs:
{"image":"kubectl-readonly","changed_key":"KUBECTL_READONLY","dockerfile":"tools/kubectl-readonly/Dockerfile","context":"."},
{"image":"signoz-query","changed_key":"SIGNOZ_QUERY","dockerfile":"tools/signoz-query/Dockerfile","context":"."},
{"image":"github","changed_key":"GITHUB_TOOL","dockerfile":"tools/github/Dockerfile","context":"."},
{"image":"ssh","changed_key":"SSH_TOOL","dockerfile":"tools/ssh/Dockerfile","context":"."},
{"image":"core-controller","changed_key":"CORE_CONTROLLER","dockerfile":"controllers/core-controller/Dockerfile","context":"controllers/core-controller"},
{"image":"localtool-executor-node","changed_key":"LOCALTOOL_EXECUTOR","dockerfile":"sidecars/localtool-executor/Dockerfile","context":"sidecars/localtool-executor","build_args":"BASE_IMAGE=node:24-bookworm-slim\nRUNTIME=node"},
{"image":"localtool-executor-python","changed_key":"LOCALTOOL_EXECUTOR","dockerfile":"sidecars/localtool-executor/Dockerfile","context":"sidecars/localtool-executor","build_args":"BASE_IMAGE=python:3.12-slim-bookworm\nRUNTIME=python"},
Expand All @@ -167,9 +174,10 @@ jobs:
--arg KUBECTL_READONLY "$KUBECTL_READONLY" \
--arg SIGNOZ_QUERY "$SIGNOZ_QUERY" \
--arg GITHUB_TOOL "$GITHUB_TOOL" \
--arg SSH_TOOL "$SSH_TOOL" \
--arg CORE_CONTROLLER "$CORE_CONTROLLER" \
--arg LOCALTOOL_EXECUTOR "$LOCALTOOL_EXECUTOR" \
'{AGENT_ORCHESTRATOR:$AGENT_ORCHESTRATOR,OPENCODE_SWE_AGENT:$OPENCODE_SWE_AGENT,CLAUDE_CODE_SWE_AGENT:$CLAUDE_CODE_SWE_AGENT,INTEGRATION_GATEWAY:$INTEGRATION_GATEWAY,RECIPE_SCRAPER:$RECIPE_SCRAPER,RECIPE_PUBLISHER:$RECIPE_PUBLISHER,WEB_SEARCH:$WEB_SEARCH,WEB_FETCH:$WEB_FETCH,KUBECTL_READONLY:$KUBECTL_READONLY,SIGNOZ_QUERY:$SIGNOZ_QUERY,GITHUB_TOOL:$GITHUB_TOOL,CORE_CONTROLLER:$CORE_CONTROLLER,LOCALTOOL_EXECUTOR:$LOCALTOOL_EXECUTOR}')
'{AGENT_ORCHESTRATOR:$AGENT_ORCHESTRATOR,OPENCODE_SWE_AGENT:$OPENCODE_SWE_AGENT,CLAUDE_CODE_SWE_AGENT:$CLAUDE_CODE_SWE_AGENT,INTEGRATION_GATEWAY:$INTEGRATION_GATEWAY,RECIPE_SCRAPER:$RECIPE_SCRAPER,RECIPE_PUBLISHER:$RECIPE_PUBLISHER,WEB_SEARCH:$WEB_SEARCH,WEB_FETCH:$WEB_FETCH,KUBECTL_READONLY:$KUBECTL_READONLY,SIGNOZ_QUERY:$SIGNOZ_QUERY,GITHUB_TOOL:$GITHUB_TOOL,SSH_TOOL:$SSH_TOOL,CORE_CONTROLLER:$CORE_CONTROLLER,LOCALTOOL_EXECUTOR:$LOCALTOOL_EXECUTOR}')
MATRIX=$(jq -c --argjson flags "$FLAGS" 'map(select($flags[.changed_key] == "true"))' all.json)
fi

Expand Down
18 changes: 18 additions & 0 deletions charts/community-components/templates/serviceaccount-ssh.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{{- if and .Values.sshTool.enabled .Values.sshTool.serviceAccount.create }}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ .Values.sshTool.serviceAccountName }}
namespace: {{ .Release.Namespace }}
labels:
{{- include "tools.labels" . | nindent 4 }}
{{- with .Values.sshTool.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 2 }}
{{- end }}
automountServiceAccountToken: {{ .Values.sshTool.serviceAccount.automount }}
{{- end }}
80 changes: 80 additions & 0 deletions charts/community-components/templates/tool-ssh.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
{{- if .Values.sshTool.enabled }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR turns CI red. .github/workflows/validate-crds.yml renders the chart with values-ci-all.yaml and asserts every file under templates/ appears in the output. tool-ssh.yaml and serviceaccount-ssh.yaml render nothing there because sshTool is disabled by default and values-ci-all.yaml was not updated to enable it, so the "Assert every template was actually rendered" step exits 1 (this is the failing validate check on the PR).

🤖 Prompt to fix this with an AI agent
In imaustink/agent-controller on PR #192, the validate-crds CI job fails because charts/community-components/templates/tool-ssh.yaml and serviceaccount-ssh.yaml render nothing under charts/community-components/values-ci-all.yaml. Add a `sshTool` block to values-ci-all.yaml with `enabled: true` and a non-empty `sshConfig` (a throwaway Host block, e.g. HostName ci-host.example.invalid / User ci) so the tool-ssh.yaml `fail` guard passes and both templates render. Verify `helm template charts/community-components -f charts/community-components/values-ci-all.yaml` includes both `# Source:` lines. Do not change unrelated behavior.

{{- if and (not .Values.sshTool.allowedHosts) (not .Values.sshTool.sshConfig) }}
{{- fail "sshTool requires at least one of allowedHosts or sshConfig -- with neither set this tool would have no boundary on which target it dials" }}
{{- end }}
{{- $wideOpen := eq .Values.sshTool.allowedCommands "*" }}
apiVersion: {{ .Values.crdApiVersion }}
kind: Tool
metadata:
name: ssh
labels:
{{- include "tools.labels" . | nindent 4 }}
spec:
description: >-
{{- if $wideOpen }}
Runs a single command over SSH against a target resolved via an optional
ssh_config-style Host list and/or restricted to a fixed allowlist, for
managing infrastructure outside the cluster. allowedCommands is "*" for
this deployment -- there is NO command allowlist here beyond the
remote-shell-injection charset check, so this Tool can write, delete,
and restart services on any target it can reach, not just diagnose them.
{{- else }}
Runs a single read-only diagnostic command (df, ps, uptime, systemctl
status, docker ps/logs/inspect, journalctl, ...) over SSH against a
target resolved via an optional ssh_config-style Host list and/or
restricted to a fixed allowlist, for debugging infrastructure outside
the cluster. Strictly read-only: an in-tool command allowlist blocks any
write/restart/delete action or interactive shell.
{{- end }}
input: >-
A single "<target> <command> [args...]" line, where <target> is either
"user@host[:port]" or an alias resolved via the configured ssh_config
Host list (e.g. "nas.kurpuis.internal df -h", "kube0 systemctl status
docker").
{{- if $wideOpen }}
Any remote command is accepted (no command allowlist in this
deployment); if a target allowlist is configured the resolved target
must still match it.
{{- else }}
Only a fixed set of read-only diagnostic commands are accepted, and if a
target allowlist is configured the resolved target must match it;
anything else is rejected before it reaches the target.
{{- end }}
output: >-
The remote command's own stdout, wrapped in a fenced code block.
allowedRoles:
{{- if $wideOpen }}
- writer
{{- else }}
- reader
{{- end }}
tier: standard
image: {{ .Values.sshTool.image | quote }}
serviceAccountName: {{ .Values.sshTool.serviceAccountName | quote }}
env:
{{- if .Values.sshTool.allowedHosts }}
- name: SSH_ALLOWED_HOSTS
value: {{ .Values.sshTool.allowedHosts | quote }}
{{- end }}
{{- if .Values.sshTool.sshConfig }}
- name: SSH_CONFIG
value: {{ .Values.sshTool.sshConfig | quote }}
{{- end }}
{{- if .Values.sshTool.defaultUser }}
- name: SSH_DEFAULT_USER
value: {{ .Values.sshTool.defaultUser | quote }}
{{- end }}
{{- if .Values.sshTool.allowedCommands }}
- name: SSH_ALLOWED_COMMANDS
value: {{ .Values.sshTool.allowedCommands | quote }}
{{- end }}
secretEnv:
- name: SSH_PRIVATE_KEY
secretRef:
name: {{ .Values.sshTool.secretName | quote }}
key: {{ .Values.sshTool.privateKeySecretKey | quote }}
- name: SSH_KNOWN_HOSTS
secretRef:
name: {{ .Values.sshTool.secretName | quote }}
key: {{ .Values.sshTool.knownHostsSecretKey | quote }}
{{- end }}
9 changes: 9 additions & 0 deletions charts/community-components/values-ci-all.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@ kubectlReadonly:
signozQuery:
enabled: true

# Enabled with a throwaway sshConfig so the fail guard (at least one of
# allowedHosts/sshConfig required) passes and both templates render.
sshTool:
enabled: true
sshConfig: |
Host ci-host
HostName ci-host.example.invalid
User ci

recipePublisher:
enabled: true
# Required by the template when enabled; never reached, the render is discarded.
Expand Down
92 changes: 92 additions & 0 deletions charts/community-components/values-production.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,98 @@ githubTool:
providers:
- github

# ssh: command execution over SSH, resolved via a copy of the operator's own
# ~/.ssh/config Host aliases (home/bastion/console, printcam/airvinyl/
# airbuddy -- deliberately excludes the Verizon/client boxes and "parents"
# also in that file). kube0-8 and db1-3 were deliberately dropped even
# though they're read-only-safe candidates: allowedCommands: "*" below
# means this tool can now WRITE on anything it reaches, and those are the
# actual cluster/database nodes this repo runs on -- too much blast radius
# for "*" for now. Re-add them (and their allowedHosts entries) once a
# dedicated, less-privileged key backs this tool instead of the operator's
# own id_rsa, or if allowedCommands is narrowed back down from "*".
#
# allowedCommands is "*" -- WIDE OPEN, no command allowlist at all beyond
# the remote-shell-injection charset check (tools/ssh/src/allowlist.ts).
# This deliberately trades the tool's default read-only-diagnostics posture
# for read/write capability (restart services, edit/delete files, ...) on
# every host below, per explicit operator request: accepted as tolerable
# risk against homelab boxes the operator doesn't mind breaking for now.
# Don't copy "*" into a values file for infrastructure that matters more --
# use a custom comma-separated command list instead. The only remaining
# boundaries here are allowedHosts below and whatever the operator's own
# id_rsa can already do on each target.
#
# allowedHosts is set below, DERIVED from the sshConfig block's own Host
# list -- it is NOT a redundant second allowlist. resolveTarget
# (tools/ssh/src/target.ts) falls back to any literal "user@host" when a
# caller's target string doesn't match an sshConfig alias, so with
# allowedHosts empty sshConfig's Host list restricts nothing at all: a
# caller could dial "root@192.168.1.71" directly (a different user than the
# admin this file maps console to) or any other host reachable with this key.
# Populating allowedHosts with exactly the resolved user@host:port pairs
# below is what actually turns "the aliases we curated" into "the only
# targets this tool can reach."
#
# TEMPORARY CREDENTIAL: this tool's SSH_PRIVATE_KEY is currently the
# operator's own personal id_rsa (full interactive/admin access to every
# host below), not a dedicated scoped key -- see tools/ssh/README.md's
# "Choosing a credential" section for why that's a real blast-radius
# tradeoff. Rotate to a dedicated keypair + restricted authorized_keys entry
# per tools/ssh/README.md, then update SSH_PRIVATE_KEY in the Secret below
# and delete this note. id_rsa is already authorized on home/bastion/
# printcam/airvinyl/airbuddy for the operator's own interactive access, so
# no new key install is needed for those.
#
# console is the EXCEPTION: `ssh -i ~/.ssh/id_rsa admin@192.168.1.71`
# returns "Permission denied (publickey)" -- id_rsa is NOT in admin's
# authorized_keys on this box the way it is on the others. Until that's
# fixed (e.g. `ssh-copy-id -i ~/.ssh/id_rsa.pub admin@192.168.1.71`, using
# whatever credential admin@console does currently accept), every ssh tool
# call against the "console" alias will fail at the auth step, not the
# allowlist -- this is a real gap, not a formality.
#
# console's IP also changed from 192.168.1.83 to 192.168.1.71 (DHCP lease
# churn -- same box, confirmed by keyscanning both the ed25519, rsa, and
# ecdsa host keys at the new address and matching them against the ones
# already on file for it under its console.local mDNS name). Its host key
# is now included in the keyscan below.
#
# Prerequisite:
# kubectl create secret generic ssh-tool-secrets -n controller-agent \
# --from-file=SSH_PRIVATE_KEY=~/.ssh/id_rsa \
# --from-literal=SSH_KNOWN_HOSTS="$(ssh-keyscan -t ed25519 \
# 166.113.38.187 192.168.1.43 192.168.1.71 \
# 192.168.1.235 airvinyl.local 192.168.1.218)"
sshTool:
enabled: true
image: registry.kurpuis.com:5000/ssh:latest
serviceAccountName: ssh-tool
allowedHosts: "austinkurpuis@166.113.38.187,austinkurpuis@192.168.1.43,admin@192.168.1.71,pi@192.168.1.235,pi@airvinyl.local,pi@192.168.1.218"
allowedCommands: "*"
sshConfig: |
Host home
HostName 166.113.38.187
User austinkurpuis
Host bastion
HostName 192.168.1.43
User austinkurpuis
Host console
HostName 192.168.1.71
User admin
Host printcam
HostName 192.168.1.235
User pi
Host airvinyl
HostName airvinyl.local
User pi
Host airbuddy
HostName 192.168.1.218
User pi
secretName: ssh-tool-secrets
privateKeySecretKey: SSH_PRIVATE_KEY
knownHostsSecretKey: SSH_KNOWN_HOSTS

skills:
recipeRefining:
enabled: true
Expand Down
62 changes: 62 additions & 0 deletions charts/community-components/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,68 @@ githubTool:
providers:
- github

# ssh: runs a single allowlisted read-only diagnostic command (df/ps/
# systemctl status/docker ps/journalctl/...) over SSH against a fixed set of
# allowlisted hosts, authenticated with one shared, operator-provisioned key
# -- there is no per-user SSH identity model in this repo (contrast
# githubTool's identityLink above). See tools/ssh/README.md for the full
# defense-in-depth model (host allowlist, command allowlist, plain-argument
# charset, pinned host keys) before enabling this against real
# infrastructure.
sshTool:
enabled: false
image: ssh:latest
serviceAccountName: ssh-tool
# Creates the ServiceAccount named above. Set to false and pre-create it
# yourself if you need annotations managed elsewhere (e.g. IRSA) that this
# chart doesn't own.
serviceAccount:
create: true
annotations: {}
automount: true
# Two INDEPENDENT, individually optional features for resolving/restricting
# a caller-supplied target -- at least one must be set or the Tool render
# fails (see templates/tool-ssh.yaml's `fail` guard):
#
# allowedHosts: the authorization boundary -- comma-separated
# "user@host[:port]" entries with no wildcard. A resolved target must
# match this list when it's set; when it's empty/unset, ANY target
# that resolves (via sshConfig below, or a caller's own literal
# "user@host") is permitted -- only safe if sshConfig's own Host list
# is itself a closed, trusted set.
# sshConfig: ssh_config(5)-shaped content (Host/HostName/User/Port
# blocks only -- every other directive is ignored, see
# tools/ssh/src/sshconfig.ts) for resolving an alias like "kube0" the
# same way the operator's own ~/.ssh/config already does. Independent
# of allowedHosts -- can be used alone (trusting this file's own
# curated Host list as the boundary) or together with it (aliases
# resolve here, then the resolved user@host:port must still match
# allowedHosts).
allowedHosts: ""
sshConfig: ""
# Fallback user when a target supplies none and no sshConfig Host block
# sets one either. Optional -- resolution fails closed without a user.
defaultUser: ""
# Which top-level remote commands are accepted. Empty/unset here means
# "use the tool's own built-in default" -- a curated read-only diagnostic
# set (df/ps/systemctl status/docker ps/journalctl/...), with the
# allowedRoles/description below adjusted to match. Set to "*" to disable
# the command allowlist entirely for this deployment -- this Tool can then
# write, delete, and restart services on any target it can reach, gated
# only by target resolution (allowedHosts/sshConfig above) and the
# remote-shell-injection charset check (tools/ssh/src/allowlist.ts), which
# always applies regardless of this setting. Only use "*" against
# infrastructure you're comfortable an agent could break.
allowedCommands: ""
# Secret (must already exist) supplying the shared private key and pinned
# known_hosts content:
# kubectl create secret generic ssh-tool-secrets -n <ns> \
# --from-file=SSH_PRIVATE_KEY=./id_ed25519_monitor \
# --from-literal=SSH_KNOWN_HOSTS="$(ssh-keyscan nas.kurpuis.internal)"
secretName: ssh-tool-secrets
privateKeySecretKey: SSH_PRIVATE_KEY
knownHostsSecretKey: SSH_KNOWN_HOSTS

skills:
# recipe-refining: extract -> confirm -> publish -> refine (recipe-scraper +
# recipe-publisher). Enable both those tools above for its derived audience
Expand Down
22 changes: 22 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading