@zicolasjac-ai/remote-readonly-ssh 1.0.4 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,14 +1,15 @@
1
1
  # rrs
2
2
 
3
- 只读 SSH 运维 CLI —— 通过堡垒机(JumpServer 式菜单)在服务器上执行**白名单强制的只读查询**。设计上零写能力。
3
+ **CLI and Agent Skill for safe read-only SSH inspection via JumpServer.**
4
4
 
5
- 把"只读"从行为自律变成**技术强制**,适合 AI 助手、脚本、团队成员以最小权限做服务器排查。
5
+ 让 AI 助手(opencode / Claude Code / Cursor / Windsurf / Codex 等)通过堡垒机白名单强制地在服务器上做只读查询——技术上零写能力,每次调用(含被拒绝的)写入本地审计日志。
6
6
 
7
7
  ## 为什么需要它
8
8
 
9
9
  - **技术强制只读**:命令前缀白名单 + 禁止模式 + 参数防注入 + 目标主机白名单,多层校验。
10
10
  - **审计留痕**:每次调用(含被拒绝的记录)写入本地审计日志,可随时审查。
11
11
  - **零依赖**:只依赖系统自带 `ssh` 客户端,无 npm 依赖树。
12
+ - **AI Agent 友好**:附带 SKILL.md 可一键安装到 opencode / Claude / Cursor / Windsurf / Codex / OpenHands 等 AI 助手的 skills 目录。
12
13
 
13
14
  ## 安装
14
15
 
@@ -23,10 +24,12 @@ npm install -g @zicolasjac-ai/remote-readonly-ssh
23
24
  ## 快速开始
24
25
 
25
26
  ```bash
26
- rrs init # 生成配置模板 ~/.rrs/config.json
27
+ npm install -g @zicolasjac-ai/remote-readonly-ssh
28
+ rrs init # 生成配置模板 ~/.rrs/config.json
29
+ rrs setup skills # 把 SKILL.md 安装到 opencode / claude / agents 等目录
27
30
  # 编辑配置: 填 jump_server / private_key_path / target_hosts
28
- rrs list-tools # 查看白名单
29
- rrs sys-info
31
+ rrs list-tools
32
+ rrs sys-info --json
30
33
  ```
31
34
 
32
35
  ## 用法
@@ -67,6 +70,26 @@ rrs <tool> [arg] [--host <ip>] [--config <file>] [--json]
67
70
 
68
71
  审计日志:`~/.rrs/audit.log`
69
72
 
73
+ ## Agent Skills
74
+
75
+ 本工具附带一份 `SKILL.md`,教 AI 助手何时用、怎么用 `rrs`。安装后用 `rrs setup skills` 一键分发到各 agent 的 skills 目录:
76
+
77
+ | `--agent` | 写入路径 |
78
+ |-----------|----------|
79
+ | `opencode` | `~/.config/opencode/skills/rrs/SKILL.md` |
80
+ | `claude` / `cursor` / `windsurf` / `codex` | `~/.claude/skills/rrs/SKILL.md`(Claude skills 兼容) |
81
+ | `agents` / `openhands` | `~/.agents/skills/rrs/SKILL.md` |
82
+ | `all`(默认) | 上述三类目录都写 |
83
+
84
+ ```bash
85
+ rrs setup skills # 安装到全部三类目录
86
+ rrs setup skills --agent opencode # 只写 opencode
87
+ rrs setup skills --dry-run # 仅预览目标路径
88
+ rrs setup uninstall skills # 从全部目录移除
89
+ ```
90
+
91
+ 卸载 npm 包时会自动尝试清理这些 skill 文件(`preuninstall` 钩子,失败不影响卸载)。
92
+
70
93
  ## 开发
71
94
 
72
95
  ```bash
package/bin/rrs.js CHANGED
@@ -8,6 +8,7 @@ const { JumpShell, cleanAnsi } = require("../src/jumpshell");
8
8
  const {
9
9
  validateCmd, validatePath, validateHost, validateArg, validateBin,
10
10
  } = require("../src/whitelist");
11
+ const setupSkill = require("../src/setup");
11
12
 
12
13
  const PACKAGE_ROOT = path.join(__dirname, "..");
13
14
 
@@ -22,32 +23,83 @@ function printUsage() {
22
23
  ["dir <path>", "列目录(路径白名单)"],
23
24
  ["bin-version <bin>", "查二进制版本(二进制白名单)"],
24
25
  ["exec-only <cmd>", "通用只读命令(前缀白名单+禁止模式)"],
26
+ ["setup skills [--agent <n>]", "把 SKILL.md 安装到 opencode/claude/agents 等目录"],
27
+ ["setup uninstall skills [--agent <n>]", "从上述目录移除 skill"],
25
28
  ];
26
- console.log("rrs — 只读 SSH 运维 CLI(白名单强制,零写能力)\n");
27
- console.log("用法: rrs <工具> [参数] [--host <ip>] [--config <file>] [--json]\n");
29
+ console.log("rrs — CLI and Agent Skill for safe read-only SSH inspection\n");
30
+ console.log("用法: rrs <工具> [参数] [--host <ip>] [--config <file>] [--json]");
31
+ console.log(" rrs setup skills [--agent opencode|claude|cursor|windsurf|codex|openhands|agents|all] [--dry-run]\n");
28
32
  console.log("选项: --json 以 JSON 输出结果(便于脚本/AI 调用); -v/--version 查看版本\n");
29
33
  for (const [k, v] of cmds) {
30
- console.log(` ${k.padEnd(22)} ${v}`);
34
+ console.log(` ${k.padEnd(40)} ${v}`);
31
35
  }
32
36
  }
33
37
 
34
38
  function parseArgs(argv) {
35
- const opts = { tool: null, arg: null, host: null, config: null, version: false, json: false };
39
+ const opts = {
40
+ mode: "tool",
41
+ tool: null,
42
+ arg: null,
43
+ setupCmd: null,
44
+ agent: "all",
45
+ dryRun: false,
46
+ host: null,
47
+ config: null,
48
+ version: false,
49
+ json: false,
50
+ };
36
51
  const rest = [];
37
52
  for (let i = 0; i < argv.length; i++) {
38
53
  const a = argv[i];
39
54
  if (a === "--host") opts.host = argv[++i];
40
55
  else if (a === "--config") opts.config = argv[++i];
41
- else if (a === "-h" || a === "--help") { opts.tool = "help"; }
56
+ else if (a === "--agent") opts.agent = argv[++i];
57
+ else if (a === "--dry-run") opts.dryRun = true;
58
+ else if (a === "-h" || a === "--help") { opts.mode = "tool"; opts.tool = "help"; }
42
59
  else if (a === "-v" || a === "--version") opts.version = true;
43
60
  else if (a === "--json") opts.json = true;
44
61
  else rest.push(a);
45
62
  }
63
+ if (rest[0] === "setup") {
64
+ opts.mode = "setup";
65
+ if (rest[1] === "uninstall") {
66
+ opts.setupCmd = "uninstall";
67
+ if (rest[2] === "skills") opts.setupTarget = "skills";
68
+ } else if (rest[1] === "skills") {
69
+ opts.setupCmd = "install";
70
+ opts.setupTarget = "skills";
71
+ }
72
+ return opts;
73
+ }
46
74
  opts.tool = opts.tool || rest[0] || "help";
47
75
  opts.arg = rest[1] || null;
48
76
  return opts;
49
77
  }
50
78
 
79
+ function runSetup(opts) {
80
+ const agent = opts.agent || "all";
81
+ if (!opts.setupTarget || opts.setupTarget !== "skills") {
82
+ console.error("用法: rrs setup skills [--agent all|opencode|claude|cursor|windsurf|codex|openhands|agents] [--dry-run]");
83
+ console.error(" rrs setup uninstall skills [--agent ...] [--dry-run]");
84
+ process.exit(1);
85
+ }
86
+ let results;
87
+ try {
88
+ results = opts.setupCmd === "uninstall"
89
+ ? setupSkill.uninstall({ agent, dryRun: opts.dryRun })
90
+ : setupSkill.install(PACKAGE_ROOT, { agent, dryRun: opts.dryRun });
91
+ } catch (e) {
92
+ console.error(`[setup] 错误: ${e.message}`);
93
+ process.exit(1);
94
+ }
95
+ const verb = opts.setupCmd === "uninstall" ? "卸载" : "安装";
96
+ console.log(`${verb} skill (agent=${agent}):`);
97
+ for (const r of results) {
98
+ const status = r.action.startsWith("would-") ? "将" : "已";
99
+ console.log(` [${r.agent}] ${status}${r.action.replace("would-", "")} ${r.dir}`);
100
+ }
101
+ }
102
+
51
103
  function initConfig() {
52
104
  const dir = configDir();
53
105
  fs.mkdirSync(dir, { recursive: true });
@@ -121,6 +173,7 @@ async function main() {
121
173
  console.log(require(path.join(PACKAGE_ROOT, "package.json")).version);
122
174
  return;
123
175
  }
176
+ if (opts.mode === "setup") { runSetup(opts); return; }
124
177
  if (opts.tool === "help") { printUsage(); return; }
125
178
  if (opts.tool === "init") { initConfig(); return; }
126
179
 
@@ -165,4 +218,4 @@ if (require.main === module) {
165
218
  main();
166
219
  }
167
220
 
168
- module.exports = { parseArgs, buildCmds, printUsage };
221
+ module.exports = { parseArgs, buildCmds, printUsage, runSetup };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@zicolasjac-ai/remote-readonly-ssh",
3
- "version": "1.0.4",
4
- "description": "只读 SSH 运维 CLI:基于白名单强制校验,通过堡垒机对服务器执行只读查询,设计上零写权限。",
3
+ "version": "1.1.1",
4
+ "description": "只读 SSH 运维 CLI 与 Agent Skill:通过 JumpServer 堡垒机白名单强制执行只读查询;附 SKILL.md 让 opencode / Claude Code / Cursor 等 AI 助手自动发现并正确调用。",
5
5
  "license": "MIT",
6
6
  "bin": {
7
7
  "rrs": "bin/rrs.js"
@@ -10,6 +10,7 @@
10
10
  "files": [
11
11
  "bin/",
12
12
  "src/",
13
+ "skills/",
13
14
  "config.example.json",
14
15
  "README.md",
15
16
  "LICENSE"
@@ -26,10 +27,15 @@
26
27
  "whitelist",
27
28
  "audit",
28
29
  "security",
29
- "cli"
30
+ "cli",
31
+ "agent-skill",
32
+ "opencode",
33
+ "claude",
34
+ "ai"
30
35
  ],
31
36
  "dependencies": {},
32
37
  "scripts": {
33
- "test": "node --test"
38
+ "test": "node --test",
39
+ "preuninstall": "node -e \"try { require('./src/setup').uninstall({agent:'all'}) } catch(_) {}\""
34
40
  }
35
41
  }
@@ -0,0 +1,115 @@
1
+ ---
2
+ name: rrs
3
+ description: 通过 JumpServer 堡垒机对服务器做白名单强制的只读运维查询(sys-info / proc / ports / dir / read-file / bin-version / exec-only)。技术强制只读、命令前缀白名单 + 禁止模式 + 路径白名单 + 主机白名单多层校验,每次调用(含被拒绝的)都写入本地审计日志。AI Agent 需要做"查看服务器现状"这类操作时调用此 skill;它不会写入任何远程状态。
4
+ license: MIT
5
+ compatibility: opencode, claude-code, cursor, windsurf, openhands, codex
6
+ metadata:
7
+ audience: ai-agents
8
+ category: ops
9
+ safe: read-only
10
+ ---
11
+
12
+ # rrs — Read-Only SSH Inspection Skill
13
+
14
+ `rrs`(remote-read-only-ssh)是一个面向 AI Agent 的只读 SSH 运维工具。它通过 JumpServer 风格的堡垒机在白名单目标主机上执行**白名单强制**的只读查询。每次调用(含被拒绝的)都写入本地审计日志。
15
+
16
+ ## When to use me
17
+
18
+ 当你需要从服务器**获取信息**而不是**修改状态**时调用。典型场景:
19
+
20
+ - 排查时拉主机名 / OS / 内核 / uptime
21
+ - 查某个进程是否在跑(`proc nginx`)
22
+ - 看监听端口(`ports 8080`)
23
+ - 读配置文件(`read-file /etc/nginx/nginx.conf`)
24
+ - 列目录结构(`dir /var/log`)
25
+ - 查二进制版本(`bin-version nginx`)
26
+ - 跑一段自定义只读命令(`exec-only "df -h | head"`)
27
+
28
+ **不要**用它做任何写操作——rm / mv / cp / vi / sed -i / systemctl / 重定向到文件 等一律被拒绝。
29
+
30
+ ## Setup(用户一次性)
31
+
32
+ ```bash
33
+ npm install -g @zicolasjac-ai/remote-readonly-ssh
34
+ rrs init # 生成配置模板 ~/.rrs/config.json
35
+ # 编辑配置: jump_server / private_key_path / target_hosts
36
+ rrs setup skills # 把本 skill 安装到 opencode / claude / agents 等目录
37
+ ```
38
+
39
+ ## Usage
40
+
41
+ ```bash
42
+ rrs <tool> [arg] [--host <ip>] [--config <file>] [--json]
43
+ ```
44
+
45
+ **始终加 `--json`**,便于解析。
46
+
47
+ ### Tools
48
+
49
+ | 工具 | 用途 | 示例 |
50
+ |------|------|------|
51
+ | `sys-info` | 主机名 / OS / 内核 / uptime | `rrs sys-info --json` |
52
+ | `proc <pattern>` | 进程查询(参数严格校验) | `rrs proc nginx --json` |
53
+ | `ports [filter]` | 监听端口 | `rrs ports 80 --json` |
54
+ | `read-file <path>` | 读文件(路径白名单) | `rrs read-file /etc/nginx/nginx.conf --json` |
55
+ | `dir <path>` | 列目录 | `rrs dir /var/log --json` |
56
+ | `bin-version <bin>` | 查二进制版本/编译参数 | `rrs bin-version nginx --json` |
57
+ | `exec-only <cmd>` | 通用只读命令(白名单校验) | `rrs exec-only "df -h" --json` |
58
+ | `list-tools` | 查看白名单 / 配置 | `rrs list-tools --json` |
59
+ | `init` | 生成配置模板 | `rrs init` |
60
+
61
+ ### Options
62
+
63
+ - `--host <ip>`:指定目标主机(必须存在于 `target_hosts` 白名单)
64
+ - `--config <file>`:使用自定义配置文件
65
+ - `--json`:以 JSON 输出
66
+ - `-v` / `--version`:查看版本
67
+ - `-h` / `--help`:查看用法
68
+
69
+ ### Return shape
70
+
71
+ 成功:
72
+
73
+ ```json
74
+ {"ok":true,"tool":"sys-info","host":"10.0.0.1","output":"..."}
75
+ ```
76
+
77
+ 拒绝(参数非法 / 敏感路径 / 命令不在白名单):
78
+
79
+ ```json
80
+ {"ok":false,"tool":"proc","host":"","error":"拒绝: 进程名 含非法字符 [a;b]","kind":"拒绝"}
81
+ ```
82
+
83
+ 错误(连接 / 鉴权 / 超时):
84
+
85
+ ```json
86
+ {"ok":false,"tool":"sys-info","host":"10.0.0.1","error":"等待命令执行完成超时: ...","kind":"错误"}
87
+ ```
88
+
89
+ 退出码:`0`=成功 / `2`=拒绝 / `3`=配置错误 / `1`=其他错误。
90
+
91
+ ## Hard rules(不要试图绕过)
92
+
93
+ 1. **永远不要**用 `exec-only` 跑写命令(`rm`、`mv`、`sed -i`、`>`、`tee` 等都被白名单拒绝)
94
+ 2. **永远不要**用 `read-file` 读 `blocked_paths`(`/etc/passwd`、`/etc/shadow`、`~/.ssh/id_*` 等)
95
+ 3. **永远不要**指定未在 `target_hosts` 注册的 IP
96
+ 4. **不要**试图把 `;` `&&` `|` `>` `$()` 拼到参数里——所有路径和参数都做严格字符集校验
97
+
98
+ 被拒绝的尝试一样会进审计日志。
99
+
100
+ ## Audit
101
+
102
+ 每次调用(含拒绝)追加写入 `~/.rrs/audit.log`:
103
+
104
+ ```
105
+ [巡检记录] 2026-08-18 10:30:00
106
+ 主机: 10.0.0.1
107
+ 巡检项: sys-info
108
+ 结果: 成功
109
+ 命令明细:
110
+ 1. echo ===HOST===; hostname; whoami; id
111
+ 读取主机标识与权限
112
+ ...
113
+ ```
114
+
115
+ 如需让审计同时记录命令输出摘要,在 `~/.rrs/config.json` 设置 `"audit": { "include_output": true }`。
package/src/setup.js ADDED
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const os = require("os");
6
+
7
+ const SKILL_NAME = "rrs";
8
+
9
+ const ALIAS_TO_GROUP = {
10
+ opencode: ["opencode"],
11
+ claude: ["claude"],
12
+ agents: ["agents"],
13
+ codex: ["claude"],
14
+ cursor: ["claude"],
15
+ windsurf: ["claude"],
16
+ openhands: ["agents"],
17
+ };
18
+
19
+ const RELATIVE_DIRS = {
20
+ opencode: [".config", "opencode", "skills"],
21
+ claude: [".claude", "skills"],
22
+ agents: [".agents", "skills"],
23
+ };
24
+
25
+ function listTargets(agent) {
26
+ const a = (agent || "all").toLowerCase();
27
+ if (a === "all") return ["opencode", "claude", "agents"];
28
+ const groups = ALIAS_TO_GROUP[a];
29
+ if (!groups) throw new Error(`未知 --agent [${agent}],支持: all / opencode / claude / cursor / windsurf / codex / openhands / agents`);
30
+ return groups;
31
+ }
32
+
33
+ function targetFile(agent, home) {
34
+ const segs = RELATIVE_DIRS[agent];
35
+ if (!segs) throw new Error(`内部错误: 未知 agent ${agent}`);
36
+ return path.join(home, ...segs, SKILL_NAME, "SKILL.md");
37
+ }
38
+
39
+ function skillSrc(packageRoot) {
40
+ return path.join(packageRoot, "skills", SKILL_NAME, "SKILL.md");
41
+ }
42
+
43
+ function install(packageRoot, opts = {}) {
44
+ const agent = opts.agent || "all";
45
+ const dryRun = !!opts.dryRun;
46
+ const home = opts.home || os.homedir();
47
+ const src = skillSrc(packageRoot);
48
+ if (!fs.existsSync(src)) throw new Error(`缺少 skill 资源: ${src}`);
49
+ const targets = listTargets(agent);
50
+ const results = [];
51
+ for (const t of targets) {
52
+ const dest = targetFile(t, home);
53
+ if (dryRun) {
54
+ results.push({ agent: t, dir: dest, action: "would-write", exists: fs.existsSync(dest) });
55
+ continue;
56
+ }
57
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
58
+ fs.writeFileSync(dest, fs.readFileSync(src, "utf8"), "utf8");
59
+ results.push({ agent: t, dir: dest, action: "written", exists: true });
60
+ }
61
+ return results;
62
+ }
63
+
64
+ function uninstall(opts = {}) {
65
+ const agent = opts.agent || "all";
66
+ const dryRun = !!opts.dryRun;
67
+ const home = opts.home || os.homedir();
68
+ const targets = listTargets(agent);
69
+ const results = [];
70
+ for (const t of targets) {
71
+ const dest = targetFile(t, home);
72
+ const dir = path.dirname(dest);
73
+ if (!fs.existsSync(dest)) {
74
+ results.push({ agent: t, dir: dest, action: "missing", exists: false });
75
+ continue;
76
+ }
77
+ if (dryRun) {
78
+ results.push({ agent: t, dir: dest, action: "would-delete", exists: true });
79
+ continue;
80
+ }
81
+ fs.unlinkSync(dest);
82
+ try { fs.rmdirSync(dir); } catch (_) {}
83
+ results.push({ agent: t, dir: dest, action: "deleted", exists: false });
84
+ }
85
+ return results;
86
+ }
87
+
88
+ module.exports = { install, uninstall, listTargets, targetFile, skillSrc, ALIAS_TO_GROUP, SKILL_NAME };