shennian 0.4.3 → 0.4.4
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.
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import t from"node:fs";import n from"node:path";function p(e){const r=n.resolve(e.policy.adapterPolicy.workDir),i=e.policy.adapterPolicy.fileAccess!=="workspace-read",
|
|
1
|
+
import t from"node:fs";import n from"node:path";function p(e){const r=t.realpathSync(n.resolve(e.policy.adapterPolicy.workDir)),i=e.policy.adapterPolicy.fileAccess!=="workspace-read",a=e.allowRoomControl,s=e.allowManagerControl,o=e.allowHostShell,c=`
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
@@ -6,25 +6,33 @@ import { Type } from 'typebox';
|
|
|
6
6
|
|
|
7
7
|
const ROOT = ${JSON.stringify(r)};
|
|
8
8
|
const WRITABLE = ${JSON.stringify(i)};
|
|
9
|
-
const ALLOW_ROOM = ${JSON.stringify(
|
|
10
|
-
const ALLOW_MANAGER = ${JSON.stringify(
|
|
11
|
-
const ALLOW_HOST_SHELL = ${JSON.stringify(
|
|
9
|
+
const ALLOW_ROOM = ${JSON.stringify(a)};
|
|
10
|
+
const ALLOW_MANAGER = ${JSON.stringify(s)};
|
|
11
|
+
const ALLOW_HOST_SHELL = ${JSON.stringify(o)};
|
|
12
12
|
const MAX_READ = 1024 * 1024;
|
|
13
13
|
const MAX_WRITE = 4 * 1024 * 1024;
|
|
14
|
-
const
|
|
14
|
+
const SENSITIVE_NAMES = new Set(['.ssh', '.aws', '.config', '.shennian']);
|
|
15
|
+
function sensitive(candidate) {
|
|
16
|
+
const parts = String(candidate).split(/[\\\\/]+/).filter(Boolean).map(part => part.toLowerCase());
|
|
17
|
+
if (parts.some(part => part === '.env' || part.startsWith('.env.') || SENSITIVE_NAMES.has(part))) return true;
|
|
18
|
+
const joined = '/' + parts.join('/') + '/';
|
|
19
|
+
return joined.includes('/library/keychains/') || joined.includes('/appdata/roaming/microsoft/credentials/');
|
|
20
|
+
}
|
|
15
21
|
|
|
16
22
|
function inside(root, candidate) {
|
|
17
23
|
const relative = path.relative(root, candidate);
|
|
18
24
|
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
19
25
|
}
|
|
20
26
|
function rejectSensitive(candidate) {
|
|
21
|
-
if (
|
|
27
|
+
if (sensitive(candidate)) throw new Error('managed_path_sensitive');
|
|
22
28
|
}
|
|
23
29
|
function resolveExisting(raw) {
|
|
24
30
|
const candidate = path.resolve(ROOT, String(raw || '.'));
|
|
25
31
|
rejectSensitive(candidate);
|
|
26
32
|
const real = fs.realpathSync(candidate);
|
|
27
33
|
if (!inside(ROOT, real)) throw new Error('managed_path_outside_workspace');
|
|
34
|
+
const stat = fs.lstatSync(real);
|
|
35
|
+
if (stat.isFile() && stat.nlink > 1) throw new Error('managed_path_link_rejected');
|
|
28
36
|
return real;
|
|
29
37
|
}
|
|
30
38
|
function resolveWrite(raw) {
|
|
@@ -55,6 +63,20 @@ function tokenize(command) {
|
|
|
55
63
|
if (current) result.push(current);
|
|
56
64
|
return result;
|
|
57
65
|
}
|
|
66
|
+
function commandAllowed(argv) {
|
|
67
|
+
if (argv[0] !== 'shennian') return false;
|
|
68
|
+
if (ALLOW_ROOM && argv[1] === 'room') {
|
|
69
|
+
return ['status', 'read', 'send', 'pause'].includes(argv[2]) ||
|
|
70
|
+
(argv[2] === 'attachment' && argv[3] === 'get');
|
|
71
|
+
}
|
|
72
|
+
if (ALLOW_MANAGER && argv[1] === 'manager') {
|
|
73
|
+
if (argv[2] === 'memory') return argv[3] === 'path';
|
|
74
|
+
if (argv[2] !== 'sessions') return false;
|
|
75
|
+
if (['list', 'start', 'send', 'stop', 'read'].includes(argv[3])) return true;
|
|
76
|
+
return argv[3] === 'queue' && ['list', 'edit', 'delete'].includes(argv[4]);
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
58
80
|
|
|
59
81
|
export default function (pi) {
|
|
60
82
|
pi.registerTool({ name: 'read', label: 'Read', description: 'Read a file inside the authorized workspace.', parameters: Type.Object({ path: Type.String(), offset: Type.Optional(Type.Number()), limit: Type.Optional(Type.Number()) }), async execute(_id, p) {
|
|
@@ -63,16 +85,16 @@ export default function (pi) {
|
|
|
63
85
|
return textResult(lines.slice(offset - 1, offset - 1 + limit).join('\\n'));
|
|
64
86
|
}});
|
|
65
87
|
pi.registerTool({ name: 'ls', label: 'List', description: 'List a directory inside the authorized workspace.', parameters: Type.Object({ path: Type.Optional(Type.String()) }), async execute(_id, p) {
|
|
66
|
-
const dir = resolveExisting(p.path || '.'); return textResult(fs.readdirSync(dir, { withFileTypes: true }).slice(0, 1000).map(e => e.name + (e.isDirectory() ? '/' : '')).join('\\n'));
|
|
88
|
+
const dir = resolveExisting(p.path || '.'); return textResult(fs.readdirSync(dir, { withFileTypes: true }).filter(e => !sensitive(path.join(dir, e.name))).slice(0, 1000).map(e => e.name + (e.isDirectory() ? '/' : '')).join('\\n'));
|
|
67
89
|
}});
|
|
68
90
|
pi.registerTool({ name: 'find', label: 'Find', description: 'Find file names inside the authorized workspace.', parameters: Type.Object({ pattern: Type.String(), path: Type.Optional(Type.String()) }), async execute(_id, p) {
|
|
69
91
|
const base = resolveExisting(p.path || '.'); const needle = String(p.pattern).toLowerCase(); const out = []; const queue = [base];
|
|
70
|
-
while (queue.length && out.length < 1000) { const dir = queue.shift(); for (const e of fs.readdirSync(dir, { withFileTypes: true })) { const full = path.join(dir, e.name); if (
|
|
92
|
+
while (queue.length && out.length < 1000) { const dir = queue.shift(); for (const e of fs.readdirSync(dir, { withFileTypes: true })) { const full = path.join(dir, e.name); if (sensitive(full) || e.isSymbolicLink()) continue; const stat = fs.lstatSync(full); if (stat.isFile() && stat.nlink > 1) continue; if (e.name.toLowerCase().includes(needle)) out.push(path.relative(ROOT, full)); if (e.isDirectory()) queue.push(full); } }
|
|
71
93
|
return textResult(out.join('\\n'));
|
|
72
94
|
}});
|
|
73
95
|
pi.registerTool({ name: 'grep', label: 'Grep', description: 'Search text files inside the authorized workspace.', parameters: Type.Object({ pattern: Type.String(), path: Type.Optional(Type.String()) }), async execute(_id, p) {
|
|
74
96
|
const base = resolveExisting(p.path || '.'); const regex = new RegExp(String(p.pattern), 'i'); const out = []; const queue = [base];
|
|
75
|
-
while (queue.length && out.length < 1000) { const next = queue.shift(); const stat = fs.statSync(next); if (stat.isDirectory()) { for (const e of fs.readdirSync(next, { withFileTypes: true })) { const full = path.join(next, e.name); if (!
|
|
97
|
+
while (queue.length && out.length < 1000) { const next = queue.shift(); const stat = fs.statSync(next); if (stat.isDirectory()) { for (const e of fs.readdirSync(next, { withFileTypes: true })) { const full = path.join(next, e.name); if (!sensitive(full) && !e.isSymbolicLink()) queue.push(full); } continue; } if (!stat.isFile() || stat.nlink > 1 || stat.size > MAX_READ) continue; const lines = fs.readFileSync(next, 'utf8').split(/\\r?\\n/); lines.forEach((line, i) => { if (out.length < 1000 && regex.test(line)) out.push(path.relative(ROOT, next) + ':' + (i + 1) + ':' + line); }); }
|
|
76
98
|
return textResult(out.join('\\n'));
|
|
77
99
|
}});
|
|
78
100
|
pi.registerTool({ name: 'write', label: 'Write', description: 'Write a file inside the authorized workspace.', parameters: Type.Object({ path: Type.String(), content: Type.String() }), async execute(_id, p) {
|
|
@@ -82,7 +104,7 @@ export default function (pi) {
|
|
|
82
104
|
const file = resolveWrite(p.path); const current = fs.readFileSync(file, 'utf8'); if (!p.oldText || !current.includes(p.oldText)) throw new Error('managed_edit_target_not_found'); const next = current.replace(p.oldText, p.newText); if (Buffer.byteLength(next, 'utf8') > MAX_WRITE) throw new Error('managed_write_size_rejected'); fs.writeFileSync(file, next, { encoding: 'utf8', flag: 'w' }); return textResult('Edited ' + path.relative(ROOT, file));
|
|
83
105
|
}});
|
|
84
106
|
pi.registerTool({ name: 'bash', label: 'Shennian control', description: 'Run only capability-scoped shennian room or manager commands. General shell commands are unavailable.', parameters: Type.Object({ command: Type.String() }), async execute(_id, p, signal) {
|
|
85
|
-
const argv = tokenize(p.command);
|
|
107
|
+
const argv = tokenize(p.command); if (!ALLOW_HOST_SHELL && !commandAllowed(argv)) throw new Error('managed_shell_command_rejected');
|
|
86
108
|
return await new Promise((resolve, reject) => { const child = ALLOW_HOST_SHELL ? spawn(String(p.command), [], { cwd: ROOT, shell: true, env: process.env, windowsHide: true }) : spawn('shennian', argv.slice(1), { cwd: ROOT, shell: false, env: process.env, windowsHide: true }); let output = ''; const add = c => { output = (output + String(c)).slice(0, 256 * 1024); }; child.stdout.on('data', add); child.stderr.on('data', add); signal?.addEventListener('abort', () => child.kill('SIGTERM'), { once: true }); child.once('error', reject); child.once('close', code => resolve(textResult(output + '\\n[exit ' + code + ']'))); });
|
|
87
109
|
}});
|
|
88
110
|
}
|