srvgov-cli 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 JiangHe12
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,161 @@
1
+ # srvgov-cli
2
+
3
+ [English](README.md) | [中文](README_zh.md)
4
+
5
+ Governed remote server command execution for AI agents and operators. `srvgov`
6
+ combines fail-closed command classification, R0-R3 authorization, strict TOFU
7
+ SSH host-key pinning, output redaction, and structured audit records.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install -g srvgov-cli
13
+ # or
14
+ go install github.com/JiangHe12/srvgov-cli@latest
15
+ ```
16
+
17
+ GitHub Releases provide Linux, macOS, and Windows binaries for amd64 and arm64.
18
+ The npm package downloads the matching release binary and verifies SHA-256
19
+ checksums by default.
20
+
21
+ ## Quickstart
22
+
23
+ ```bash
24
+ srvgov ctx set dev --server ssh://alice@example.com:22 --identity-file ~/.ssh/id_ed25519 -o json
25
+ srvgov ctx use dev -o json
26
+ srvgov exec --dry-run "uptime" -o json
27
+ srvgov exec "uptime" -o json
28
+ srvgov audit --limit 20 -o json
29
+ ```
30
+
31
+ Use `-o json` for automation and AI agents.
32
+
33
+ ## Governance Model
34
+
35
+ | Risk | Meaning | Authorization |
36
+ |---|---|---|
37
+ | R0 | known read-only command | free to run, still audited |
38
+ | R1 | known benign change | `--reason` and `--yes` |
39
+ | R2 | unknown or elevated command | `--reason`, non-empty `--ticket`, and `--yes` |
40
+ | R3 | destructive, privileged, dynamic, or parser-uncertain command | `--reason`, `--ticket`, `--allow-destructive`, and `--yes` |
41
+
42
+ Protected contexts raise R1 to R2 and R2 to R3. AI agents must never auto-fill
43
+ `--ticket`, `--allow-destructive`, or high-risk `--yes`. Use `exec --dry-run`
44
+ to obtain the classifier's risk and required authorization; do not guess impact.
45
+
46
+ ## Contexts
47
+
48
+ ```bash
49
+ srvgov ctx set prod \
50
+ --server ssh://deploy@example.com:22 \
51
+ --identity-file ~/.ssh/id_ed25519 \
52
+ --auth-method private-key,agent,password \
53
+ --env production \
54
+ --protected \
55
+ -o json
56
+
57
+ srvgov ctx use prod -o json
58
+ srvgov ctx current -o json
59
+ srvgov ctx list -o json
60
+ srvgov ctx delete old-host -o json
61
+ srvgov ctx role set prod --target-operator alice --role writer -o json
62
+ srvgov ctx role list prod -o json
63
+ srvgov ctx export prod > prod.ctx.yaml
64
+ srvgov ctx import -f prod.ctx.yaml --rename prod-copy --yes -o json
65
+ srvgov ctx migrate-credentials --to encrypted-file --context prod -o json
66
+ ```
67
+
68
+ Context output never includes passwords, private-key contents, passphrases, or
69
+ identity-file paths.
70
+
71
+ Portable context export uses `srvgov.io/ctx-export/v1`. Literal password and
72
+ SSH identity passphrase values are redacted by default; credstore references are
73
+ preserved. `--include-credentials` is limited to plain-yaml contexts.
74
+
75
+ ## Governed Execution
76
+
77
+ Preview without connecting or executing:
78
+
79
+ ```bash
80
+ srvgov exec --dry-run "touch /tmp/deploy-ready" -o json
81
+ ```
82
+
83
+ Execute according to the reported tier:
84
+
85
+ ```bash
86
+ # R0
87
+ srvgov exec "systemctl status nginx" -o json
88
+
89
+ # R1
90
+ srvgov exec "touch /tmp/deploy-ready" \
91
+ --reason "mark deployment ready" --yes -o json
92
+
93
+ # R2
94
+ srvgov exec "custom-maintenance-command" \
95
+ --reason "scheduled maintenance" --ticket OPS-123 --yes -o json
96
+
97
+ # R3
98
+ srvgov exec "rm -rf /tmp/old-release" \
99
+ --reason "remove failed release" \
100
+ --ticket OPS-123 --allow-destructive --yes -o json
101
+ ```
102
+
103
+ Commands run without a PTY. stdout, stderr, command text, and audit fields are
104
+ redacted before output or persistence. A remote non-zero exit returns structured
105
+ output and process exit code 7 (`BACKEND_ERROR`).
106
+
107
+ ## SSH Trust And Credentials
108
+
109
+ The first connection to an unknown `host:port` pins its SSH public key in
110
+ `~/.srvgov/known_hosts`. Later key mismatches, including an unpinned key type
111
+ for an already known address, are rejected. Host-key rotation requires manual
112
+ review and removal of the old pin; there is no insecure bypass.
113
+
114
+ Authentication order is private key, SSH agent, then password, subject to the
115
+ context's `--auth-method` preference. Passwords and key passphrases may use
116
+ opskit-core credential-store references. Credentials and raw SSH output are not
117
+ logged by the transport.
118
+
119
+ ## Audit And Diagnostics
120
+
121
+ ```bash
122
+ srvgov capabilities -o json
123
+ srvgov audit query -o json
124
+ srvgov audit query --type authorization.denied --status denied -o json
125
+ srvgov audit verify --strict -o json
126
+ srvgov audit prune --keep-last 20 -o json
127
+ srvgov doctor -o json
128
+ srvgov version -o json
129
+ srvgov --version
130
+ ```
131
+
132
+ Audit records live at `~/.srvgov/audit.log` by default and include effective
133
+ risk, authorization status, target, redacted command/output, exit code, and
134
+ error details.
135
+
136
+ `capabilities` reports the current command surface, `srvgov.io/context/v1`,
137
+ `srvgov.io/audit/v1`, R0-R3 authorization rules, `--allow-destructive`, JSONL
138
+ audit, RBAC reader/writer/admin, dry-run, strict TOFU, and redaction.
139
+
140
+ ## AI Skill
141
+
142
+ ```bash
143
+ srvgov install claude --skills
144
+ srvgov install codex --skills
145
+ srvgov install /custom/skills/path --skills
146
+ ```
147
+
148
+ ## Build
149
+
150
+ ```bash
151
+ go build ./...
152
+ go test -count=1 ./...
153
+ gofmt -l main.go cmd internal
154
+ golangci-lint run --timeout=5m
155
+ go vet -tags=integration ./...
156
+ ```
157
+
158
+ ## Contributing, Security, License
159
+
160
+ See [CONTRIBUTING.md](CONTRIBUTING.md), [SECURITY.md](SECURITY.md), and
161
+ [LICENSE](LICENSE).
@@ -0,0 +1,35 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { spawn } = require('child_process');
4
+ const path = require('path');
5
+ const fs = require('fs');
6
+ const os = require('os');
7
+
8
+ function getBinaryPath() {
9
+ const binaryName = os.platform() === 'win32' ? 'srvgov.exe' : 'srvgov';
10
+ return path.join(__dirname, binaryName);
11
+ }
12
+
13
+ function main() {
14
+ const binaryPath = getBinaryPath();
15
+ if (!fs.existsSync(binaryPath)) {
16
+ console.error('srvgov binary not found. Please reinstall:');
17
+ console.error(' npm install -g srvgov-cli');
18
+ process.exit(1);
19
+ }
20
+
21
+ const child = spawn(binaryPath, process.argv.slice(2), { stdio: 'inherit' });
22
+ child.on('error', (err) => {
23
+ console.error('Failed to run srvgov:', err.message);
24
+ process.exit(1);
25
+ });
26
+ child.on('exit', (code, signal) => {
27
+ if (signal) {
28
+ process.kill(process.pid, signal);
29
+ return;
30
+ }
31
+ process.exit(code == null ? 1 : code);
32
+ });
33
+ }
34
+
35
+ main();
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "srvgov-cli",
3
+ "version": "0.1.0",
4
+ "description": "Governed remote server command execution CLI for AI agents",
5
+ "bin": {
6
+ "srvgov": "bin/srvgov-cli.js",
7
+ "srvgov-cli": "bin/srvgov-cli.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "scripts/install.js",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "scripts": {
16
+ "postinstall": "node scripts/install.js"
17
+ },
18
+ "keywords": [
19
+ "ssh",
20
+ "server",
21
+ "cli",
22
+ "ai",
23
+ "governance"
24
+ ],
25
+ "author": "JiangHe12",
26
+ "license": "MIT",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/JiangHe12/srvgov-cli.git"
30
+ },
31
+ "homepage": "https://github.com/JiangHe12/srvgov-cli",
32
+ "engines": {
33
+ "node": ">=14"
34
+ }
35
+ }
@@ -0,0 +1,230 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const https = require('https');
6
+ const http = require('http');
7
+ const crypto = require('crypto');
8
+ const { URL } = require('url');
9
+
10
+ const pkg = require('../package.json');
11
+ const VERSION = pkg.version;
12
+ const REPO = 'JiangHe12/srvgov-cli';
13
+ const TIMEOUT_MS = 30000;
14
+
15
+ const ALLOWED_REDIRECT_HOSTS = new Set([
16
+ 'github.com',
17
+ 'objects.githubusercontent.com',
18
+ 'github-releases.githubusercontent.com',
19
+ 'release-assets.githubusercontent.com',
20
+ 'github.githubassets.com',
21
+ 'cdn.jsdelivr.net',
22
+ 'fastly.jsdelivr.net',
23
+ ]);
24
+
25
+ function isAllowedRedirectHost(urlStr) {
26
+ try {
27
+ const parsed = new URL(urlStr);
28
+ return ALLOWED_REDIRECT_HOSTS.has(parsed.hostname) || parsed.hostname.endsWith('.github.io');
29
+ } catch {
30
+ return false;
31
+ }
32
+ }
33
+
34
+ function applyMirror(canonicalUrl) {
35
+ const mirror = process.env.SRVGOV_CLI_DOWNLOAD_MIRROR;
36
+ if (!mirror) return canonicalUrl;
37
+ return mirror.replace(/\/+$/, '') + '/' + canonicalUrl;
38
+ }
39
+
40
+ function pickClient(url) {
41
+ return new URL(url).protocol === 'http:' ? http : https;
42
+ }
43
+
44
+ function getPlatform() {
45
+ const platformMap = { win32: 'windows', darwin: 'darwin', linux: 'linux' };
46
+ const archMap = { x64: 'amd64', arm64: 'arm64' };
47
+ return {
48
+ os: platformMap[process.platform] || process.platform,
49
+ arch: archMap[process.arch] || process.arch,
50
+ };
51
+ }
52
+
53
+ function getBinaryName() {
54
+ const { os, arch } = getPlatform();
55
+ const ext = os === 'windows' ? '.exe' : '';
56
+ return `srvgov-cli-${os}-${arch}${ext}`;
57
+ }
58
+
59
+ function getDownloadUrl() {
60
+ const binary = getBinaryName();
61
+ return applyMirror(`https://github.com/${REPO}/releases/download/v${VERSION}/${binary}`);
62
+ }
63
+
64
+ function request(url, onResponse) {
65
+ const req = pickClient(url).get(url, onResponse);
66
+ req.setTimeout(TIMEOUT_MS, () => {
67
+ req.destroy(new Error(`Download timed out after ${TIMEOUT_MS / 1000}s`));
68
+ });
69
+ return req;
70
+ }
71
+
72
+ function redirectTarget(currentUrl, response) {
73
+ return new URL(response.headers.location, currentUrl).toString();
74
+ }
75
+
76
+ function download(url, dest, redirectCount = 0) {
77
+ return new Promise((resolve, reject) => {
78
+ if (redirectCount > 5) {
79
+ reject(new Error('Too many redirects'));
80
+ return;
81
+ }
82
+
83
+ const req = request(url, (response) => {
84
+ if (response.statusCode === 301 || response.statusCode === 302 ||
85
+ response.statusCode === 307 || response.statusCode === 308) {
86
+ response.resume();
87
+ const target = redirectTarget(url, response);
88
+ if (!isAllowedRedirectHost(target)) {
89
+ reject(new Error(`Redirect to non-allowed host rejected: ${target}`));
90
+ return;
91
+ }
92
+ download(target, dest, redirectCount + 1).then(resolve).catch(reject);
93
+ return;
94
+ }
95
+ if (response.statusCode !== 200) {
96
+ response.resume();
97
+ reject(new Error(`Download failed: ${response.statusCode}`));
98
+ return;
99
+ }
100
+
101
+ const file = fs.createWriteStream(dest);
102
+ response.pipe(file);
103
+ file.on('finish', () => file.close(resolve));
104
+ file.on('error', (err) => {
105
+ response.destroy();
106
+ fs.unlink(dest, () => {});
107
+ reject(err);
108
+ });
109
+ });
110
+ req.on('error', (err) => {
111
+ fs.unlink(dest, () => {});
112
+ reject(err);
113
+ });
114
+ });
115
+ }
116
+
117
+ function getChecksumsUrl() {
118
+ return `https://github.com/${REPO}/releases/download/v${VERSION}/checksums.txt`;
119
+ }
120
+
121
+ function downloadToString(url, redirectsLeft = 5) {
122
+ return new Promise((resolve, reject) => {
123
+ const req = request(url, (response) => {
124
+ if (response.statusCode === 301 || response.statusCode === 302 ||
125
+ response.statusCode === 307 || response.statusCode === 308) {
126
+ response.resume();
127
+ if (redirectsLeft <= 0) {
128
+ reject(new Error('Too many redirects'));
129
+ return;
130
+ }
131
+ const target = redirectTarget(url, response);
132
+ if (!isAllowedRedirectHost(target)) {
133
+ reject(new Error(`Redirect to non-allowed host rejected: ${target}`));
134
+ return;
135
+ }
136
+ downloadToString(target, redirectsLeft - 1).then(resolve).catch(reject);
137
+ return;
138
+ }
139
+ if (response.statusCode !== 200) {
140
+ response.resume();
141
+ reject(new Error(`Download failed: ${response.statusCode}`));
142
+ return;
143
+ }
144
+ let data = '';
145
+ response.setEncoding('utf8');
146
+ response.on('data', (chunk) => { data += chunk; });
147
+ response.on('end', () => resolve(data));
148
+ });
149
+ req.on('error', reject);
150
+ });
151
+ }
152
+
153
+ function sha256File(filePath) {
154
+ return new Promise((resolve, reject) => {
155
+ const hash = crypto.createHash('sha256');
156
+ const stream = fs.createReadStream(filePath);
157
+ stream.on('data', (data) => hash.update(data));
158
+ stream.on('end', () => resolve(hash.digest('hex')));
159
+ stream.on('error', reject);
160
+ });
161
+ }
162
+
163
+ function parseChecksums(text) {
164
+ const checksums = {};
165
+ for (const line of text.split('\n')) {
166
+ const match = line.trim().match(/^([a-f0-9]{64})\s+\*?(.+)$/);
167
+ if (match) checksums[match[2]] = match[1];
168
+ }
169
+ return checksums;
170
+ }
171
+
172
+ async function verifyDownloadedBinary(binaryPath, binaryName) {
173
+ if (process.env.SRVGOV_CLI_SKIP_VERIFY === '1') {
174
+ console.log('Verification skipped (SRVGOV_CLI_SKIP_VERIFY=1)');
175
+ return;
176
+ }
177
+ const checksumsUrl = getChecksumsUrl();
178
+ let checksums;
179
+ try {
180
+ checksums = parseChecksums(await downloadToString(checksumsUrl));
181
+ } catch (err) {
182
+ throw new Error(
183
+ `Could not fetch canonical checksums.txt from ${checksumsUrl}: ${err.message}. ` +
184
+ 'Set SRVGOV_CLI_SKIP_VERIFY=1 to install without checksum verification.'
185
+ );
186
+ }
187
+ if (!checksums[binaryName]) {
188
+ throw new Error(
189
+ `No checksum found for ${binaryName}. ` +
190
+ 'Set SRVGOV_CLI_SKIP_VERIFY=1 to install without checksum verification.'
191
+ );
192
+ }
193
+ const actual = await sha256File(binaryPath);
194
+ if (actual !== checksums[binaryName]) {
195
+ try { fs.unlinkSync(binaryPath); } catch {}
196
+ throw new Error(
197
+ `SHA-256 mismatch for ${binaryName}\n` +
198
+ ` Expected: ${checksums[binaryName]}\n` +
199
+ ` Actual: ${actual}\n` +
200
+ 'The downloaded binary may be corrupted or tampered with.'
201
+ );
202
+ }
203
+ console.log('SHA-256 verification passed');
204
+ }
205
+
206
+ async function main() {
207
+ const { os, arch } = getPlatform();
208
+ const binary = getBinaryName();
209
+ const url = getDownloadUrl();
210
+ const destDir = path.join(__dirname, '..', 'bin');
211
+ const dest = path.join(destDir, os === 'windows' ? 'srvgov.exe' : 'srvgov');
212
+
213
+ console.log(`Installing srvgov v${VERSION} for ${os}/${arch}...`);
214
+ fs.mkdirSync(destDir, { recursive: true });
215
+
216
+ try {
217
+ await download(url, dest);
218
+ await verifyDownloadedBinary(dest, binary);
219
+ if (os !== 'windows') fs.chmodSync(dest, 0o755);
220
+ console.log('srvgov installed successfully!');
221
+ } catch (err) {
222
+ console.error('Failed to install srvgov:', err.message);
223
+ console.error('');
224
+ console.error('Please download manually from:');
225
+ console.error(` ${url}`);
226
+ process.exit(1);
227
+ }
228
+ }
229
+
230
+ main();