helloagents 3.0.12 → 3.0.15-beta.1
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/.claude-plugin/marketplace.json +6 -4
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +169 -30
- package/README_CN.md +169 -30
- package/bootstrap-lite.md +27 -20
- package/bootstrap.md +30 -23
- package/cli.mjs +119 -11
- package/gemini-extension.json +1 -1
- package/install.ps1 +125 -0
- package/install.sh +118 -0
- package/package.json +23 -4
- package/scripts/advisor-state.mjs +36 -63
- package/scripts/capability-registry.mjs +3 -3
- package/scripts/cli-branch.mjs +84 -0
- package/scripts/cli-codex-config.mjs +11 -20
- package/scripts/cli-codex.mjs +32 -38
- package/scripts/cli-doctor-render.mjs +4 -0
- package/scripts/cli-doctor.mjs +40 -30
- package/scripts/cli-host-detect.mjs +0 -1
- package/scripts/cli-hosts.mjs +16 -8
- package/scripts/cli-lifecycle-hosts.mjs +92 -27
- package/scripts/cli-lifecycle.mjs +9 -7
- package/scripts/cli-messages.mjs +34 -16
- package/scripts/cli-runtime-carrier.mjs +36 -0
- package/scripts/cli-runtime-root.mjs +72 -0
- package/scripts/cli-toml.mjs +0 -79
- package/scripts/cli-utils.mjs +30 -4
- package/scripts/closeout-state.mjs +35 -62
- package/scripts/delivery-gate-messages.mjs +70 -0
- package/scripts/delivery-gate.mjs +9 -75
- package/scripts/guard-rules.mjs +42 -42
- package/scripts/guard.mjs +44 -24
- package/scripts/notify-context.mjs +19 -28
- package/scripts/notify-gates.mjs +2 -0
- package/scripts/notify-route.mjs +9 -7
- package/scripts/notify-ui.mjs +46 -33
- package/scripts/notify.mjs +60 -32
- package/scripts/project-storage.mjs +35 -66
- package/scripts/ralph-loop.mjs +36 -31
- package/scripts/replay-state.mjs +31 -128
- package/scripts/review-state.mjs +34 -61
- package/scripts/runtime-artifacts.mjs +95 -0
- package/scripts/runtime-context.mjs +35 -29
- package/scripts/runtime-scope.mjs +313 -0
- package/scripts/session-capsule.mjs +202 -0
- package/scripts/turn-state-cli.mjs +17 -0
- package/scripts/turn-state.mjs +185 -66
- package/scripts/turn-stop-gate.mjs +24 -6
- package/scripts/verify-state.mjs +34 -85
- package/scripts/visual-state.mjs +38 -65
- package/scripts/workflow-core.mjs +2 -2
- package/scripts/workflow-plan-files.mjs +1 -1
- package/scripts/workflow-recommendation.mjs +17 -13
- package/scripts/workflow-state.mjs +5 -5
- package/skills/commands/build/SKILL.md +1 -1
- package/skills/commands/commit/SKILL.md +1 -1
- package/skills/commands/help/SKILL.md +3 -3
- package/skills/commands/loop/SKILL.md +1 -1
- package/skills/commands/plan/SKILL.md +8 -6
- package/skills/commands/prd/SKILL.md +5 -3
- package/skills/commands/verify/SKILL.md +5 -5
- package/skills/hello-debug/SKILL.md +20 -3
- package/skills/hello-review/SKILL.md +2 -2
- package/skills/hello-subagent/SKILL.md +2 -2
- package/skills/hello-test/SKILL.md +6 -2
- package/skills/hello-ui/SKILL.md +4 -4
- package/skills/hello-verify/SKILL.md +10 -7
- package/skills/helloagents/SKILL.md +12 -7
- package/templates/context.md +6 -0
- package/templates/plans/plan.md +3 -0
- package/templates/plans/tasks.md +8 -3
package/scripts/guard-rules.mjs
CHANGED
|
@@ -2,29 +2,29 @@ import { readFileSync } from 'node:fs'
|
|
|
2
2
|
import { dirname, join } from 'node:path'
|
|
3
3
|
|
|
4
4
|
export const DANGEROUS_PATTERNS = [
|
|
5
|
-
{ pattern: /(sudo\s+)?rm\s+(-[a-zA-Z]*f[a-zA-Z]*\s+)?(-[a-zA-Z]*r[a-zA-Z]*\s+)?(\/|~|\*)/, reason: '
|
|
6
|
-
{ pattern: /(sudo\s+)?rm\s+(-[a-zA-Z]*r[a-zA-Z]*\s+)?(-[a-zA-Z]*f[a-zA-Z]*\s+)?(\/|~|\*)/, reason: '
|
|
7
|
-
{ pattern: /(sudo\s+)?rm\s+--recursive/, reason: '
|
|
8
|
-
{ pattern: /(sudo\s+)?rm\s+-[a-zA-Z]*r[a-zA-Z]*\s+\.\.?(\s|$)/, reason: '
|
|
9
|
-
{ pattern: /\bcmd(?:\.exe)?\s*\/c\b/i, reason: '
|
|
10
|
-
{ pattern: /\bStart-Process\s+cmd(?:\.exe)?\b/i, reason: '
|
|
11
|
-
{ pattern: /git\s+push\s+(-f|--force)/, reason: '
|
|
12
|
-
{ pattern: /git\s+reset\s+--hard/, reason: '
|
|
13
|
-
{ pattern: /DROP\s+(DATABASE|TABLE|SCHEMA)/i, reason: '
|
|
14
|
-
{ pattern: /\bTRUNCATE(?:\s+TABLE)?\b/i, reason: '
|
|
15
|
-
{ pattern: /chmod\s+777/, reason: '
|
|
16
|
-
{ pattern: /mkfs\b/, reason: '
|
|
17
|
-
{ pattern: /dd\s+.*of=\/dev\//, reason: '
|
|
18
|
-
{ pattern: /FLUSHALL|FLUSHDB/i, reason: 'Redis
|
|
5
|
+
{ pattern: /(sudo\s+)?rm\s+(-[a-zA-Z]*f[a-zA-Z]*\s+)?(-[a-zA-Z]*r[a-zA-Z]*\s+)?(\/|~|\*)/, reason: '递归删除关键路径' },
|
|
6
|
+
{ pattern: /(sudo\s+)?rm\s+(-[a-zA-Z]*r[a-zA-Z]*\s+)?(-[a-zA-Z]*f[a-zA-Z]*\s+)?(\/|~|\*)/, reason: '递归删除关键路径' },
|
|
7
|
+
{ pattern: /(sudo\s+)?rm\s+--recursive/, reason: '递归删除命令' },
|
|
8
|
+
{ pattern: /(sudo\s+)?rm\s+-[a-zA-Z]*r[a-zA-Z]*\s+\.\.?(\s|$)/, reason: '递归删除当前目录或父目录' },
|
|
9
|
+
{ pattern: /\bcmd(?:\.exe)?\s*\/c\b/i, reason: '嵌套 cmd 会绕过 PowerShell 安全规则' },
|
|
10
|
+
{ pattern: /\bStart-Process\s+cmd(?:\.exe)?\b/i, reason: '嵌套 cmd 会绕过 PowerShell 安全规则' },
|
|
11
|
+
{ pattern: /git\s+push\s+(-f|--force)/, reason: '强制推送风险高,必须明确分支与授权' },
|
|
12
|
+
{ pattern: /git\s+reset\s+--hard/, reason: '硬重置会丢弃本地变更' },
|
|
13
|
+
{ pattern: /DROP\s+(DATABASE|TABLE|SCHEMA)/i, reason: '数据库破坏性命令' },
|
|
14
|
+
{ pattern: /\bTRUNCATE(?:\s+TABLE)?\b/i, reason: '表数据清空命令' },
|
|
15
|
+
{ pattern: /chmod\s+777/, reason: '全局可写权限风险高' },
|
|
16
|
+
{ pattern: /mkfs\b/, reason: '文件系统格式化命令' },
|
|
17
|
+
{ pattern: /dd\s+.*of=\/dev\//, reason: '直接写入设备' },
|
|
18
|
+
{ pattern: /FLUSHALL|FLUSHDB/i, reason: 'Redis 数据清空命令' },
|
|
19
19
|
]
|
|
20
20
|
|
|
21
21
|
export const HIGH_RISK_COMMAND_PATTERNS = [
|
|
22
|
-
{ pattern: /\bnpm\s+publish\b/i, reason: '
|
|
23
|
-
{ pattern: /\bgh\s+release\s+create\b/i, reason: '
|
|
24
|
-
{ pattern: /\bterraform\s+(apply|destroy)\b/i, reason: '
|
|
25
|
-
{ pattern: /\b(kubectl|helm)\s+(apply|delete|upgrade|rollback|set|rollout)\b/i, reason: '
|
|
26
|
-
{ pattern: /\b(prisma|drizzle-kit|sequelize-cli|typeorm)\b.*\b(migrate|migration)\b/i, reason: '
|
|
27
|
-
{ pattern: /\b(vercel|wrangler|netlify|flyctl|fly)\b.*\b(deploy|publish)\b/i, reason: '
|
|
22
|
+
{ pattern: /\bnpm\s+publish\b/i, reason: '包发布命令', gate: 'post-verify' },
|
|
23
|
+
{ pattern: /\bgh\s+release\s+create\b/i, reason: '发布 release 命令', gate: 'post-verify' },
|
|
24
|
+
{ pattern: /\bterraform\s+(apply|destroy)\b/i, reason: '基础设施变更命令', gate: 'post-verify' },
|
|
25
|
+
{ pattern: /\b(kubectl|helm)\s+(apply|delete|upgrade|rollback|set|rollout)\b/i, reason: '集群变更命令', gate: 'post-verify' },
|
|
26
|
+
{ pattern: /\b(prisma|drizzle-kit|sequelize-cli|typeorm)\b.*\b(migrate|migration)\b/i, reason: '数据库迁移命令', gate: 'plan-first' },
|
|
27
|
+
{ pattern: /\b(vercel|wrangler|netlify|flyctl|fly)\b.*\b(deploy|publish)\b/i, reason: '部署命令', gate: 'post-verify' },
|
|
28
28
|
]
|
|
29
29
|
|
|
30
30
|
export const IDEA_SIDE_EFFECT_COMMAND_PATTERNS = [
|
|
@@ -36,20 +36,20 @@ export const IDEA_SIDE_EFFECT_COMMAND_PATTERNS = [
|
|
|
36
36
|
]
|
|
37
37
|
|
|
38
38
|
const SECRET_PATTERNS = [
|
|
39
|
-
{ pattern: /AKIA[0-9A-Z]{16}/, reason: 'AWS Access Key ID
|
|
40
|
-
{ pattern: /ghp_[a-zA-Z0-9]{36}/, reason: 'GitHub Personal Access Token
|
|
41
|
-
{ pattern: /github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59}/, reason: 'GitHub Fine-grained PAT
|
|
42
|
-
{ pattern: /sk-[a-zA-Z0-9]{20,}/, reason: 'API secret key
|
|
43
|
-
{ pattern: /key-[a-zA-Z0-9]{20,}/, reason: 'API key
|
|
44
|
-
{ pattern: /-----BEGIN (RSA |EC |DSA )?PRIVATE KEY-----/, reason: '
|
|
45
|
-
{ pattern: /password\s*[:=]\s*["'][^"']{4,}["']/i, reason: '
|
|
46
|
-
{ pattern: /secret\s*[:=]\s*["'][^"']{4,}["']/i, reason: '
|
|
47
|
-
{ pattern: /AIza[0-9A-Za-z\-_]{35}/, reason: 'Google API Key
|
|
48
|
-
{ pattern: /xox[bpras]-[0-9a-zA-Z\-]+/, reason: 'Slack Token
|
|
49
|
-
{ pattern: /eyJ[A-Za-z0-9\-_]+\.eyJ[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_.+/=]+/, reason: 'JWT token
|
|
50
|
-
{ pattern: /(postgres|mysql|mongodb(\+srv)?):\/\/[^:]+:[^@]+@/i, reason: '
|
|
51
|
-
{ pattern: /sk_live_[a-zA-Z0-9]{24,}/, reason: 'Stripe Secret Key
|
|
52
|
-
{ pattern: /sk-ant-[a-zA-Z0-9\-]{20,}/, reason: 'Anthropic API Key
|
|
39
|
+
{ pattern: /AKIA[0-9A-Z]{16}/, reason: '检测到 AWS Access Key ID' },
|
|
40
|
+
{ pattern: /ghp_[a-zA-Z0-9]{36}/, reason: '检测到 GitHub Personal Access Token' },
|
|
41
|
+
{ pattern: /github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59}/, reason: '检测到 GitHub Fine-grained PAT' },
|
|
42
|
+
{ pattern: /sk-[a-zA-Z0-9]{20,}/, reason: '检测到 API secret key(sk-)' },
|
|
43
|
+
{ pattern: /key-[a-zA-Z0-9]{20,}/, reason: '检测到 API key(key-)' },
|
|
44
|
+
{ pattern: /-----BEGIN (RSA |EC |DSA )?PRIVATE KEY-----/, reason: '检测到私钥' },
|
|
45
|
+
{ pattern: /password\s*[:=]\s*["'][^"']{4,}["']/i, reason: '检测到硬编码密码' },
|
|
46
|
+
{ pattern: /secret\s*[:=]\s*["'][^"']{4,}["']/i, reason: '检测到硬编码密钥' },
|
|
47
|
+
{ pattern: /AIza[0-9A-Za-z\-_]{35}/, reason: '检测到 Google API Key' },
|
|
48
|
+
{ pattern: /xox[bpras]-[0-9a-zA-Z\-]+/, reason: '检测到 Slack Token' },
|
|
49
|
+
{ pattern: /eyJ[A-Za-z0-9\-_]+\.eyJ[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_.+/=]+/, reason: '检测到 JWT token' },
|
|
50
|
+
{ pattern: /(postgres|mysql|mongodb(\+srv)?):\/\/[^:]+:[^@]+@/i, reason: '检测到包含凭据的数据库连接串' },
|
|
51
|
+
{ pattern: /sk_live_[a-zA-Z0-9]{24,}/, reason: '检测到 Stripe Secret Key' },
|
|
52
|
+
{ pattern: /sk-ant-[a-zA-Z0-9\-]{20,}/, reason: '检测到 Anthropic API Key' },
|
|
53
53
|
]
|
|
54
54
|
|
|
55
55
|
export function scanForSecrets(content) {
|
|
@@ -81,13 +81,13 @@ export function scanShellSafetyWarnings(command = '') {
|
|
|
81
81
|
.map((entry) => entry.trim())
|
|
82
82
|
.filter(Boolean)
|
|
83
83
|
if (logicalLines.length > 3) {
|
|
84
|
-
warnings.push('PowerShell
|
|
84
|
+
warnings.push('PowerShell 内联脚本超过 3 个逻辑行,建议改用临时 .ps1 文件')
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
const fileOps = normalized.match(/\b(remove-item|move-item|copy-item|new-item|set-content|add-content|out-file|mkdir|md|touch|cp|copy|mv|move|ren|rename|del|erase|rm|rmdir)\b/ig) || []
|
|
89
89
|
if (fileOps.length > 1 && /[;\r\n]/.test(normalized)) {
|
|
90
|
-
warnings.push('
|
|
90
|
+
warnings.push('单条 shell 命令串联了多个文件操作,建议拆成独立命令')
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
return warnings
|
|
@@ -99,11 +99,11 @@ export function scanUnrequestedFiles(filePath, toolName) {
|
|
|
99
99
|
const warnings = []
|
|
100
100
|
|
|
101
101
|
const patterns = [
|
|
102
|
-
{ pattern: /^(SUMMARY|NOTES|TODO|SCRATCH|TEMP)\.(md|txt)$/i, reason:
|
|
102
|
+
{ pattern: /^(SUMMARY|NOTES|TODO|SCRATCH|TEMP)\.(md|txt)$/i, reason: `检测到未请求的文件创建:${basename}` },
|
|
103
103
|
{
|
|
104
104
|
pattern: /^README.*\.md$/i,
|
|
105
105
|
matches: () => filePath.replace(/\\/g, '/').split('/').length > 4,
|
|
106
|
-
reason:
|
|
106
|
+
reason: `检测到嵌套路径中的可疑 README 创建:${basename}`,
|
|
107
107
|
},
|
|
108
108
|
]
|
|
109
109
|
|
|
@@ -120,12 +120,12 @@ export function scanDangerousPackages(content, filePath) {
|
|
|
120
120
|
if (filePath.endsWith('package.json')) {
|
|
121
121
|
const dangerousScripts = /("(preinstall|postinstall|preuninstall)")\s*:\s*"[^"]*\b(curl|wget|bash|sh|eval|exec)\b/i
|
|
122
122
|
if (dangerousScripts.test(content)) {
|
|
123
|
-
warnings.push('
|
|
123
|
+
warnings.push('package.json 中存在潜在危险的生命周期脚本(preinstall/postinstall 调用 curl、wget、bash 或 eval)')
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
const unsafeInstall = /npm install\s+[^-].*--ignore-scripts\s*=\s*false|pip install\s+--trusted-host|pip install\s+http:/i
|
|
127
127
|
if (unsafeInstall.test(content)) {
|
|
128
|
-
warnings.push('
|
|
128
|
+
warnings.push('检测到不安全的依赖安装写法')
|
|
129
129
|
}
|
|
130
130
|
return warnings
|
|
131
131
|
}
|
|
@@ -136,12 +136,12 @@ export function scanEnvCoverage(filePath) {
|
|
|
136
136
|
for (let i = 0; i < 10; i += 1) {
|
|
137
137
|
try {
|
|
138
138
|
const gitignore = readFileSync(join(dir, '.gitignore'), 'utf-8')
|
|
139
|
-
return gitignore.includes('.env') ? [] : ['.env
|
|
139
|
+
return gitignore.includes('.env') ? [] : ['写入了 .env 文件,但 .gitignore 未包含 .env 规则']
|
|
140
140
|
} catch {
|
|
141
141
|
const parent = dirname(dir)
|
|
142
142
|
if (parent === dir) break
|
|
143
143
|
dir = parent
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
|
-
return ['.env
|
|
146
|
+
return ['写入了 .env 文件,但未找到 .gitignore']
|
|
147
147
|
}
|
package/scripts/guard.mjs
CHANGED
|
@@ -52,13 +52,14 @@ function emitHookPayload(payload) {
|
|
|
52
52
|
process.stdout.write(JSON.stringify(payload))
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
function emitGuardEvent(cwd, event, source, reason, details = {}) {
|
|
55
|
+
function emitGuardEvent(cwd, event, source, reason, details = {}, payload = {}) {
|
|
56
56
|
appendReplayEvent(cwd, {
|
|
57
57
|
host: HOST,
|
|
58
58
|
event,
|
|
59
59
|
source,
|
|
60
60
|
reason,
|
|
61
61
|
details,
|
|
62
|
+
payload,
|
|
62
63
|
})
|
|
63
64
|
}
|
|
64
65
|
|
|
@@ -67,7 +68,7 @@ function buildHighRiskGate(matches, cwd, payload = {}) {
|
|
|
67
68
|
const stateSyncHint = buildStateSyncHint(cwd, workflowOptions)
|
|
68
69
|
if (stateSyncHint) {
|
|
69
70
|
return {
|
|
70
|
-
reason: `[HelloAGENTS Guard]
|
|
71
|
+
reason: `[HelloAGENTS Guard] 已阻止 T3 命令:项目恢复状态尚未同步。\n${stateSyncHint}`,
|
|
71
72
|
}
|
|
72
73
|
}
|
|
73
74
|
|
|
@@ -75,25 +76,26 @@ function buildHighRiskGate(matches, cwd, payload = {}) {
|
|
|
75
76
|
if (!recommendation) return null
|
|
76
77
|
if (matches.some((match) => match.gate === 'post-verify')) {
|
|
77
78
|
return {
|
|
78
|
-
reason: `[HelloAGENTS Guard]
|
|
79
|
+
reason: `[HelloAGENTS Guard] 已阻止 T3 命令:当前工作流尚未进入 VERIFY / CONSOLIDATE。\n当前工作流:${recommendation.summary}\n建议路径:${recommendation.nextPath}\n${recommendation.guidance}`,
|
|
79
80
|
}
|
|
80
81
|
}
|
|
81
82
|
if (matches.some((match) => match.gate === 'plan-first') && recommendation.nextCommand === 'plan') {
|
|
82
83
|
return {
|
|
83
|
-
reason: `[HelloAGENTS Guard]
|
|
84
|
+
reason: `[HelloAGENTS Guard] 已阻止 T3 命令:高风险 schema 变更前仍需先完成 ~plan。\n当前工作流:${recommendation.summary}\n建议路径:${recommendation.nextPath}\n${recommendation.guidance}`,
|
|
84
85
|
}
|
|
85
86
|
}
|
|
86
87
|
return null
|
|
87
88
|
}
|
|
88
89
|
|
|
89
90
|
function buildIdeaBoundaryReason(kind) {
|
|
90
|
-
return `[HelloAGENTS Guard]
|
|
91
|
+
return `[HelloAGENTS Guard] 已阻止 ~idea 中的${kind}。\n当前路由:~idea 是只读探索;先停留在比较方案。若要写文件、改代码、创建知识库或执行有副作用的命令,请先升级到 ~plan / ~build / ~prd / ~auto。`
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
function detectIdeaBoundaryContext(data) {
|
|
94
95
|
return getApplicableRouteContext({
|
|
95
96
|
cwd: data.cwd || process.cwd(),
|
|
96
97
|
filePath: data.tool_input?.file_path || '',
|
|
98
|
+
payload: data,
|
|
97
99
|
})
|
|
98
100
|
}
|
|
99
101
|
|
|
@@ -106,17 +108,24 @@ function emitIdeaBoundaryBlock(data, kind, target) {
|
|
|
106
108
|
permissionDecisionReason: reason,
|
|
107
109
|
},
|
|
108
110
|
})
|
|
109
|
-
emitGuardEvent(
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
111
|
+
emitGuardEvent(
|
|
112
|
+
data.cwd || process.cwd(),
|
|
113
|
+
'guard_blocked',
|
|
114
|
+
kind === 'write' ? 'pre-write' : 'command',
|
|
115
|
+
buildIdeaBoundaryReason(kind),
|
|
116
|
+
{
|
|
117
|
+
command: kind === '有副作用命令' ? target.replace(/^命令:\s*/, '') : '',
|
|
118
|
+
target: kind === '写入操作' ? target.replace(/^目标:\s*/, '') : '',
|
|
119
|
+
guardType: kind === '写入操作' ? 'idea-write-boundary' : 'idea-command-boundary',
|
|
120
|
+
},
|
|
121
|
+
data,
|
|
122
|
+
)
|
|
114
123
|
}
|
|
115
124
|
|
|
116
125
|
function preWriteGuard(data) {
|
|
117
126
|
if (readSettings().guard_enabled === false) return
|
|
118
127
|
if (!detectIdeaBoundaryContext(data)?.zeroSideEffect) return
|
|
119
|
-
emitIdeaBoundaryBlock(data, '
|
|
128
|
+
emitIdeaBoundaryBlock(data, '写入操作', `目标:${data.tool_input?.file_path || '未知文件'}`)
|
|
120
129
|
}
|
|
121
130
|
|
|
122
131
|
function buildPostWriteWarnings(data) {
|
|
@@ -146,23 +155,23 @@ function postWriteScan(data) {
|
|
|
146
155
|
emitGuardEvent(data.cwd || process.cwd(), 'guard_warning', 'post-write', '', {
|
|
147
156
|
warnings,
|
|
148
157
|
guardType: 'post-write-l2',
|
|
149
|
-
})
|
|
158
|
+
}, data)
|
|
150
159
|
}
|
|
151
160
|
|
|
152
161
|
function handleDangerousCommand(data, command) {
|
|
153
162
|
for (const { pattern, reason } of DANGEROUS_PATTERNS) {
|
|
154
163
|
if (!pattern.test(command)) continue
|
|
155
164
|
emitHookPayload({
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
165
|
+
hookSpecificOutput: {
|
|
166
|
+
hookEventName: HOOK_EVENT,
|
|
167
|
+
permissionDecision: 'deny',
|
|
168
|
+
permissionDecisionReason: `[HelloAGENTS Guard] 已阻止:${reason}\n命令:${command.slice(0, 200)}`,
|
|
169
|
+
},
|
|
161
170
|
})
|
|
162
171
|
emitGuardEvent(data.cwd || process.cwd(), 'guard_blocked', 'command', reason, {
|
|
163
172
|
command: command.slice(0, 200),
|
|
164
173
|
guardType: 'dangerous-command',
|
|
165
|
-
})
|
|
174
|
+
}, data)
|
|
166
175
|
return true
|
|
167
176
|
}
|
|
168
177
|
return false
|
|
@@ -179,14 +188,14 @@ function handleHighRiskCommand(data, command) {
|
|
|
179
188
|
hookSpecificOutput: {
|
|
180
189
|
hookEventName: HOOK_EVENT,
|
|
181
190
|
permissionDecision: 'deny',
|
|
182
|
-
permissionDecisionReason: `${gate.reason}\
|
|
191
|
+
permissionDecisionReason: `${gate.reason}\n命令:${command.slice(0, 200)}`,
|
|
183
192
|
},
|
|
184
193
|
})
|
|
185
194
|
emitGuardEvent(cwd, 'guard_blocked', 'command', gate.reason, {
|
|
186
195
|
command: command.slice(0, 200),
|
|
187
196
|
guardType: 'high-risk-gate',
|
|
188
197
|
matches: warnings.map((warning) => warning.reason),
|
|
189
|
-
})
|
|
198
|
+
}, data)
|
|
190
199
|
return null
|
|
191
200
|
}
|
|
192
201
|
return warnings.map((warning) => warning.reason)
|
|
@@ -215,14 +224,14 @@ function emitShellWarnings(data, command, highRiskWarnings, shellSafetyWarnings)
|
|
|
215
224
|
guardType: 'high-risk-warning',
|
|
216
225
|
command: command.slice(0, 200),
|
|
217
226
|
warnings: highRiskWarnings,
|
|
218
|
-
})
|
|
227
|
+
}, data)
|
|
219
228
|
}
|
|
220
229
|
if (shellSafetyWarnings.length > 0) {
|
|
221
230
|
emitGuardEvent(cwd, 'guard_warning', 'command', '', {
|
|
222
231
|
guardType: 'shell-safety-warning',
|
|
223
232
|
command: command.slice(0, 200),
|
|
224
233
|
warnings: shellSafetyWarnings,
|
|
225
|
-
})
|
|
234
|
+
}, data)
|
|
226
235
|
}
|
|
227
236
|
}
|
|
228
237
|
|
|
@@ -236,7 +245,7 @@ function handleShellCommand(data) {
|
|
|
236
245
|
if (detectIdeaBoundaryContext(data)?.zeroSideEffect) {
|
|
237
246
|
for (const pattern of IDEA_SIDE_EFFECT_COMMAND_PATTERNS) {
|
|
238
247
|
if (!pattern.test(command)) continue
|
|
239
|
-
emitIdeaBoundaryBlock(data, '
|
|
248
|
+
emitIdeaBoundaryBlock(data, '有副作用命令', `命令:${command.slice(0, 200)}`)
|
|
240
249
|
return
|
|
241
250
|
}
|
|
242
251
|
}
|
|
@@ -265,4 +274,15 @@ async function main() {
|
|
|
265
274
|
handleShellCommand(data)
|
|
266
275
|
}
|
|
267
276
|
|
|
268
|
-
main().catch(() => {
|
|
277
|
+
main().catch((error) => {
|
|
278
|
+
const reason = `[HelloAGENTS Guard] 守卫脚本执行异常,已阻止本次操作以避免静默放行。\n原因:${error?.message || error}`
|
|
279
|
+
emitHookPayload({
|
|
280
|
+
hookSpecificOutput: {
|
|
281
|
+
hookEventName: HOOK_EVENT,
|
|
282
|
+
permissionDecision: 'deny',
|
|
283
|
+
permissionDecisionReason: reason,
|
|
284
|
+
},
|
|
285
|
+
})
|
|
286
|
+
process.stderr.write(`${reason}\n`)
|
|
287
|
+
process.exitCode = 1
|
|
288
|
+
})
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
|
-
import {
|
|
3
|
-
import { homedir } from 'node:os';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
4
3
|
import { buildCommandRouteHint, buildStateSyncHint, buildWorkflowRouteHint, readStateSnapshot } from './workflow-state.mjs';
|
|
5
4
|
import { buildCapabilityHint } from './capability-registry.mjs';
|
|
6
5
|
import {
|
|
@@ -15,35 +14,27 @@ const COMMAND_ALIASES = {
|
|
|
15
14
|
review: 'verify',
|
|
16
15
|
};
|
|
17
16
|
|
|
18
|
-
function
|
|
17
|
+
function buildRuntimeRootBlock(pkgRoot) {
|
|
19
18
|
if (!pkgRoot) return '';
|
|
20
|
-
return `## 当前 HelloAGENTS
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function resolveStandbyHostRoot(host) {
|
|
24
|
-
const home = homedir();
|
|
25
|
-
const map = {
|
|
26
|
-
claude: join(home, '.claude', 'helloagents'),
|
|
27
|
-
codex: join(home, '.codex', 'helloagents'),
|
|
28
|
-
gemini: join(home, '.gemini', 'helloagents'),
|
|
29
|
-
};
|
|
30
|
-
return map[host] || '';
|
|
19
|
+
return `## 当前 HelloAGENTS 运行根目录\n\`\`\`text\n${pkgRoot}\n\`\`\``;
|
|
31
20
|
}
|
|
32
21
|
|
|
33
22
|
function resolveReadRoot({ cwd, pkgRoot, host, settings }) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return { source: 'package', root: pkgRoot };
|
|
23
|
+
void cwd
|
|
24
|
+
void host
|
|
25
|
+
void settings
|
|
26
|
+
return { source: 'runtime-root', root: pkgRoot }
|
|
42
27
|
}
|
|
43
28
|
|
|
44
29
|
function buildReadRootBlock(readRoot) {
|
|
45
30
|
if (!readRoot?.root) return '';
|
|
46
|
-
|
|
31
|
+
const block = {
|
|
32
|
+
...readRoot,
|
|
33
|
+
scriptRoot: join(readRoot.root, 'scripts'),
|
|
34
|
+
turnStateCommand: 'helloagents-turn-state write --kind complete --role main',
|
|
35
|
+
turnStateUsage: '仅在运行时需要识别完成、等待或阻塞时调用;普通问答不调用',
|
|
36
|
+
};
|
|
37
|
+
return `## 本轮 HelloAGENTS 读取根目录\n\`\`\`json\n${JSON.stringify(block, null, 2)}\n\`\`\``;
|
|
47
38
|
}
|
|
48
39
|
|
|
49
40
|
export function resolveCanonicalCommandSkill(skillName) {
|
|
@@ -89,10 +80,10 @@ export function buildCompactionContext({ payload, pkgRoot, settings, bootstrapFi
|
|
|
89
80
|
summaryParts.push(bootstrap);
|
|
90
81
|
}
|
|
91
82
|
|
|
92
|
-
const
|
|
93
|
-
if (
|
|
83
|
+
const runtimeRootBlock = buildRuntimeRootBlock(pkgRoot);
|
|
84
|
+
if (runtimeRootBlock) {
|
|
94
85
|
summaryParts.push('');
|
|
95
|
-
summaryParts.push(
|
|
86
|
+
summaryParts.push(runtimeRootBlock);
|
|
96
87
|
}
|
|
97
88
|
|
|
98
89
|
const readRootBlock = buildReadRootBlock(resolveReadRoot({ cwd, pkgRoot, host, settings }));
|
|
@@ -123,7 +114,7 @@ export function buildCompactionContext({ payload, pkgRoot, settings, bootstrapFi
|
|
|
123
114
|
|
|
124
115
|
export function buildInjectContext({ source, bootstrap, settings, pkgRoot, host, cwd, payload = {} }) {
|
|
125
116
|
const workflowOptions = { payload };
|
|
126
|
-
const
|
|
117
|
+
const runtimeRootBlock = buildRuntimeRootBlock(pkgRoot);
|
|
127
118
|
const readRootBlock = buildReadRootBlock(resolveReadRoot({ cwd, pkgRoot, host, settings }));
|
|
128
119
|
const workflowHint = buildWorkflowRouteHint(cwd, workflowOptions);
|
|
129
120
|
const capabilityHint = buildCapabilityHint({ cwd, options: workflowOptions });
|
|
@@ -135,7 +126,7 @@ export function buildInjectContext({ source, bootstrap, settings, pkgRoot, host,
|
|
|
135
126
|
: '';
|
|
136
127
|
|
|
137
128
|
let context = bootstrap;
|
|
138
|
-
if (
|
|
129
|
+
if (runtimeRootBlock) context += `\n\n${runtimeRootBlock}`;
|
|
139
130
|
if (readRootBlock) context += `\n\n${readRootBlock}`;
|
|
140
131
|
if (projectStorageBlock) context += `\n\n${projectStorageBlock}`;
|
|
141
132
|
if (workflowHint) context += `\n\n## 当前工作流提示\n${workflowHint}`;
|
package/scripts/notify-gates.mjs
CHANGED
|
@@ -27,6 +27,7 @@ function emitGateError({
|
|
|
27
27
|
event: 'runtime_gate_error',
|
|
28
28
|
source,
|
|
29
29
|
reason,
|
|
30
|
+
payload,
|
|
30
31
|
})
|
|
31
32
|
output({
|
|
32
33
|
decision: 'block',
|
|
@@ -119,6 +120,7 @@ export function runGateScript({
|
|
|
119
120
|
event: blockEvent,
|
|
120
121
|
source,
|
|
121
122
|
reason: gateOutput.reason || '',
|
|
123
|
+
payload,
|
|
122
124
|
})
|
|
123
125
|
output(gateOutput)
|
|
124
126
|
return true
|
package/scripts/notify-route.mjs
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { join } from 'node:path'
|
|
1
|
+
import { isProjectRuntimeActive } from './runtime-scope.mjs'
|
|
3
2
|
|
|
4
3
|
export function resolveBootstrapFile(cwd, installMode) {
|
|
5
|
-
const isActivated =
|
|
4
|
+
const isActivated = isProjectRuntimeActive(cwd)
|
|
6
5
|
return (installMode === 'global' || isActivated) ? 'bootstrap.md' : 'bootstrap-lite.md'
|
|
7
6
|
}
|
|
8
7
|
|
|
@@ -12,7 +11,7 @@ function shouldBypassRoute(prompt) {
|
|
|
12
11
|
|
|
13
12
|
function buildHelpExtraRules(skillName) {
|
|
14
13
|
if (skillName !== 'help') return ''
|
|
15
|
-
return ' 这是 HelloAGENTS 的帮助命令,不是宿主 CLI 的内置帮助。仅显示 HelloAGENTS
|
|
14
|
+
return ' 这是 HelloAGENTS 的帮助命令,不是宿主 CLI 的内置帮助。仅显示 HelloAGENTS 的帮助和当前设置;优先使用当前上下文中已注入的“当前用户设置”,上下文不存在或缺少要展示的配置项时才读取 ~/.helloagents/helloagents.json;自动激活技能说明仅在全局模式或已激活项目中生效。不要调用宿主 CLI 的帮助工具(如 cli_help 或 /help),不要使用子代理,不要读取项目文件;若受工作区限制无法读取配置,必须明确说明并按已知默认值或已注入设置展示。'
|
|
16
15
|
}
|
|
17
16
|
|
|
18
17
|
function routeExplicitCommand({
|
|
@@ -37,6 +36,7 @@ function routeExplicitCommand({
|
|
|
37
36
|
cwd,
|
|
38
37
|
skillName: canonicalSkillName,
|
|
39
38
|
sourceSkillName: skillName,
|
|
39
|
+
payload,
|
|
40
40
|
})
|
|
41
41
|
appendReplayEvent(cwd, {
|
|
42
42
|
host,
|
|
@@ -44,6 +44,7 @@ function routeExplicitCommand({
|
|
|
44
44
|
source: 'route',
|
|
45
45
|
skillName: canonicalSkillName,
|
|
46
46
|
sourceSkillName: skillName,
|
|
47
|
+
payload,
|
|
47
48
|
})
|
|
48
49
|
suppress(buildRouteInstruction({
|
|
49
50
|
skillName,
|
|
@@ -75,7 +76,7 @@ export function handleRouteCommand({
|
|
|
75
76
|
const prompt = (payload.prompt || '').trim()
|
|
76
77
|
const cwd = payload.cwd || process.cwd()
|
|
77
78
|
if (shouldBypassRoute(prompt)) {
|
|
78
|
-
clearRouteContext()
|
|
79
|
+
clearRouteContext({ cwd, payload })
|
|
79
80
|
emptySuppress()
|
|
80
81
|
return
|
|
81
82
|
}
|
|
@@ -98,17 +99,18 @@ export function handleRouteCommand({
|
|
|
98
99
|
|
|
99
100
|
const bootstrapFile = resolveBootstrapFile(cwd, settings.install_mode)
|
|
100
101
|
if (bootstrapFile === 'bootstrap.md') {
|
|
101
|
-
clearRouteContext()
|
|
102
|
+
clearRouteContext({ cwd, payload })
|
|
102
103
|
appendReplayEvent(cwd, {
|
|
103
104
|
host,
|
|
104
105
|
event: 'semantic_route_prompted',
|
|
105
106
|
source: 'route',
|
|
106
107
|
recommendation: getWorkflowRecommendation(cwd, { payload }),
|
|
108
|
+
payload,
|
|
107
109
|
})
|
|
108
110
|
suppress(buildSemanticRouteInstruction(cwd, payload))
|
|
109
111
|
return
|
|
110
112
|
}
|
|
111
113
|
|
|
112
|
-
clearRouteContext()
|
|
114
|
+
clearRouteContext({ cwd, payload })
|
|
113
115
|
emptySuppress()
|
|
114
116
|
}
|
package/scripts/notify-ui.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { platform } from 'node:os';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
import { existsSync } from 'node:fs';
|
|
8
|
-
import {
|
|
8
|
+
import { spawn } from 'node:child_process';
|
|
9
9
|
|
|
10
10
|
const PLAT = platform();
|
|
11
11
|
|
|
@@ -55,47 +55,53 @@ function resolveWav(pkgRoot, event) {
|
|
|
55
55
|
return existsSync(p) ? p : null;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
function spawnDetached(command, args) {
|
|
59
|
+
try {
|
|
60
|
+
const child = spawn(command, args, {
|
|
61
|
+
detached: true,
|
|
62
|
+
stdio: 'ignore',
|
|
63
|
+
windowsHide: true,
|
|
64
|
+
});
|
|
65
|
+
child.on('error', () => {});
|
|
66
|
+
child.unref();
|
|
67
|
+
return true;
|
|
68
|
+
} catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
58
73
|
export function playSound(pkgRoot, event) {
|
|
59
74
|
if (DISABLE_OS_NOTIFICATIONS) return;
|
|
60
75
|
const wav = resolveWav(pkgRoot, event);
|
|
61
76
|
if (!wav) { process.stderr.write('\x07'); return; }
|
|
62
77
|
try {
|
|
63
78
|
if (PLAT === 'win32') {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
79
|
+
spawnDetached('powershell', [
|
|
80
|
+
'-NoProfile',
|
|
81
|
+
'-c',
|
|
82
|
+
`(New-Object Media.SoundPlayer '${wav.replace(/'/g, "''")}').PlaySync()`,
|
|
83
|
+
]);
|
|
67
84
|
} else if (PLAT === 'darwin') {
|
|
68
|
-
|
|
85
|
+
spawnDetached('afplay', [wav]);
|
|
69
86
|
} else {
|
|
70
|
-
|
|
71
|
-
if (result.status !== 0) {
|
|
72
|
-
const pa = spawnSync('paplay', [wav], { stdio: 'ignore' });
|
|
73
|
-
if (pa.status !== 0) process.stderr.write('\x07');
|
|
74
|
-
}
|
|
87
|
+
spawnDetached('aplay', ['-q', wav]) || spawnDetached('paplay', [wav]);
|
|
75
88
|
}
|
|
76
89
|
} catch { process.stderr.write('\x07'); }
|
|
77
90
|
}
|
|
78
91
|
|
|
79
|
-
function
|
|
80
|
-
if (PLAT !== 'win32') return;
|
|
92
|
+
function buildWindowsToastScript(notification, iconPath) {
|
|
81
93
|
const regKey = `HKCU:\\Software\\Classes\\AppUserModelId\\${WIN_APPID}`;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
94
|
+
const iconXml = existsSync(iconPath)
|
|
95
|
+
? `<image placement="appLogoOverride" src="${escapeToastText(iconPath)}" />`
|
|
96
|
+
: '';
|
|
97
|
+
const textXml = notification.toastLines
|
|
98
|
+
.map((line) => `<text>${escapeToastText(line)}</text>`)
|
|
99
|
+
.join('\n ');
|
|
100
|
+
return `
|
|
101
|
+
if (-not (Test-Path '${regKey}')) {
|
|
102
|
+
New-Item -Path '${regKey}' -Force | Out-Null
|
|
103
|
+
Set-ItemProperty -Path '${regKey}' -Name 'DisplayName' -Value 'HelloAgents 通知' -Force
|
|
85
104
|
}
|
|
86
|
-
|
|
87
|
-
export function desktopNotify(pkgRoot, event, extra) {
|
|
88
|
-
if (DISABLE_OS_NOTIFICATIONS) return;
|
|
89
|
-
const notification = buildDesktopNotificationContent(event, extra);
|
|
90
|
-
try {
|
|
91
|
-
if (PLAT === 'win32') {
|
|
92
|
-
ensureWinAppId(pkgRoot);
|
|
93
|
-
const iconPath = join(pkgRoot, 'assets', 'icons', 'icon.png').replace(/\//g, '\\');
|
|
94
|
-
const iconXml = existsSync(iconPath) ? `<image placement="appLogoOverride" src="${iconPath}" />` : '';
|
|
95
|
-
const textXml = notification.toastLines
|
|
96
|
-
.map((line) => `<text>${escapeToastText(line)}</text>`)
|
|
97
|
-
.join('\n ');
|
|
98
|
-
const ps = `
|
|
99
105
|
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
|
|
100
106
|
[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom, ContentType = WindowsRuntime] | Out-Null
|
|
101
107
|
$xml = @"
|
|
@@ -113,17 +119,24 @@ $doc.LoadXml($xml)
|
|
|
113
119
|
$toast = [Windows.UI.Notifications.ToastNotification]::new($doc)
|
|
114
120
|
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('${WIN_APPID}').Show($toast)
|
|
115
121
|
`.trim();
|
|
116
|
-
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function desktopNotify(pkgRoot, event, extra) {
|
|
125
|
+
if (DISABLE_OS_NOTIFICATIONS) return;
|
|
126
|
+
const notification = buildDesktopNotificationContent(event, extra);
|
|
127
|
+
try {
|
|
128
|
+
if (PLAT === 'win32') {
|
|
129
|
+
const iconPath = join(pkgRoot, 'assets', 'icons', 'icon.png').replace(/\//g, '\\');
|
|
130
|
+
spawnDetached('powershell', ['-NoProfile', '-c', buildWindowsToastScript(notification, iconPath)]);
|
|
117
131
|
} else if (PLAT === 'darwin') {
|
|
118
132
|
const subtitle = notification.sourceLabel
|
|
119
133
|
? ` subtitle "${escapeAppleScriptText(notification.sourceLabel)}"`
|
|
120
134
|
: '';
|
|
121
|
-
|
|
135
|
+
spawnDetached('osascript', ['-e',
|
|
122
136
|
`display notification "${escapeAppleScriptText(notification.message)}" with title "${escapeAppleScriptText(notification.title)}"${subtitle}`],
|
|
123
|
-
|
|
137
|
+
);
|
|
124
138
|
} else {
|
|
125
|
-
|
|
126
|
-
if (result.status !== 0) process.stderr.write('\x07');
|
|
139
|
+
spawnDetached('notify-send', [notification.title, notification.body]);
|
|
127
140
|
}
|
|
128
141
|
} catch { process.stderr.write('\x07'); }
|
|
129
142
|
}
|