msg
This commit is contained in:
1 parent
81c7f99174
commit
e9946694e6
7 files changed
+1306
No files matched your search
@@ -0,0 +1,521 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (lib)
|
||||
mkEnableOption
|
||||
mkOption
|
||||
mkIf
|
||||
types
|
||||
;
|
||||
|
||||
cfg = config.services.ssh-mcp;
|
||||
hostConfig = config;
|
||||
|
||||
toml = pkgs.formats.toml { };
|
||||
|
||||
renderEnvValue =
|
||||
value:
|
||||
if builtins.isBool value then
|
||||
(if value then "true" else "false")
|
||||
else if builtins.isList value then
|
||||
lib.concatStringsSep "," value
|
||||
else
|
||||
toString value;
|
||||
|
||||
authType = types.submodule {
|
||||
options = {
|
||||
method = mkOption {
|
||||
type = types.enum [ "agent" "key" "password" ];
|
||||
default = "agent";
|
||||
description = "SSH authentication method.";
|
||||
};
|
||||
|
||||
key = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "Private key path for `method = \"key\"`.";
|
||||
};
|
||||
|
||||
identityFingerprint = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "Pin one agent identity by SHA-256 fingerprint.";
|
||||
};
|
||||
|
||||
identitiesOnly = mkOption {
|
||||
type = types.nullOr types.bool;
|
||||
default = null;
|
||||
description = "Offer only the pinned identity to the server.";
|
||||
};
|
||||
|
||||
identityAgent = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "SSH agent socket to use instead of SSH_AUTH_SOCK.";
|
||||
};
|
||||
|
||||
passphraseCmd = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "Local command that prints the key passphrase.";
|
||||
};
|
||||
|
||||
extraConfig = mkOption {
|
||||
type = types.attrsOf types.anything;
|
||||
default = { };
|
||||
description = "Escape hatch merged into this auth table.";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
defaultsType = types.submodule {
|
||||
options = {
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = "root";
|
||||
description = "Default SSH user.";
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 22;
|
||||
description = "Default SSH port.";
|
||||
};
|
||||
|
||||
platform = mkOption {
|
||||
type = types.str;
|
||||
default = "linux";
|
||||
description = "Default target platform.";
|
||||
};
|
||||
|
||||
persistentSession = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Allow persistent shell sessions by default.";
|
||||
};
|
||||
|
||||
pathAllowlist = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
description = "Default path roots. Empty means no paths are allowed.";
|
||||
};
|
||||
|
||||
restrictedPaths = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
description = "Default paths carved out of the allowlist.";
|
||||
};
|
||||
|
||||
commandAllowlist = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
description = "Default exec command allowlist.";
|
||||
};
|
||||
|
||||
auth = mkOption {
|
||||
type = authType;
|
||||
default = { };
|
||||
description = "Default SSH authentication policy.";
|
||||
};
|
||||
|
||||
extraConfig = mkOption {
|
||||
type = types.attrsOf types.anything;
|
||||
default = { };
|
||||
description = "Escape hatch merged into the `[defaults]` table.";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
hostType = types.submodule {
|
||||
options = {
|
||||
hostname = mkOption {
|
||||
type = types.str;
|
||||
description = "Canonical hostname or address used on the wire.";
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "Per-host SSH user override.";
|
||||
};
|
||||
|
||||
port = mkOption {
|
||||
type = types.nullOr types.port;
|
||||
default = null;
|
||||
description = "Per-host SSH port override.";
|
||||
};
|
||||
|
||||
platform = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "Per-host platform override.";
|
||||
};
|
||||
|
||||
persistentSession = mkOption {
|
||||
type = types.nullOr types.bool;
|
||||
default = null;
|
||||
description = "Per-host persistent-session override.";
|
||||
};
|
||||
|
||||
pathAllowlist = mkOption {
|
||||
type = types.nullOr (types.listOf types.str);
|
||||
default = null;
|
||||
description = "Per-host path roots.";
|
||||
};
|
||||
|
||||
restrictedPaths = mkOption {
|
||||
type = types.nullOr (types.listOf types.str);
|
||||
default = null;
|
||||
description = "Per-host paths carved out of the allowlist.";
|
||||
};
|
||||
|
||||
commandAllowlist = mkOption {
|
||||
type = types.nullOr (types.listOf types.str);
|
||||
default = null;
|
||||
description = "Per-host exec command allowlist.";
|
||||
};
|
||||
|
||||
auth = mkOption {
|
||||
type = types.nullOr authType;
|
||||
default = null;
|
||||
description = "Per-host authentication policy override.";
|
||||
};
|
||||
|
||||
extraConfig = mkOption {
|
||||
type = types.attrsOf types.anything;
|
||||
default = { };
|
||||
description = "Escape hatch merged into this host's table.";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
settingsType = types.submodule {
|
||||
options = {
|
||||
enabledGroups = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
description = "Tool groups to expose. Empty exposes all groups.";
|
||||
};
|
||||
|
||||
hostsAllowlist = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
description = "Additional hostnames allowed by the server.";
|
||||
};
|
||||
|
||||
hostsBlocklist = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
description = "Hostnames denied by the server. Deny wins.";
|
||||
};
|
||||
|
||||
pathAllowlist = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
description = "Server-wide path allowlist.";
|
||||
};
|
||||
|
||||
restrictedPaths = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
description = "Server-wide restricted paths.";
|
||||
};
|
||||
|
||||
commandAllowlist = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
description = "Server-wide exec command allowlist.";
|
||||
};
|
||||
|
||||
allowLowAccessTools = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Enable SFTP-mediated file mutation tools.";
|
||||
};
|
||||
|
||||
allowDangerousTools = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Enable arbitrary command execution tools.";
|
||||
};
|
||||
|
||||
allowSudo = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Enable sudo-tier tools. Also requires dangerous tools.";
|
||||
};
|
||||
|
||||
allowAnyCommand = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Permit arbitrary commands when no command allowlist is set.";
|
||||
};
|
||||
|
||||
allowPasswordAuth = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Permit password authentication.";
|
||||
};
|
||||
|
||||
pingIncludesNotes = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Include operator notes in host ping results.";
|
||||
};
|
||||
|
||||
pingIncludesAgentNotes = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Include agent notes in host ping results.";
|
||||
};
|
||||
|
||||
otelEnabled = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Enable OpenTelemetry export.";
|
||||
};
|
||||
|
||||
localTransferRoots = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
description = "Local paths the server may read for uploads and transfers.";
|
||||
};
|
||||
|
||||
dockerCmd = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "Docker-compatible CLI to use, e.g. `podman`.";
|
||||
};
|
||||
|
||||
configFile = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "Optional OpenSSH config file to fill in unset host fields.";
|
||||
};
|
||||
|
||||
extraEnv = mkOption {
|
||||
type = types.attrsOf types.str;
|
||||
default = { };
|
||||
description = "Extra environment variables for the ssh-mcp process.";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
renderAuth =
|
||||
auth:
|
||||
{ inherit (auth) method; }
|
||||
// lib.optionalAttrs (auth.key != null) { inherit (auth) key; }
|
||||
// lib.optionalAttrs (auth.identityFingerprint != null) {
|
||||
identity_fingerprint = auth.identityFingerprint;
|
||||
}
|
||||
// lib.optionalAttrs (auth.identitiesOnly != null) {
|
||||
identities_only = auth.identitiesOnly;
|
||||
}
|
||||
// lib.optionalAttrs (auth.identityAgent != null) {
|
||||
identity_agent = auth.identityAgent;
|
||||
}
|
||||
// lib.optionalAttrs (auth.passphraseCmd != null) {
|
||||
passphrase_cmd = auth.passphraseCmd;
|
||||
}
|
||||
// auth.extraConfig;
|
||||
|
||||
renderDefaults =
|
||||
defaults:
|
||||
{
|
||||
inherit (defaults) user port platform;
|
||||
persistent_session = defaults.persistentSession;
|
||||
path_allowlist = defaults.pathAllowlist;
|
||||
restricted_paths = defaults.restrictedPaths;
|
||||
command_allowlist = defaults.commandAllowlist;
|
||||
auth = renderAuth defaults.auth;
|
||||
}
|
||||
// defaults.extraConfig;
|
||||
|
||||
renderHost =
|
||||
host:
|
||||
{ inherit (host) hostname; }
|
||||
// lib.optionalAttrs (host.user != null) { inherit (host) user; }
|
||||
// lib.optionalAttrs (host.port != null) { inherit (host) port; }
|
||||
// lib.optionalAttrs (host.platform != null) { inherit (host) platform; }
|
||||
// lib.optionalAttrs (host.persistentSession != null) {
|
||||
persistent_session = host.persistentSession;
|
||||
}
|
||||
// lib.optionalAttrs (host.pathAllowlist != null) {
|
||||
path_allowlist = host.pathAllowlist;
|
||||
}
|
||||
// lib.optionalAttrs (host.restrictedPaths != null) {
|
||||
restricted_paths = host.restrictedPaths;
|
||||
}
|
||||
// lib.optionalAttrs (host.commandAllowlist != null) {
|
||||
command_allowlist = host.commandAllowlist;
|
||||
}
|
||||
// lib.optionalAttrs (host.auth != null) {
|
||||
auth = renderAuth host.auth;
|
||||
}
|
||||
// host.extraConfig;
|
||||
|
||||
renderInstanceSettings =
|
||||
instance:
|
||||
{
|
||||
ALLOW_LOW_ACCESS_TOOLS = renderEnvValue instance.settings.allowLowAccessTools;
|
||||
ALLOW_DANGEROUS_TOOLS = renderEnvValue instance.settings.allowDangerousTools;
|
||||
ALLOW_SUDO = renderEnvValue instance.settings.allowSudo;
|
||||
ALLOW_ANY_COMMAND = renderEnvValue instance.settings.allowAnyCommand;
|
||||
ALLOW_PASSWORD_AUTH = renderEnvValue instance.settings.allowPasswordAuth;
|
||||
SSH_PING_INCLUDES_NOTES = renderEnvValue instance.settings.pingIncludesNotes;
|
||||
SSH_PING_INCLUDES_AGENT_NOTES = renderEnvValue instance.settings.pingIncludesAgentNotes;
|
||||
OTEL_ENABLED = renderEnvValue instance.settings.otelEnabled;
|
||||
}
|
||||
// lib.optionalAttrs (instance.settings.enabledGroups != [ ]) {
|
||||
SSH_ENABLED_GROUPS = renderEnvValue instance.settings.enabledGroups;
|
||||
}
|
||||
// lib.optionalAttrs (instance.settings.hostsAllowlist != [ ]) {
|
||||
SSH_HOSTS_ALLOWLIST = renderEnvValue instance.settings.hostsAllowlist;
|
||||
}
|
||||
// lib.optionalAttrs (instance.settings.hostsBlocklist != [ ]) {
|
||||
SSH_HOSTS_BLOCKLIST = renderEnvValue instance.settings.hostsBlocklist;
|
||||
}
|
||||
// lib.optionalAttrs (instance.settings.pathAllowlist != [ ]) {
|
||||
SSH_PATH_ALLOWLIST = renderEnvValue instance.settings.pathAllowlist;
|
||||
}
|
||||
// lib.optionalAttrs (instance.settings.restrictedPaths != [ ]) {
|
||||
SSH_RESTRICTED_PATHS = renderEnvValue instance.settings.restrictedPaths;
|
||||
}
|
||||
// lib.optionalAttrs (instance.settings.commandAllowlist != [ ]) {
|
||||
SSH_COMMAND_ALLOWLIST = renderEnvValue instance.settings.commandAllowlist;
|
||||
}
|
||||
// lib.optionalAttrs (instance.settings.localTransferRoots != [ ]) {
|
||||
SSH_LOCAL_TRANSFER_ROOTS = renderEnvValue instance.settings.localTransferRoots;
|
||||
}
|
||||
// lib.optionalAttrs (instance.settings.dockerCmd != null) {
|
||||
SSH_DOCKER_CMD = instance.settings.dockerCmd;
|
||||
}
|
||||
// lib.optionalAttrs (instance.settings.configFile != null) {
|
||||
SSH_CONFIG_FILE = instance.settings.configFile;
|
||||
}
|
||||
// instance.settings.extraEnv;
|
||||
|
||||
instanceType = types.submodule (
|
||||
{ name, config, ... }:
|
||||
{
|
||||
options = {
|
||||
enable = mkEnableOption "the ssh-mcp instance ${name}";
|
||||
|
||||
package = mkOption {
|
||||
type = types.package;
|
||||
default = pkgs.python-ssh-mcp;
|
||||
description = "ssh-mcp package for this instance.";
|
||||
};
|
||||
|
||||
description = mkOption {
|
||||
type = types.str;
|
||||
default = "ssh-mcp instance ${name}";
|
||||
description = "Backend description used by `gatewayBackend`.";
|
||||
};
|
||||
|
||||
hostsFileName = mkOption {
|
||||
type = types.str;
|
||||
default = "hosts.toml";
|
||||
description = "Generated hosts policy filename under `~/.config/ssh-mcp/${name}`.";
|
||||
};
|
||||
|
||||
knownHostsFileName = mkOption {
|
||||
type = types.str;
|
||||
default = "known_hosts";
|
||||
description = "Generated known_hosts filename under `~/.config/ssh-mcp/${name}`.";
|
||||
};
|
||||
|
||||
defaults = mkOption {
|
||||
type = defaultsType;
|
||||
default = { };
|
||||
description = "Default host policy for this instance.";
|
||||
};
|
||||
|
||||
hosts = mkOption {
|
||||
type = types.attrsOf hostType;
|
||||
default = { };
|
||||
description = "Hosts reachable through this instance, keyed by alias.";
|
||||
};
|
||||
|
||||
knownHosts = mkOption {
|
||||
type = types.attrsOf types.str;
|
||||
default = { };
|
||||
description = ''
|
||||
Pinned host keys keyed by canonical hostname. Each value is the
|
||||
key type and public key, e.g. `ssh-ed25519 AAAA...`.
|
||||
'';
|
||||
};
|
||||
|
||||
settings = mkOption {
|
||||
type = settingsType;
|
||||
default = { };
|
||||
description = "ssh-mcp server-wide environment settings.";
|
||||
};
|
||||
|
||||
hostsFile = mkOption {
|
||||
type = types.str;
|
||||
readOnly = true;
|
||||
default = "${hostConfig.xdg.configHome}/ssh-mcp/${name}/${config.hostsFileName}";
|
||||
description = "Runtime path of the generated hosts policy.";
|
||||
};
|
||||
|
||||
knownHostsFile = mkOption {
|
||||
type = types.str;
|
||||
readOnly = true;
|
||||
default = "${hostConfig.xdg.configHome}/ssh-mcp/${name}/${config.knownHostsFileName}";
|
||||
description = "Runtime path of the generated known_hosts file.";
|
||||
};
|
||||
|
||||
gatewayBackend = mkOption {
|
||||
type = types.attrsOf types.anything;
|
||||
readOnly = true;
|
||||
default = {
|
||||
command = "${config.package}/bin/ssh-mcp";
|
||||
inherit (config) description;
|
||||
env = {
|
||||
SSH_HOSTS_FILE = config.hostsFile;
|
||||
SSH_KNOWN_HOSTS = config.knownHostsFile;
|
||||
}
|
||||
// renderInstanceSettings config;
|
||||
};
|
||||
description = "Backend attrset ready for `services.mcp-gateway.backends`.";
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
in
|
||||
{
|
||||
options.services.ssh-mcp = {
|
||||
instances = mkOption {
|
||||
type = types.attrsOf instanceType;
|
||||
default = { };
|
||||
description = "Independent ssh-mcp server instances.";
|
||||
};
|
||||
};
|
||||
|
||||
config = {
|
||||
xdg.configFile = lib.mkMerge (
|
||||
lib.mapAttrsToList (
|
||||
name: instance:
|
||||
mkIf instance.enable {
|
||||
"ssh-mcp/${name}/${instance.hostsFileName}".source = toml.generate "ssh-mcp-${name}-hosts.toml" {
|
||||
defaults = renderDefaults instance.defaults;
|
||||
hosts = lib.mapAttrs (_host: host: renderHost host) instance.hosts;
|
||||
};
|
||||
|
||||
"ssh-mcp/${name}/${instance.knownHostsFileName}".text =
|
||||
lib.optionalString (instance.knownHosts != { }) (
|
||||
lib.concatStringsSep "\n" (
|
||||
lib.mapAttrsToList (host: key: "${host} ${key}") instance.knownHosts
|
||||
)
|
||||
+ "\n"
|
||||
);
|
||||
}
|
||||
) cfg.instances
|
||||
);
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user