shennian 0.4.2 → 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.
@@ -81,8 +81,8 @@
81
81
  },
82
82
  {
83
83
  "file": "src/agents/detect.js",
84
- "beforeBytes": 3055,
85
- "afterBytes": 1544
84
+ "beforeBytes": 3366,
85
+ "afterBytes": 1504
86
86
  },
87
87
  {
88
88
  "file": "src/agents/external-channel-instructions.js",
@@ -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",
@@ -9,5 +9,5 @@ export type DetectedAgent = {
9
9
  executableFingerprint?: string;
10
10
  models?: ModelInfo[];
11
11
  };
12
- export declare function isPiCodingAgentRuntimeCompatible(piVersion: string | undefined, nodeVersion?: string): boolean;
12
+ export declare function isPiCodingAgentRuntimeCompatible(piVersion: string | undefined): boolean;
13
13
  export declare function detectAgents(): DetectedAgent[];
@@ -1 +1 @@
1
- import{AVAILABLE_BUILTIN_AGENT_TYPES as m}from"@shennian/wire";import{loadConfig as c}from"../config/index.js";import{getBuiltinAgentVersion as l,resolveBuiltinCommand as u}from"./command-spec.js";import{registerCustomRuntimeSecurity as a,resolveCustomRuntimeExecutableFingerprint as f}from"./custom-managed-runtime.js";import{resolveManagedAgentExecutableIdentity as d}from"../security/managed-agent-executable.js";const p=m.filter(t=>t!=="pi");function i(t,r){const o=t?.match(/(?:^|v)(\d+)\.(\d+)\.(\d+)/);if(!o)return!1;const e=o.slice(1,4).map(Number);for(let n=0;n<r.length;n+=1)if(e[n]!==r[n])return e[n]>r[n];return!0}function g(t,r=process.versions.node){return i(r,[22,19,0])&&i(t,[0,85,0])}function A(t){const r=t.models?.length?t.models:t.model?[t.model]:[],o=t.defaultModel??t.model;return r.map(e=>({id:e,name:e,isDefault:e===o}))}function y(){const t=[];for(const o of p){const e=u(o);if(!e)continue;const n=l(o,e);if(o==="pi-coding-agent"&&!g(n))continue;const s=d(e);t.push({type:o,command:e.command,args:e.args,version:n,runtimePlatform:s.platform,runtimeIsWsl:s.isWsl,executableFingerprint:s.executableFingerprint})}const r=c();for(const[o,e]of Object.entries(r.customAgents??{})){const n=`custom:${o}`;a(n,e),t.push({type:n,command:e.command,args:[],version:e.caps.version,runtimePlatform:process.platform,runtimeIsWsl:process.platform==="linux"&&!!(process.env.WSL_DISTRO_NAME||process.env.WSL_INTEROP),executableFingerprint:f(e),models:A(e.caps)})}return t}export{y as detectAgents,g as isPiCodingAgentRuntimeCompatible};
1
+ import{AVAILABLE_BUILTIN_AGENT_TYPES as i}from"@shennian/wire";import{loadConfig as m}from"../config/index.js";import{getBuiltinAgentVersion as c,resolveBuiltinCommand as l}from"./command-spec.js";import{registerCustomRuntimeSecurity as u,resolveCustomRuntimeExecutableFingerprint as a}from"./custom-managed-runtime.js";import{resolveManagedAgentExecutableIdentity as f}from"../security/managed-agent-executable.js";const d=i.filter(t=>t!=="pi");function p(t,r){const o=t?.match(/(?:^|v)(\d+)\.(\d+)\.(\d+)/);if(!o)return!1;const e=o.slice(1,4).map(Number);for(let n=0;n<r.length;n+=1)if(e[n]!==r[n])return e[n]>r[n];return!0}function g(t){return p(t,[0,85,0])}function A(t){const r=t.models?.length?t.models:t.model?[t.model]:[],o=t.defaultModel??t.model;return r.map(e=>({id:e,name:e,isDefault:e===o}))}function y(){const t=[];for(const o of d){const e=l(o);if(!e)continue;const n=c(o,e);if(o==="pi-coding-agent"&&!g(n))continue;const s=f(e);t.push({type:o,command:e.command,args:e.args,version:n,runtimePlatform:s.platform,runtimeIsWsl:s.isWsl,executableFingerprint:s.executableFingerprint})}const r=m();for(const[o,e]of Object.entries(r.customAgents??{})){const n=`custom:${o}`;u(n,e),t.push({type:n,command:e.command,args:[],version:e.caps.version,runtimePlatform:process.platform,runtimeIsWsl:process.platform==="linux"&&!!(process.env.WSL_DISTRO_NAME||process.env.WSL_INTEROP),executableFingerprint:a(e),models:A(e.caps)})}return t}export{y as detectAgents,g as isPiCodingAgentRuntimeCompatible};
@@ -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.2",
3
+ "version": "0.4.4",
4
4
  "description": "Shennian — AI Agent Control Plane CLI",
5
5
  "type": "module",
6
6
  "bin": {