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.
@@ -166,8 +166,8 @@
166
166
  },
167
167
  {
168
168
  "file": "src/agents/pi-managed-extension.js",
169
- "beforeBytes": 8913,
170
- "afterBytes": 8208
169
+ "beforeBytes": 10216,
170
+ "afterBytes": 9261
171
171
  },
172
172
  {
173
173
  "file": "src/agents/pi-managed-model-broker.js",
@@ -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",o=e.allowRoomControl,a=e.allowManagerControl,s=e.allowHostShell,c=`
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(o)};
10
- const ALLOW_MANAGER = ${JSON.stringify(a)};
11
- const ALLOW_HOST_SHELL = ${JSON.stringify(s)};
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 SENSITIVE = /(^|[\\/])(\\.env(?:\\.|$)|\\.ssh|\\.aws|\\.config|\\.shennian|Library[\\/]Keychains|AppData[\\/]Roaming[\\/]Microsoft[\\/]Credentials)([\\/]|$)/i;
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 (SENSITIVE.test(candidate)) throw new Error('managed_path_sensitive');
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 (SENSITIVE.test(full) || e.isSymbolicLink()) continue; if (e.name.toLowerCase().includes(needle)) out.push(path.relative(ROOT, full)); if (e.isDirectory()) queue.push(full); } }
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 (!SENSITIVE.test(full) && !e.isSymbolicLink()) queue.push(full); } continue; } if (!stat.isFile() || 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); }); }
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); const scoped = argv[0] === 'shennian' && ((ALLOW_ROOM && argv[1] === 'room') || (ALLOW_MANAGER && argv[1] === 'manager')); if (!ALLOW_HOST_SHELL && !scoped) throw new Error('managed_shell_command_rejected');
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shennian",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
4
4
  "description": "Shennian — AI Agent Control Plane CLI",
5
5
  "type": "module",
6
6
  "bin": {