@agentguardx/helper 0.0.0-stage → 0.3.88
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 +41 -2
- package/bin/agentguard.cjs +21 -0
- package/lib/preuninstall.cjs +2 -0
- package/lib/runtime.cjs +106 -0
- package/lib/wizard.cjs +196 -0
- package/package.json +30 -4
- package/release-public.pem +11 -0
package/README.md
CHANGED
|
@@ -1,3 +1,42 @@
|
|
|
1
|
-
#
|
|
1
|
+
# AgentGuard 员工接入助手
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
需要 Node.js 18 或更新版本。企业将包发布到可信 npm 仓库后,可使用:
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npx @agentguardx/helper
|
|
7
|
+
npm i -g @agentguardx/helper
|
|
8
|
+
agentguard init --server https://企业服务器域名
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
公共 npmjs 尚未发布;可发布到企业私有仓库后用 `npx --registry <仓库地址> @agentguardx/helper`(见 docs/client/B13_INSTALLATION.md 的发布步骤),或使用管理员分发的离线 tgz。离线安装时同时安装主包和本机对应的平台包;全局命令仍为 `agentguard`。包不固定企业服务器地址;可交互填写,或使用 `--server` / `AGENTGUARD_SERVER`。支持 HTTP/HTTPS;HTTP 每个新服务器先明确确认明文风险,确认前不发送令牌。
|
|
12
|
+
|
|
13
|
+
npx 即用不会创建全局 `agentguard` 命令;后续使用 `npx @agentguardx/helper launch opencode`、`npx @agentguardx/helper uninstall`,或全局安装后使用短命令。
|
|
14
|
+
|
|
15
|
+
向导先显示当前服务器地址(优先级:`--server` 参数 > 环境变量 `AGENTGUARD_SERVER` > 上次保存的配置)作为默认值:回车沿用,也可直接输入别的 IP 或域名;输入后重新校验 `/client/v1/config`,HTTP 只提示一次明文风险。已有登录属于旧服务器时,向导提示需要重新登录,并在你确认后先退出旧登录(撤销旧令牌、恢复已接入工具配置);旧令牌绝不会发给新服务器。命令行同样可用:`agentguard server show` 查看当前地址,`agentguard server set <地址>` 修改(加 `--switch` 同时退出旧登录并恢复工具配置;旧服务器无法访问时可再加 `--local-only`,只清除本机登录,旧令牌在服务端仍有效直到到期或由管理员撤销)。
|
|
16
|
+
|
|
17
|
+
登录方式为“浏览器网页授权”(`agentguard login`):命令行在本机回环地址开一次性回调,打开控制台 `/client-authorize` 授权页,由控制台当前登录的人点“授权”,成功后显示“已登录为 姓名(账号)”。无浏览器时命令行会打印授权链接,须在本机浏览器打开(回调只监听 127.0.0.1)。也可以隐藏输入个人令牌。令牌只通过匿名标准输入交给 Go 核心,经网关校验后写系统钥匙串,不进入命令行或普通配置文件。方向键移动,空格多选已检测的工具,Enter 确认;配置完成后显示实际 doctor 结果。
|
|
18
|
+
|
|
19
|
+
企业对话正文留存不要求员工告知或确认,向导不展示告知步骤、不提交已读;正常对话不会因告知流程被阻断。留存开启仍须独立双审,正文访问按当前角色与服务端审批结果执行,平台管理员可审核本人申请;正式环境默认关闭。
|
|
20
|
+
|
|
21
|
+
支持入口为 OpenCode、OpenClaw、Cursor 远程 MCP、Claude Desktop MCP 桥接及 Cherry Studio 手动说明;厂商原生实测范围以企业兼容记录为准。Codex 暂缓,Gemini 和 Claude Code 不接入。B15 为 OpenCode 同时注册本机安全 stdio MCP,并写入关闭自带本机工具的配置;仅经 ag_* 入口的调用由服务端逐次授权、执行上报和输出检查。其他厂商未验证关闭原生工具时,其自带本机终端、文件操作不受该 MCP 管控。客户端可被员工修改的边界不等同操作系统强制隔离。
|
|
22
|
+
|
|
23
|
+
启动器每次执行 Go 核心前,使用内置固定公钥验证 originless npm 清单的 RSA-3072/SHA-256 签名,再检查平台二进制与 macOS 钥匙串 helper 的大小和 SHA-256。验证失败拒绝执行,不下载替换公钥。
|
|
24
|
+
|
|
25
|
+
卸载先执行 `agentguard uninstall`,确认配置恢复、令牌撤销和钥匙串清理后,再执行 `npm rm -g @agentguardx/helper`。直接 npm 卸载不会清理用户数据;`preuninstall` 仅作提示,部分 npm 版本不会运行卸载生命周期,不能依赖该提示代替清理。
|
|
26
|
+
|
|
27
|
+
离线安装若将主包与平台 tgz 同时全局安装,npm 可能把平台包登记为显式根依赖。移除主包后若该平台包仍在,请同时 `npm rm -g @agentguardx/helper-<平台>-<架构>`;正常 registry 安装的 optional 依赖由 npm 管理。卸载检查只确认命令消失和用户凭据/配置清理,不声称清空全部 npm 缓存。
|
|
28
|
+
|
|
29
|
+
Claude Desktop 的 stdio 桥接保存当前 Node 程序与主包启动器的绝对路径,复用同一个执行前验签入口,再由 Go 自行复核自身和 helper。若清理了对应 npx 缓存,或原 Node 路径已失效,须重新接入;可全局安装后再接入,不能保证 npx 缓存永久保留。
|
|
30
|
+
|
|
31
|
+
本机安全工具命令(全局安装示例;仅 npx 使用时替换为 `npx -- @agentguardx/helper`):
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
agentguard connect opencode
|
|
35
|
+
agentguard launch opencode
|
|
36
|
+
agentguard mcp serve
|
|
37
|
+
agentguard pause
|
|
38
|
+
agentguard resume
|
|
39
|
+
agentguard activity
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`mcp serve` 的 stdout 专用于 stdio MCP 协议,工作根固定为进程启动目录,不接受模型自报身份或 root。`pause` 是本机应急停用,不是企业级一键暂停,也不能撤回已发生的副作用。
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
const { verifiedRuntime, run } = require('../lib/runtime.cjs');
|
|
4
|
+
async function main() {
|
|
5
|
+
if (Number(process.versions.node.split('.')[0]) < 18) throw new Error('node_18_required');
|
|
6
|
+
const runtime = verifiedRuntime();
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
// The wizard starts with no arguments, "init", or only --server/--config-dir
|
|
9
|
+
// pairs; anything else (for example "--server X server show") goes to the Go core.
|
|
10
|
+
const initArgs = args[0] === 'init' ? args.slice(1) : args;
|
|
11
|
+
const wizardOnly = initArgs.length % 2 === 0 && initArgs.every((value, index) => index % 2 ? !value.startsWith('--') : value === '--server' || value === '--config-dir');
|
|
12
|
+
if (!args.length || args[0] === 'init' || wizardOnly) {
|
|
13
|
+
await require('../lib/wizard.cjs').init(runtime, initArgs);
|
|
14
|
+
} else {
|
|
15
|
+
process.exitCode = await run(runtime, args);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
main().catch(error => {
|
|
19
|
+
const code = /^[a-z0-9_]+$/.test(error.message) ? error.message : 'helper_operation_failed';
|
|
20
|
+
process.stderr.write(`接入助手未完成:${code}\n`); process.exitCode = 1;
|
|
21
|
+
});
|
package/lib/runtime.cjs
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const crypto = require('node:crypto');
|
|
5
|
+
const { spawn } = require('node:child_process');
|
|
6
|
+
const PIN = 'b6ad0e1a776a0db7b3edc5c4a9a367d1cd3801dc97dc0e27af0ea0d3e7b52e1f';
|
|
7
|
+
const KEY_ID = 'agentguard-cli-rsa3072-20261023';
|
|
8
|
+
const hash = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
|
|
9
|
+
function ordinary(file, limit) {
|
|
10
|
+
const before = fs.lstatSync(file);
|
|
11
|
+
if (!before.isFile() || before.size > limit || before.isSymbolicLink()) throw new Error('npm_artifact_invalid');
|
|
12
|
+
const fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
|
|
13
|
+
try {
|
|
14
|
+
const after = fs.fstatSync(fd);
|
|
15
|
+
if (after.dev !== before.dev || after.ino !== before.ino || !after.isFile()) throw new Error('npm_artifact_changed');
|
|
16
|
+
return fs.readFileSync(fd);
|
|
17
|
+
} finally { fs.closeSync(fd); }
|
|
18
|
+
}
|
|
19
|
+
function verifiedRuntime() {
|
|
20
|
+
const owner = path.resolve(__dirname, '..');
|
|
21
|
+
const metadata = JSON.parse(ordinary(path.join(owner, 'package.json'), 65536));
|
|
22
|
+
const target = `${process.platform}-${process.arch}`;
|
|
23
|
+
if (!Object.hasOwn(metadata.optionalDependencies, `@agentguardx/helper-${target}`)) throw new Error('npm_platform_unsupported');
|
|
24
|
+
let directory;
|
|
25
|
+
try { directory = path.dirname(require.resolve(`@agentguardx/helper-${target}/package.json`)); }
|
|
26
|
+
catch { throw new Error('npm_platform_package_missing'); }
|
|
27
|
+
directory = fs.realpathSync(directory);
|
|
28
|
+
const platformMetadata = JSON.parse(ordinary(path.join(directory, 'package.json'), 65536));
|
|
29
|
+
if (platformMetadata.name !== `@agentguardx/helper-${target}` || platformMetadata.version !== metadata.version) throw new Error('npm_package_version_mismatch');
|
|
30
|
+
const key = ordinary(path.join(owner, 'release-public.pem'), 8192);
|
|
31
|
+
if (hash(key) !== PIN) throw new Error('npm_release_key_changed');
|
|
32
|
+
const bytes = ordinary(path.join(directory, 'manifest.tsv'), 1 << 20);
|
|
33
|
+
const signature = ordinary(path.join(directory, 'manifest.tsv.sig'), 8192);
|
|
34
|
+
if (!crypto.verify('RSA-SHA256', bytes, { key, padding: crypto.constants.RSA_PKCS1_PADDING }, signature)) throw new Error('npm_release_signature_invalid');
|
|
35
|
+
const values = new Map(), artifacts = new Map();
|
|
36
|
+
if (!bytes.length || bytes[bytes.length - 1] !== 10 || bytes.includes(13)) throw new Error('npm_manifest_invalid');
|
|
37
|
+
for (const line of bytes.toString('utf8').slice(0, -1).split('\n')) {
|
|
38
|
+
const row = line.split('\t');
|
|
39
|
+
if (row.length === 2 && ['format', 'distribution', 'version', 'key_id', 'public_key_sha256'].includes(row[0]) && !values.has(row[0])) values.set(row[0], row[1]);
|
|
40
|
+
else if (row.length === 4 && row[0] === 'artifact' && /^agentguard(?:-keychain)?-(?:darwin|linux|windows)-(?:amd64|arm64)(?:\.exe)?$/.test(row[1]) && /^[1-9][0-9]*$/.test(row[2]) && /^[0-9a-f]{64}$/.test(row[3]) && !artifacts.has(row[1])) artifacts.set(row[1], { size: Number(row[2]), hash: row[3] });
|
|
41
|
+
else throw new Error('npm_manifest_invalid');
|
|
42
|
+
}
|
|
43
|
+
if (values.size !== 5 || values.get('format') !== '1' || values.get('distribution') !== 'npm' || values.get('version') !== `v${metadata.version}` || values.get('key_id') !== KEY_ID || values.get('public_key_sha256') !== PIN) throw new Error('npm_manifest_identity_invalid');
|
|
44
|
+
const goOS = process.platform === 'win32' ? 'windows' : process.platform;
|
|
45
|
+
const goArch = process.arch === 'x64' ? 'amd64' : process.arch;
|
|
46
|
+
const binaryName = `agentguard-${goOS}-${goArch}${goOS === 'windows' ? '.exe' : ''}`;
|
|
47
|
+
const names = [binaryName];
|
|
48
|
+
if (process.platform === 'darwin') names.push(`agentguard-keychain-${goOS}-${goArch}`);
|
|
49
|
+
for (const name of names) {
|
|
50
|
+
const expected = artifacts.get(name), actual = ordinary(path.join(directory, name), 128 << 20);
|
|
51
|
+
if (!expected || actual.length !== expected.size || hash(actual) !== expected.hash) throw new Error('npm_artifact_hash_mismatch');
|
|
52
|
+
}
|
|
53
|
+
return { binary: path.join(directory, binaryName), directory, owner, version: metadata.version };
|
|
54
|
+
}
|
|
55
|
+
function run(runtime, args, input) {
|
|
56
|
+
// Verify again directly before each process launch; never download a new key.
|
|
57
|
+
const current = verifiedRuntime();
|
|
58
|
+
if (current.binary !== runtime.binary) throw new Error('npm_runtime_changed');
|
|
59
|
+
const env = { ...process.env, AGENTGUARD_DISTRIBUTION: 'npm', AGENTGUARD_NPM_PACKAGE_DIR: runtime.owner, AGENTGUARD_NPM_PLATFORM_DIR: runtime.directory, AGENTGUARD_NPM_NODE_EXECUTABLE: process.execPath };
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
const child = spawn(runtime.binary, args, { env, shell: false, stdio: [input === undefined ? 'inherit' : 'pipe', 'inherit', 'inherit'] });
|
|
62
|
+
child.once('error', () => reject(new Error('npm_binary_launch_failed')));
|
|
63
|
+
child.once('exit', code => resolve(code === null ? 1 : code));
|
|
64
|
+
if (input !== undefined) { child.stdin.on('error', () => {}); child.stdin.end(input); }
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
function queryConsent(runtime, server, prefix) {
|
|
68
|
+
const current = verifiedRuntime();
|
|
69
|
+
if (current.binary !== runtime.binary) throw new Error('npm_runtime_changed');
|
|
70
|
+
const env = { ...process.env, AGENTGUARD_DISTRIBUTION: 'npm', AGENTGUARD_NPM_PACKAGE_DIR: runtime.owner, AGENTGUARD_NPM_PLATFORM_DIR: runtime.directory, AGENTGUARD_NPM_NODE_EXECUTABLE: process.execPath };
|
|
71
|
+
return new Promise((resolve, reject) => {
|
|
72
|
+
const child = spawn(runtime.binary, [...prefix, 'http-consent', '--server', server], { env, shell: false, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
73
|
+
let bytes = 0, chunks = [];
|
|
74
|
+
child.stdout.on('data', chunk => { bytes += chunk.length; if (bytes > 65536) { child.kill(); reject(new Error('http_consent_query_failed')); } else chunks.push(chunk); });
|
|
75
|
+
child.once('error', () => reject(new Error('http_consent_query_failed')));
|
|
76
|
+
child.once('close', code => {
|
|
77
|
+
try {
|
|
78
|
+
if (code !== 0) throw new Error();
|
|
79
|
+
const value = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
80
|
+
if (value.server !== server || typeof value.acknowledged !== 'boolean' || typeof value.requires_confirmation !== 'boolean') throw new Error();
|
|
81
|
+
resolve(value);
|
|
82
|
+
} catch { reject(new Error('http_consent_query_failed')); }
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
// Reads one bounded, non-secret JSON object printed by the verified Go core.
|
|
87
|
+
function queryJSON(runtime, args) {
|
|
88
|
+
const current = verifiedRuntime();
|
|
89
|
+
if (current.binary !== runtime.binary) throw new Error('npm_runtime_changed');
|
|
90
|
+
const env = { ...process.env, AGENTGUARD_DISTRIBUTION: 'npm', AGENTGUARD_NPM_PACKAGE_DIR: runtime.owner, AGENTGUARD_NPM_PLATFORM_DIR: runtime.directory, AGENTGUARD_NPM_NODE_EXECUTABLE: process.execPath };
|
|
91
|
+
return new Promise((resolve, reject) => {
|
|
92
|
+
const child = spawn(runtime.binary, args, { env, shell: false, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
93
|
+
let bytes = 0, chunks = [];
|
|
94
|
+
child.stdout.on('data', chunk => { bytes += chunk.length; if (bytes > 65536) { child.kill(); reject(new Error('core_query_failed')); } else chunks.push(chunk); });
|
|
95
|
+
child.once('error', () => reject(new Error('core_query_failed')));
|
|
96
|
+
child.once('close', code => {
|
|
97
|
+
try {
|
|
98
|
+
if (code !== 0) throw new Error();
|
|
99
|
+
const value = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
100
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error();
|
|
101
|
+
resolve(value);
|
|
102
|
+
} catch { reject(new Error('core_query_failed')); }
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
module.exports = { verifiedRuntime, run, queryConsent, queryJSON };
|
package/lib/wizard.cjs
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('node:fs');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
const http = require('node:http');
|
|
6
|
+
const https = require('node:https');
|
|
7
|
+
const readline = require('node:readline');
|
|
8
|
+
const { run, queryConsent, queryJSON } = require('./runtime.cjs');
|
|
9
|
+
|
|
10
|
+
function terminal() {
|
|
11
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error('init_requires_interactive_terminal');
|
|
12
|
+
readline.emitKeypressEvents(process.stdin);
|
|
13
|
+
return handler => new Promise((resolve, reject) => {
|
|
14
|
+
const previous = !!process.stdin.isRaw;
|
|
15
|
+
let finished = false;
|
|
16
|
+
const finish = (error, value) => {
|
|
17
|
+
if (finished) return;
|
|
18
|
+
finished = true;
|
|
19
|
+
process.stdin.removeListener('keypress', listener); process.stdin.setRawMode(previous); process.stdin.pause();
|
|
20
|
+
error ? reject(error) : resolve(value);
|
|
21
|
+
};
|
|
22
|
+
const listener = (text, key = {}) => {
|
|
23
|
+
if (finished) return;
|
|
24
|
+
if (key.ctrl && key.name === 'c') { process.stdout.write('\n'); finish(new Error('init_cancelled')); }
|
|
25
|
+
else handler(text, key, value => finish(null, value));
|
|
26
|
+
};
|
|
27
|
+
process.stdin.on('keypress', listener);
|
|
28
|
+
process.stdin.setRawMode(true); process.stdin.resume();
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
const SOURCE_LABEL = { flag: '命令行参数', env: '环境变量', saved: '上次保存的配置' };
|
|
32
|
+
async function question(ask, label, masked = false) {
|
|
33
|
+
let value = '';
|
|
34
|
+
const pending = ask((text, key, done) => {
|
|
35
|
+
if (key.name === 'return' || key.name === 'enter') { process.stdout.write('\n'); done(value); }
|
|
36
|
+
else if (key.name === 'backspace') {
|
|
37
|
+
if (value.length) { value = Array.from(value).slice(0, -1).join(''); process.stdout.write('\b \b'); }
|
|
38
|
+
} else if (text && !key.ctrl && !key.meta && !/[\x00-\x1f\x7f]/.test(text)) {
|
|
39
|
+
if (value.length + text.length <= 8192) { value += text; process.stdout.write(masked ? '*'.repeat(Array.from(text).length) : text); }
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
// Install raw/no-echo input before exposing the token prompt to a fast paste.
|
|
43
|
+
process.stdout.write(label);
|
|
44
|
+
return pending;
|
|
45
|
+
}
|
|
46
|
+
async function choose(ask, label, options, multiple = false) {
|
|
47
|
+
let cursor = 0, selected = new Set();
|
|
48
|
+
const render = first => {
|
|
49
|
+
if (!first) process.stdout.write(`\x1b[${options.length}A`);
|
|
50
|
+
options.forEach((option, index) => process.stdout.write(`\x1b[2K\r${cursor === index ? '›' : ' '} ${multiple ? `[${selected.has(index) ? '✓' : ' '}] ` : ''}${option.label}\n`));
|
|
51
|
+
};
|
|
52
|
+
const pending = ask((text, key, done) => {
|
|
53
|
+
if (key.name === 'up') cursor = (cursor + options.length - 1) % options.length;
|
|
54
|
+
else if (key.name === 'down') cursor = (cursor + 1) % options.length;
|
|
55
|
+
else if (multiple && key.name === 'space') selected.has(cursor) ? selected.delete(cursor) : selected.add(cursor);
|
|
56
|
+
else if (key.name === 'return' || key.name === 'enter') { done(multiple ? [...selected].sort().map(i => options[i].value) : options[cursor].value); return; }
|
|
57
|
+
render(false);
|
|
58
|
+
});
|
|
59
|
+
process.stdout.write(label + (multiple ? '(↑↓ 移动,空格勾选,Enter 确认)\n' : '(↑↓ 选择,Enter 确认)\n')); render(true);
|
|
60
|
+
return pending;
|
|
61
|
+
}
|
|
62
|
+
function serverOrigin(raw) {
|
|
63
|
+
let url; try { url = new URL(raw.trim()); } catch { throw new Error('server_address_invalid'); }
|
|
64
|
+
if (url.username || url.password || url.search || url.hash || url.pathname !== '/' || !['https:', 'http:'].includes(url.protocol)) throw new Error('server_address_invalid');
|
|
65
|
+
return url.origin;
|
|
66
|
+
}
|
|
67
|
+
function config(server) {
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
const target = new URL(server + '/client/v1/config');
|
|
70
|
+
const options = { timeout: 10000, headers: { Accept: 'application/json' } };
|
|
71
|
+
if (target.protocol === 'https:' && process.env.AGENTGUARD_CA_FILE) options.ca = fs.readFileSync(process.env.AGENTGUARD_CA_FILE);
|
|
72
|
+
const request = (target.protocol === 'https:' ? https : http).get(target, options, response => {
|
|
73
|
+
if (response.statusCode !== 200) { response.resume(); reject(new Error('server_config_unavailable')); return; }
|
|
74
|
+
let bytes = 0, chunks = [];
|
|
75
|
+
response.on('data', chunk => { bytes += chunk.length; if (bytes > 65536) { response.destroy(); reject(new Error('server_config_invalid')); } else chunks.push(chunk); });
|
|
76
|
+
response.on('error', () => reject(new Error('server_config_unavailable')));
|
|
77
|
+
response.on('end', () => {
|
|
78
|
+
try {
|
|
79
|
+
const body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
|
80
|
+
if (body.code && body.code !== 'ok') throw new Error('server_config_invalid');
|
|
81
|
+
const result = body.data || body;
|
|
82
|
+
if (!result || typeof result !== 'object' || typeof result.version !== 'string') throw new Error('server_config_invalid');
|
|
83
|
+
resolve(result);
|
|
84
|
+
} catch { reject(new Error('server_config_invalid')); }
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
request.on('timeout', () => request.destroy()); request.on('error', () => reject(new Error('server_config_unavailable')));
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
function detectedTools() {
|
|
91
|
+
const home = os.homedir(), windows = process.platform === 'win32';
|
|
92
|
+
const command = name => (process.env.PATH || '').split(path.delimiter).some(dir => (windows ? ['', '.exe', '.cmd', '.bat'] : ['']).some(ext => {
|
|
93
|
+
try { const file = path.join(dir, name + ext); return fs.statSync(file).isFile() && (windows || (fs.accessSync(file, fs.constants.X_OK), true)); } catch { return false; }
|
|
94
|
+
}));
|
|
95
|
+
const apps = (mac, win) => process.platform === 'darwin' ? ['/Applications', path.join(home, 'Applications')].some(dir => fs.existsSync(path.join(dir, mac))) : windows && !!process.env.LOCALAPPDATA && fs.existsSync(path.join(process.env.LOCALAPPDATA, win));
|
|
96
|
+
return [
|
|
97
|
+
{ label: 'OpenCode(模型与远程 MCP)', value: 'opencode', present: command('opencode') },
|
|
98
|
+
{ label: 'OpenClaw(模型)', value: 'openclaw', present: command('openclaw') },
|
|
99
|
+
{ label: 'Cursor(远程 MCP)', value: 'cursor', present: command('cursor') || apps('Cursor.app', 'Programs/Cursor/Cursor.exe') },
|
|
100
|
+
{ label: 'Claude Desktop(MCP 桥接)', value: 'claude-desktop', present: command('claude-desktop') || apps('Claude.app', 'AnthropicClaude/Claude.exe') },
|
|
101
|
+
{ label: 'Cherry Studio(手动填写说明)', value: 'cherry-studio', present: apps('Cherry Studio.app', 'Programs/Cherry Studio/Cherry Studio.exe') },
|
|
102
|
+
].filter(item => item.present);
|
|
103
|
+
}
|
|
104
|
+
async function currentServer(runtime, prefix, flagServer) {
|
|
105
|
+
// Priority --server > AGENTGUARD_SERVER > saved configuration is resolved by the Go core.
|
|
106
|
+
try { return await queryJSON(runtime, [...prefix, ...(flagServer ? ['--server', flagServer] : []), 'server', 'show']); }
|
|
107
|
+
catch { return {}; }
|
|
108
|
+
}
|
|
109
|
+
function normalizeInput(raw) {
|
|
110
|
+
let value = raw.trim();
|
|
111
|
+
const explicit = /^https?:\/\//i.test(value);
|
|
112
|
+
if (value && !explicit) value = 'https://' + value;
|
|
113
|
+
return { server: serverOrigin(value), explicit };
|
|
114
|
+
}
|
|
115
|
+
async function init(runtime, args) {
|
|
116
|
+
let flagServer = '', root = '';
|
|
117
|
+
for (let i = 0; i < args.length; i++) {
|
|
118
|
+
if (args[i] === '--server' && args[i + 1]) flagServer = args[++i];
|
|
119
|
+
else if (args[i] === '--config-dir' && args[i + 1]) root = args[++i];
|
|
120
|
+
else throw new Error('init_arguments_invalid');
|
|
121
|
+
}
|
|
122
|
+
const prefix = root ? ['--config-dir', root] : [];
|
|
123
|
+
const ask = terminal();
|
|
124
|
+
process.stdout.write('AgentGuard 企业接入助手\n');
|
|
125
|
+
const state = await currentServer(runtime, prefix, flagServer);
|
|
126
|
+
const fallback = typeof state.server === 'string' ? state.server : '';
|
|
127
|
+
let server = '', details;
|
|
128
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
129
|
+
const hint = fallback ? `(${SOURCE_LABEL[state.source] || '当前配置'}:${fallback},回车沿用,或输入新的 IP/域名)` : '(https://企业域名,或 http://IP:端口)';
|
|
130
|
+
const typed = (await question(ask, `企业服务器地址${hint}:`)).trim();
|
|
131
|
+
const chosen = typed || fallback;
|
|
132
|
+
if (!chosen) throw new Error('server_address_invalid');
|
|
133
|
+
const normalized = normalizeInput(chosen);
|
|
134
|
+
try { details = await config(normalized.server); server = normalized.server; break; }
|
|
135
|
+
catch (error) {
|
|
136
|
+
if (attempt === 2) throw error;
|
|
137
|
+
process.stdout.write(`无法连接 ${normalized.server}(${error.message})。${normalized.explicit ? '' : '未写协议时按 HTTPS 访问;如服务器使用 HTTP,请输入 http://地址。'}请重新输入。\n`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (typeof details.enterprise_name === 'string') process.stdout.write(details.enterprise_name.replace(/[\x00-\x1f\x7f-\x9f]/g, '').slice(0, 100) + '\n');
|
|
141
|
+
process.stdout.write('服务器配置可达。\n');
|
|
142
|
+
const choices = [{ label: '浏览器网页授权登录(以控制台当前登录的人授权)', value: 'browser' }, { label: '粘贴个人访问令牌', value: 'token' }];
|
|
143
|
+
let httpFlags = [];
|
|
144
|
+
if (new URL(server).protocol === 'http:') {
|
|
145
|
+
const consent = await queryConsent(runtime, server, prefix);
|
|
146
|
+
if (!consent.acknowledged) {
|
|
147
|
+
const answer = await choose(ask, 'HTTP 会以明文传输登录信息、个人令牌和业务内容,网络中间人可能读取或篡改。是否确认对该服务器继续?', [
|
|
148
|
+
{ label: '取消,不发送登录信息或令牌', value: false },
|
|
149
|
+
{ label: '我已了解明文风险,确认继续', value: true },
|
|
150
|
+
]);
|
|
151
|
+
if (!answer) throw new Error('http_confirmation_required');
|
|
152
|
+
}
|
|
153
|
+
httpFlags = ['--allow-http'];
|
|
154
|
+
}
|
|
155
|
+
let code, loggedInHere = false;
|
|
156
|
+
if (state.logged_in && state.login_server && state.login_server !== server) {
|
|
157
|
+
process.stdout.write(`本机已有登录属于另一台服务器(${state.login_server})。该登录不会发送给新服务器,切换后需要重新登录。\n`);
|
|
158
|
+
const action = await choose(ask, '如何处理旧登录?', [
|
|
159
|
+
{ label: '退出旧服务器登录(撤销旧令牌)并恢复已接入工具的原配置,然后登录新服务器', value: 'switch' },
|
|
160
|
+
{ label: '取消,保持原服务器和登录不变', value: 'cancel' },
|
|
161
|
+
]);
|
|
162
|
+
if (action !== 'switch') throw new Error('server_switch_cancelled');
|
|
163
|
+
code = await run(runtime, [...prefix, 'server', 'set', server, ...httpFlags, '--switch'], '');
|
|
164
|
+
if (code) {
|
|
165
|
+
const local = await choose(ask, '旧服务器登录没有退出。若旧服务器已无法访问,可只清除本机登录:', [
|
|
166
|
+
{ label: '只清除本机旧登录(旧令牌在服务端仍有效,直到到期或由管理员撤销)', value: 'local' },
|
|
167
|
+
{ label: '取消', value: 'cancel' },
|
|
168
|
+
]);
|
|
169
|
+
if (local !== 'local') { process.exitCode = code; return; }
|
|
170
|
+
code = await run(runtime, [...prefix, 'server', 'set', server, ...httpFlags, '--switch', '--local-only'], '');
|
|
171
|
+
if (code) { process.exitCode = code; return; }
|
|
172
|
+
}
|
|
173
|
+
} else if (state.logged_in && state.login_server === server) {
|
|
174
|
+
process.stdout.write('本机已登录该服务器,沿用现有登录。如需换人,请先执行 agentguard logout。\n');
|
|
175
|
+
loggedInHere = true;
|
|
176
|
+
}
|
|
177
|
+
if ((process.env.AGENTGUARD_SERVER || '') && process.env.AGENTGUARD_SERVER.replace(/\/+$/, '') !== server) process.stdout.write(`提示:环境变量 AGENTGUARD_SERVER 指向 ${process.env.AGENTGUARD_SERVER},与本次所选服务器不同;后续命令请取消该变量或用 --server 指定。\n`);
|
|
178
|
+
const method = loggedInHere ? 'existing' : await choose(ask, '选择登录方式', choices);
|
|
179
|
+
if (method === 'existing') code = 0;
|
|
180
|
+
else if (method === 'token') {
|
|
181
|
+
let token = await question(ask, '个人访问令牌(输入隐藏):', true);
|
|
182
|
+
if (!token.trim() || /[\r\n]/.test(token)) throw new Error('personal_token_invalid');
|
|
183
|
+
code = await run(runtime, [...prefix, 'auth', 'token', '--server', server, ...httpFlags, '--defer-notice'], token.trim() + '\n'); token = '';
|
|
184
|
+
} else code = await run(runtime, [...prefix, 'login', '--server', server, ...httpFlags, '--defer-notice'], '');
|
|
185
|
+
if (code) { process.exitCode = code; return; }
|
|
186
|
+
const tools = detectedTools();
|
|
187
|
+
if (!tools.length) process.stdout.write('未检测到本批支持的工具;安装工具后可运行 agentguard connect <工具名>。\n');
|
|
188
|
+
else {
|
|
189
|
+
const selected = await choose(ask, '选择要接入的已检测工具', tools, true);
|
|
190
|
+
for (const tool of selected) { code = await run(runtime, [...prefix, 'connect', tool], ''); if (code) { process.exitCode = code; return; } }
|
|
191
|
+
if (selected.includes('opencode')) process.stdout.write('OpenCode 接入包含本机安全 MCP 配置。请通过 launch 启动,使用 ag_* 工具;实际连接与授权结果以诊断和服务端记录为准。\n');
|
|
192
|
+
}
|
|
193
|
+
process.stdout.write('运行实际诊断:\n'); code = await run(runtime, [...prefix, 'doctor'], ''); process.exitCode = code;
|
|
194
|
+
process.stdout.write(code === 0 ? '初始化完成。用 npx @agentguardx/helper launch <工具名> 启动;全局安装后也可用 agentguard launch <工具名>。\n' : '诊断未全部通过,请按上述实际结果处理。\n');
|
|
195
|
+
}
|
|
196
|
+
module.exports = { init, serverOrigin, detectedTools };
|
package/package.json
CHANGED
|
@@ -1,6 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentguardx/helper",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.3.88",
|
|
4
|
+
"description": "AgentGuard \u5458\u5de5\u63a5\u5165\u52a9\u624b",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=18"
|
|
8
|
+
},
|
|
9
|
+
"bin": {
|
|
10
|
+
"agentguard": "bin/agentguard.cjs"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"bin",
|
|
14
|
+
"lib",
|
|
15
|
+
"release-public.pem",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"preuninstall": "node lib/preuninstall.cjs"
|
|
20
|
+
},
|
|
21
|
+
"optionalDependencies": {
|
|
22
|
+
"@agentguardx/helper-darwin-x64": "0.3.88",
|
|
23
|
+
"@agentguardx/helper-darwin-arm64": "0.3.88",
|
|
24
|
+
"@agentguardx/helper-linux-x64": "0.3.88",
|
|
25
|
+
"@agentguardx/helper-linux-arm64": "0.3.88",
|
|
26
|
+
"@agentguardx/helper-win32-x64": "0.3.88",
|
|
27
|
+
"@agentguardx/helper-win32-arm64": "0.3.88"
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
-----BEGIN PUBLIC KEY-----
|
|
2
|
+
MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAzfALBceoxdwk8B6i+mHv
|
|
3
|
+
7L8DFBMM+WD7/WVOGY33/PVeO1UwGIn1sehHG4FXvFUGXPcEiDYrkAZJ4ZgidGhk
|
|
4
|
+
4W0efJCTLyIHuntRbWfCdhnxg+whTzfnPw0lLQp0PPRPjLWHT0LSQTM47nMvbMEK
|
|
5
|
+
/BbB3aF6FMoIzhib/uNNyjR6eVDDr89hsU5Gk70ki2aIkPCoiWrMbo7NX0yPjuAN
|
|
6
|
+
nSb04i1tQZKM9uYzqGu/IS6UvRIMP9N77PgPY5nHtx5UeN4jSzmedY2FH5CwBdNR
|
|
7
|
+
Ldt5h6n9m6/kxUho5KNWVbCpsg3DgjSOn97LjZqXu2L6liplrHlmo1vHxuodamdf
|
|
8
|
+
BbqBFVC/zLtzrE++ZiYlqQiS30WwY7cHa70Fed5B955uCG2vTFgwTT3Noa/Nv7in
|
|
9
|
+
Tszfi/4IHkXJuyqzR4gOvfgl3A6MdHEsnnna0FoJmFbT8AZxNpHqFRk1RE0s9+F6
|
|
10
|
+
jQadzdy4BMOrMbzg6ga1WJvHCZuSMRijve5N4ZbOgMC3AgMBAAE=
|
|
11
|
+
-----END PUBLIC KEY-----
|