178 lines
8.0 KiB
JavaScript
178 lines
8.0 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* gen-capabilities.cjs —— 生成「实例能力清单」(档案 56)
|
||
*
|
||
* 解决什么:agent 每开新会话都要**现场试一遍**才能知道能做什么(实测同一批探测重复 3 轮,
|
||
* 撞同样 4 类墙:/etc 白名单、127.0.0.1 被 SSRF 拒、技能不存在、写边界),用户也跟着反复问
|
||
* "能力有变化吗"。本脚本把**平台事实**固化成两份同源产物:
|
||
* ① /opt/dsh/state/capabilities.json —— 给平台 API / 门户(机读)
|
||
* ② <bundled-skill-dir>/platform-capabilities/SKILL.md —— 给实例内 agent(它可被 skill 机制加载)
|
||
*
|
||
* 用法:node scripts/gen-capabilities.cjs # 生成
|
||
* node scripts/gen-capabilities.cjs --print # 只打印 JSON
|
||
* 幂等、只写上述两个路径;任何探测失败只降级为 "unknown",不报错退出。
|
||
*/
|
||
'use strict'
|
||
const { execFileSync } = require('node:child_process')
|
||
const { existsSync, mkdirSync, readdirSync, statSync, writeFileSync } = require('node:fs')
|
||
const { join } = require('node:path')
|
||
|
||
const STATE = process.env.DSH_STATE_DIR ?? '/opt/dsh/state'
|
||
const OUT_JSON = join(STATE, 'capabilities.json')
|
||
const BUNDLED = process.env.DSH_BUNDLED_SKILL_DIR ?? '/var/lib/dshs/bundled-skills'
|
||
const USERS = process.env.DSH_USERS_DIR ?? '/var/lib/dshs/users'
|
||
const PERMISSION_MODE = process.env.DSH_PERMISSION_MODE ?? 'danger-full-access'
|
||
|
||
const run = (cmd, args) => {
|
||
try {
|
||
return execFileSync(cmd, args, { encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] })
|
||
.trim().split('\n')[0].slice(0, 80)
|
||
} catch {
|
||
return 'MISSING'
|
||
}
|
||
}
|
||
|
||
// ── 1) 工具链(宿主层安装 → 实例经 /usr 只读可见)──────────────
|
||
const tools = {
|
||
node: run('node', ['-v']),
|
||
npm: run('npm', ['-v']),
|
||
pnpm: run('pnpm', ['-v']),
|
||
python3: run('python3', ['--version']),
|
||
git: run('git', ['--version']),
|
||
curl: run('curl', ['--version']),
|
||
ripgrep: run('rg', ['--version']),
|
||
jq: run('jq', ['--version']),
|
||
ffmpeg: run('ffmpeg', ['-version']),
|
||
bubblewrap: run('bwrap', ['--version']),
|
||
}
|
||
|
||
// ── 2) 技能清单(共享层 + 各用户个人层)────────────────────────
|
||
const listSkills = (dir) => {
|
||
if (!existsSync(dir)) return []
|
||
try {
|
||
return readdirSync(dir).filter((n) => {
|
||
try { return statSync(join(dir, n)).isDirectory() } catch { return false }
|
||
})
|
||
} catch {
|
||
return []
|
||
}
|
||
}
|
||
const sharedSkills = listSkills(BUNDLED)
|
||
const perUserSkills = {}
|
||
if (existsSync(USERS)) {
|
||
for (const uid of readdirSync(USERS)) {
|
||
const s = listSkills(join(USERS, uid, 'home', 'skills'))
|
||
if (s.length) perUserSkills[uid.slice(0, 8)] = s
|
||
}
|
||
}
|
||
|
||
// ── 3) 能力清单(结论层;每项都可在平台上核对)──────────────────
|
||
const capabilities = {
|
||
generatedAt: new Date().toISOString(),
|
||
permissionMode: PERMISSION_MODE,
|
||
note: PERMISSION_MODE === 'danger-full-access'
|
||
? '当前平台默认档位 = 完全权限(不在实例内叠加文件沙箱、不弹审批)。dsh 界面里可切换。'
|
||
: '当前平台默认档位 = workspace-write(需要可用的沙箱后端;本机不可用时会拒绝任何 shell)。',
|
||
read: {
|
||
allowed: ['/usr(工具链与运行时,只读)', '/etc 白名单(见平台补丁)', '/proc /dev(沙箱内)', '自己的工作区 ws/ 与实例 home/'],
|
||
denied: ['其他用户目录(users/<其他 uuid>/)', '宿主内部路径与凭据', '/etc 白名单以外的配置'],
|
||
},
|
||
write: {
|
||
allowed: ['工作区 ws/**(含子目录)', '实例 home/**(如 home/skills、home/profiles)', '私有 /tmp'],
|
||
denied: ['/usr /etc /var 等系统路径(只读挂载)', '平台策略文件(profile 的 cordis.patch.yml / package.json / pnpm-lock.yaml 为只读)'],
|
||
},
|
||
network: {
|
||
egress: '可出公网(仅封云元数据端点 100.100.100.200)',
|
||
denied: ['宿主 loopback 127.0.0.0/8(含 127.0.0.1:3080 门户、:22 等)', '内网卡与 docker0 网关', '平台公网 EIP'],
|
||
hint: '**不要把 127.0.0.1 当作可用入口**:web_fetch 会以「resolves to a non-public IP」被拒;实例内也不可访问门户。',
|
||
},
|
||
tools,
|
||
skills: {
|
||
shared: sharedSkills,
|
||
perUser: perUserSkills,
|
||
hint: sharedSkills.length === 0 && Object.keys(perUserSkills).length === 0
|
||
? '当前平台**没有任何已注册技能**(共享层与个人层均为空)→ 调用 skill 工具必然报 unknown,请改用 bash/文件工具完成。'
|
||
: '技能来自共享只读层与用户个人层;调用前先按名字确认存在于上表。',
|
||
},
|
||
fileDelivery: {
|
||
hint: '把产出交给用户时:**用工作区相对路径**(如 `报告.md`),并提示「点会话页右下角『我的文件』可查看/下载」。',
|
||
avoid: ['不要给 `/var/lib/dshs/users/...` 这类服务器绝对路径', '不要给 127.0.0.1:<port> 链接(用户浏览器打不开)'],
|
||
howToShare: '用户在会话页右下角「我的文件」面板里可浏览工作区并下载任意文件(平台代理,不暴露宿主路径)。',
|
||
},
|
||
}
|
||
|
||
// ── 4) 写 JSON ────────────────────────────────────────────────
|
||
try {
|
||
mkdirSync(STATE, { recursive: true })
|
||
} catch { /* 已存在 */ }
|
||
if (!process.argv.includes('--print')) {
|
||
writeFileSync(OUT_JSON, JSON.stringify(capabilities, null, 2) + '\n')
|
||
}
|
||
|
||
// ── 5) 写 agent 可读的 SKILL.md(共享只读层)──────────────────
|
||
const fmtKV = (o) => Object.entries(o).map(([k, v]) => `| ${k} | ${v} |`).join('\n')
|
||
const skillMd = `---
|
||
name: platform-capabilities
|
||
description: 本 dsh 实例(托管在 dshs 平台上)的**能力边界与交付约定**。开工前读它可省去自行探测;包含可读/可写路径、网络边界、可用工具版本、已注册技能清单、以及把文件交给用户的正确方式。
|
||
---
|
||
|
||
# 平台能力清单(由 \`scripts/gen-capabilities.cjs\` 生成 · ${capabilities.generatedAt})
|
||
|
||
> **本文件是权威结论,不要靠现场试探推断能力**。若与实测冲突,先按本文件执行,并把差异报告给用户。
|
||
|
||
## 1. 权限档位
|
||
${capabilities.note}
|
||
|
||
## 2. 可读
|
||
| 允许 | 说明 |
|
||
|---|---|
|
||
${capabilities.read.allowed.map((x) => `| ${x} | — |`).join('\n')}
|
||
|
||
**不可读**:${capabilities.read.denied.join(';')}
|
||
|
||
## 3. 可写
|
||
**可写**:${capabilities.write.allowed.join(';')}
|
||
**不可写**:${capabilities.write.denied.join(';')}
|
||
|
||
## 4. 网络
|
||
- 出网:${capabilities.network.egress}
|
||
- **不可达**:${capabilities.network.denied.join(';')}
|
||
- ⚠️ ${capabilities.network.hint}
|
||
|
||
## 5. 可用工具(宿主层安装,全部实例共享)
|
||
| 工具 | 版本 |
|
||
|---|---|
|
||
${fmtKV(tools)}
|
||
|
||
## 6. 已注册技能
|
||
- 共享层(所有人可用):${sharedSkills.length ? sharedSkills.join(', ') : '**(空)**'}
|
||
- 个人层:${Object.keys(perUserSkills).length ? JSON.stringify(perUserSkills) : '**(空)**'}
|
||
- ⚠️ ${capabilities.skills.hint}
|
||
|
||
## 7. 把文件交给用户(重要)
|
||
${capabilities.fileDelivery.hint}
|
||
|
||
- ❌ 不要做:${capabilities.fileDelivery.avoid.join(';')}
|
||
- ✅ 怎么做:${capabilities.fileDelivery.howToShare}
|
||
`
|
||
|
||
if (!process.argv.includes('--print')) {
|
||
const dir = join(BUNDLED, 'platform-capabilities')
|
||
try {
|
||
mkdirSync(dir, { recursive: true })
|
||
writeFileSync(join(dir, 'SKILL.md'), skillMd)
|
||
} catch (e) {
|
||
console.error('写 SKILL.md 失败:', e.message)
|
||
}
|
||
}
|
||
|
||
if (process.argv.includes('--print')) {
|
||
console.log(JSON.stringify(capabilities, null, 2))
|
||
} else {
|
||
console.log('已生成:')
|
||
console.log(' ' + OUT_JSON)
|
||
console.log(' ' + join(BUNDLED, 'platform-capabilities', 'SKILL.md'))
|
||
console.log(' 工具: ' + Object.entries(tools).map(([k, v]) => `${k}=${v}`).join(' '))
|
||
console.log(' 技能: 共享 ' + sharedSkills.length + ' 个,个人层用户 ' + Object.keys(perUserSkills).length + ' 个')
|
||
}
|