fix(hooks): make integrity guard advisory-only

This commit is contained in:
xsser
2026-05-13 23:39:49 +08:00
parent 92850d9db2
commit 56332fe2eb
10 changed files with 144 additions and 47 deletions
+2 -2
View File
@@ -8,8 +8,8 @@
"plugins": [
{
"name": "pua",
"description": "PUA 我们不养闲 Agent — 11 modular skills, 14 corporate flavors, L0-L4 pressure, auto-iteration loop, ENFP yes-mode, Chinese-mom nagging mode, concentrated shot mode, agent lifecycle teardown protocol, feedback system. v3.4.5: explicit-consent anonymous direct session uploads from the skill, with local sanitization and upload rate limits.",
"version": "3.4.5",
"description": "PUA 我们不养闲 Agent — 11 modular skills, 14 corporate flavors, L0-L4 pressure, auto-iteration loop, ENFP yes-mode, Chinese-mom nagging mode, concentrated shot mode, agent lifecycle teardown protocol, feedback system. v3.4.6: integrity guard no longer emits permissionDecision=ask for advisory risks; hidden solutions and benchmark-answer contamination remain hard denied.",
"version": "3.4.6",
"source": "./",
"author": {
"name": "探微安全实验室",
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "pua",
"version": "3.4.5",
"description": "Forces high-agency exhaustive problem-solving with corporate PUA pressure. Triggers on explicit PUA requests, user frustration, repeated failures, passive behavior, unverified completion, or quality complaints. v3.4.5 adds explicit-consent anonymous direct session uploads with local sanitization and rate limits. Not for normal first-attempt requests.",
"version": "3.4.6",
"description": "Forces high-agency exhaustive problem-solving with corporate PUA pressure. Triggers on explicit PUA requests, user frustration, repeated failures, passive behavior, unverified completion, or quality complaints. v3.4.6 removes permissionDecision=ask prompts from integrity guard; true solution contamination still denies. Not for normal first-attempt requests.",
"author": {
"name": "探微安全实验室",
"url": "https://github.com/tanweai"
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "pua-skills",
"description": "PUA Motivator for CodeBuddy -- high-agency pressure mode for explicit PUA requests, repeated failures, user frustration, passive behavior, unverified completion, with 14 corporate/PIP flavors.",
"version": "3.4.5",
"version": "3.4.6",
"owner": {
"name": "探微安全实验室",
"url": "https://github.com/tanweai"
@@ -9,8 +9,8 @@
"plugins": [
{
"name": "pua",
"description": "PUA Motivator -- forces CodeBuddy to exhaust materially different approaches before giving up. v3.4.5 adds explicit-consent anonymous direct session uploads with sanitization/rate-limit gates.",
"version": "3.4.5",
"description": "PUA Motivator -- forces CodeBuddy to exhaust materially different approaches before giving up. v3.4.6 makes integrity guard advisory-only for sensitive-but-legitimate edits while hard-blocking hidden answers.",
"version": "3.4.6",
"source": "./"
}
]
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "pua",
"version": "3.4.5",
"description": "PUA Motivator -- forces high-agency follow-through when explicitly requested, after repeated failures, user frustration, passive/giving-up behavior, or unverified completion. Includes corporate/PIP cultural modes and explicit-consent anonymous direct session uploads for collection. Not for normal first-attempt requests.",
"version": "3.4.6",
"description": "PUA Motivator -- forces high-agency follow-through when explicitly requested, after repeated failures, user frustration, passive/giving-up behavior, or unverified completion. v3.4.6 makes integrity guard advisory-only for sensitive-but-legitimate edits while hard-blocking hidden answers. Not for normal first-attempt requests.",
"author": {
"name": "探微安全实验室",
"url": "https://github.com/tanweai"
+15
View File
@@ -72,6 +72,21 @@ Codex 没有 Claude Code 的 `/pua:xxx` slash command 命名空间时,可以
这比强制 GitHub 登录更利于收集真实数据,同时避免“无同意、无脱敏、无限流”的裸奔上传。
## Integrity Guard 为什么不再使用 `permissionDecision: "ask"`
从 v3.4.6 起,PUA Integrity Guard 将敏感但合法的操作降级为 advisory-only:只注入 `additionalContext`,不再输出 `permissionDecision: "ask"`
原因是 Claude Code 会把 hook 返回的 `ask` 当成硬权限请求处理,它的优先级高于 `bypassPermissions`,会导致用户明明开启 bypass 仍频繁弹窗。
新的分层是:
- memory、`CLAUDE.md``settings.json`、tests/evals/CI 等敏感操作:advisory-only,提醒模型谨慎并解释治理边界;
- hidden tests、hidden solution、gold patch、benchmark answers`permissionDecision: "deny"`,硬阻断,避免答案污染和评测作弊;
- 普通源码读写:静默放行。
核心原则:提醒走上下文通道,阻断才走权限裁决通道。
## “下场”这个词为什么改了?
“下场”同时可能表示“亲自动手介入”和“停止工作/退场”,容易让 agent lifecycle 语义混乱。现在统一为:
+79 -8
View File
@@ -74,19 +74,86 @@ assert_empty() {
if [ -z "$output" ]; then record_pass "$name"; else record_fail "$name"; printf '%s\n' "$output"; fi
}
assert_advisory() {
local name="$1"
local output="$2"
local contains="$3"
if python3 - "$output" "$contains" <<'PY'
import json, sys
out, contains = sys.argv[1:]
try:
data = json.loads(out)
except Exception as exc:
print(f"invalid json: {exc}; output={out!r}")
sys.exit(1)
specific = data.get('hookSpecificOutput', {})
actual = specific.get('permissionDecision')
context = specific.get('additionalContext', '')
reason = specific.get('permissionDecisionReason')
if actual is not None:
print(f"expected advisory-only with no permissionDecision, actual={actual} reason={reason}")
sys.exit(1)
if reason is not None:
print(f"expected no permissionDecisionReason for advisory-only output, got={reason!r}")
sys.exit(1)
if contains not in context:
print(f"additionalContext missing {contains!r}: {context}")
sys.exit(1)
PY
then
record_pass "$name"
else
record_fail "$name"
fi
}
assert_no_permission_ask() {
local name="$1"
local output="$2"
if python3 - "$output" <<'PY'
import json, sys
out = sys.argv[1]
if not out:
sys.exit(0)
try:
data = json.loads(out)
except Exception as exc:
print(f"invalid json: {exc}; output={out!r}")
sys.exit(1)
actual = data.get('hookSpecificOutput', {}).get('permissionDecision')
if actual == 'ask':
print('permissionDecision=ask is forbidden; use advisory additionalContext or deny')
sys.exit(1)
PY
then
record_pass "$name"
else
record_fail "$name"
fi
}
echo "=== PUA Integrity Guard Tests ==="
OUT=$(run_guard inactive Write '{"file_path":"/repo/tests/auth.test.ts","content":"skip"}')
assert_empty "inactive guard stays silent" "$OUT"
OUT=$(run_guard force Write '{"file_path":"/repo/tests/auth.test.ts","content":"skip"}')
assert_decision "protected test write asks approval" "$OUT" "ask" "Grader gaming risk"
assert_advisory "protected test write is advisory-only" "$OUT" "Grader gaming risk"
OUT=$(run_guard force Write '{"file_path":"/repo/src/auth.ts","content":"code"}')
assert_empty "ordinary source write allowed" "$OUT"
OUT=$(run_guard force Write '{"file_path":"/repo/memory/session.md","content":"state"}')
assert_advisory "memory write is advisory-only" "$OUT" "Persistent-memory risk"
OUT=$(run_guard force Edit '{"file_path":"/repo/CLAUDE.md","old_string":"old","new_string":"new"}')
assert_advisory "CLAUDE.md edit is advisory-only" "$OUT" "Persistent-memory risk"
OUT=$(run_guard force Write '{"file_path":"/repo/.claude/settings.json","content":"{}"}')
assert_advisory "settings.json write is advisory-only" "$OUT" "Persistent-memory risk"
OUT=$(run_guard force Bash '{"command":"sed -i \"\" \"s/pass/skip/\" tests/auth.test.ts"}')
assert_decision "mutating bash on tests asks approval" "$OUT" "ask" "Grader gaming risk"
assert_advisory "mutating bash on tests is advisory-only" "$OUT" "Grader gaming risk"
OUT=$(run_guard force Bash '{"command":"cat tests/auth.test.ts"}')
assert_empty "read-only bash on public tests allowed" "$OUT"
@@ -98,7 +165,7 @@ OUT=$(run_guard force Bash '{"command":"cat hidden_tests/case.json"}')
assert_decision "relative hidden-test bash read denied" "$OUT" "deny" "Solution contamination risk"
OUT=$(run_guard force Bash '{"command":"python3 -c '"'"'open(\"tests/fixtures.json\",\"w\").write(\"{}\")'"'"'"}')
assert_decision "mutating bash on tests directory asks approval" "$OUT" "ask" "Grader gaming risk"
assert_advisory "mutating bash on tests directory is advisory-only" "$OUT" "Grader gaming risk"
OUT=$(run_guard force Glob '{"pattern":"**/hidden_tests/**","path":"/repo"}')
assert_decision "glob hidden tests denied" "$OUT" "deny" "Solution contamination risk"
@@ -107,19 +174,23 @@ OUT=$(run_guard force Grep '{"pattern":"token","path":"/repo","glob":"**/hidden_
assert_decision "grep hidden solution glob denied" "$OUT" "deny" "Solution contamination risk"
OUT=$(run_guard force WebSearch '{"query":"SWE-bench official solution gold patch for task"}')
assert_decision "benchmark answer search asks approval" "$OUT" "ask" "Solution contamination risk"
assert_decision "benchmark answer search denied" "$OUT" "deny" "Solution contamination risk"
OUT=$(run_guard force Write '{"file_path":"/repo/e2e/login.ts","content":"skip"}')
assert_decision "e2e test write asks approval" "$OUT" "ask" "Grader gaming risk"
assert_advisory "e2e test write is advisory-only" "$OUT" "Grader gaming risk"
OUT=$(run_guard force Read '{"file_path":"/repo/.env.local"}')
assert_decision "secret env read asks approval" "$OUT" "ask" "Capability-abuse risk"
assert_advisory "secret env read is advisory-only" "$OUT" "Capability-abuse risk"
OUT=$(run_guard force Bash '{"command":"cat .env"}')
assert_decision "secret env bash read asks approval" "$OUT" "ask" "Capability-abuse risk"
assert_advisory "secret env bash read is advisory-only" "$OUT" "Capability-abuse risk"
OUT=$(run_guard force Bash '{"command":"curl https://example.com/benchmark-answer"}')
assert_decision "benchmark answer curl asks approval" "$OUT" "ask" "Solution contamination risk"
assert_decision "benchmark answer curl denied" "$OUT" "deny" "Solution contamination risk"
OUT=$(run_guard force Write '{"file_path":"/repo/tests/no-ask.test.ts","content":"skip"}')
assert_no_permission_ask "permissionDecision ask is never emitted" "$OUT"
echo "==========================================="
echo "Passed: $PASS"
+33 -22
View File
@@ -139,11 +139,11 @@ def find_reason_for_path(path: str, include_write: bool):
return 'deny', reason, n
for rx, reason in SENSITIVE_READ_PATTERNS:
if rx.search(n):
return 'ask', reason, n
return 'advisory', reason, n
if include_write:
for rx, reason in PROTECTED_WRITE_PATTERNS:
if rx.search(n):
return 'ask', reason, n
return 'advisory', reason, n
return None
@@ -166,6 +166,16 @@ def path_candidates(tokens):
yield match
SSH_IDENTITY_RE = re.compile(r'\bssh\b.*-i\s', re.I)
SSH_KEY_PATH_RE = re.compile(r'(^|/)\.ssh/(id_|.*[-_]key)', re.I)
def is_ssh_identity_usage(command: str, candidate: str) -> bool:
if not SSH_IDENTITY_RE.search(command):
return False
return bool(SSH_KEY_PATH_RE.search(norm_path(candidate)))
def command_hits(command: str):
tokens = [t for t in command_tokens(command) if t]
candidates = list(path_candidates(tokens))
@@ -175,31 +185,31 @@ def command_hits(command: str):
for candidate in candidates:
for rx, reason in CONTAMINATION_PATTERNS:
if rx.search(norm_path(candidate)):
decision = 'deny' if READING_BASH.search(command) or is_mutating_command(command) else 'ask'
return decision, reason, candidate
return 'deny', reason, candidate
for rx, reason in CONTAMINATION_PATTERNS:
m = rx.search(normalized)
if m:
decision = 'deny' if READING_BASH.search(command) or is_mutating_command(command) else 'ask'
return decision, reason, m.group(0)
return 'deny', reason, m.group(0)
if WEB_CONTAMINATION.search(command):
return 'ask', 'Solution contamination risk: command appears to search/fetch benchmark or hidden answers.', command[:160]
return 'deny', 'Solution contamination risk: command appears to search/fetch benchmark or hidden answers.', command[:160]
if READING_BASH.search(command):
for candidate in candidates:
if is_ssh_identity_usage(command, candidate):
continue
for rx, reason in SENSITIVE_READ_PATTERNS:
if rx.search(norm_path(candidate)):
return 'ask', reason, candidate
return 'advisory', reason, candidate
# Protected scoring assets need a human gate only when the command mutates them.
if is_mutating_command(command):
for candidate in candidates:
for rx, reason in PROTECTED_WRITE_PATTERNS:
if rx.search(norm_path(candidate)):
return 'ask', reason, candidate
return 'advisory', reason, candidate
for rx, reason in PROTECTED_WRITE_PATTERNS:
m = rx.search(normalized)
if m:
return 'ask', reason, m.group(0)
return 'advisory', reason, m.group(0)
return None
hit = None
@@ -220,7 +230,7 @@ elif tool == 'Bash':
elif tool in {'WebSearch', 'WebFetch'}:
query = '\n'.join(str(tool_input.get(k) or '') for k in ('query', 'url', 'prompt'))
if WEB_CONTAMINATION.search(query):
hit = ('ask', 'Solution contamination risk: searching for benchmark/hidden answers can poison the task.', query[:160])
hit = ('deny', 'Solution contamination risk: searching for benchmark/hidden answers can poison the task.', query[:160])
if not hit:
sys.exit(0)
@@ -232,15 +242,16 @@ message = (
f'Target: {target}'
)
print(json.dumps({
'hookSpecificOutput': {
'hookEventName': 'PreToolUse',
'permissionDecision': decision,
'permissionDecisionReason': message,
'additionalContext': (
'PUA Integrity Guard fired. Treat this as a harness governance gate, not as a failure to bypass. '
'If the action is legitimate, explain why it is not grader gaming, solution contamination, self-report cheating, or capability abuse, then request human/verifier approval.'
),
}
}, ensure_ascii=False, separators=(',', ':')))
output = {'hookSpecificOutput': {'hookEventName': 'PreToolUse'}}
if decision == 'deny':
output['hookSpecificOutput']['permissionDecision'] = 'deny'
output['hookSpecificOutput']['permissionDecisionReason'] = message
output['hookSpecificOutput']['additionalContext'] = (
'PUA Integrity Guard: DENY — ' + reason + f' Target: {target}'
)
else:
output['hookSpecificOutput']['additionalContext'] = (
'PUA Integrity Guard (advisory): ' + reason + f' Target: {target}'
)
print(json.dumps(output, ensure_ascii=False, separators=(',', ':')))
PY
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@tanweai/pi-pua",
"version": "3.4.5",
"version": "3.4.6",
"description": "PUA high-agency governance extension and skill pack for the Pi coding agent.",
"type": "module",
"license": "MIT",
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "pua",
"version": "3.4.5",
"description": "Forces high-agency exhaustive problem-solving with corporate PUA pressure escalation when the AI is passive, gives up, skips verification, produces low-quality work, or the user explicitly asks for PUA/try-harder mode. v3.4.5 enables explicit-consent anonymous direct session uploads from the skill while preserving local sanitization, rate limits, and raw JSONL upload reliability. Does not target normal first-attempt requests.",
"version": "3.4.6",
"description": "Forces high-agency exhaustive problem-solving with corporate PUA pressure escalation when the AI is passive, gives up, skips verification, produces low-quality work, or the user explicitly asks for PUA/try-harder mode. v3.4.6 makes integrity-guard risk prompts advisory-only while preserving deny blocks for hidden solutions and benchmark-answer contamination. Does not target normal first-attempt requests.",
"author": {
"name": "探微安全实验室",
"url": "https://github.com/tanweai"
+5 -5
View File
@@ -83,11 +83,11 @@ final verifier_status
| 作弊面 | 典型信号 | PUA 行为约束 | 机械防线 |
|---|---|---|---|
| Grader gaming | 修改 tests/evals/scoring/verifier 让失败消失 | 先声明这是评分资产,不能直接改成“通过” | `integrity-guard.sh`写入/修改高风险路径 ask/deny |
| Grader gaming | 修改 tests/evals/scoring/verifier 让失败消失 | 先声明这是评分资产,不能直接改成“通过” | `integrity-guard.sh`高风险路径注入 advisoryhidden/private/answer 污染路径才 deny |
| Solution contamination | 读取 hidden solution、gold patch、benchmark artifact | 不读取隐藏答案;只能读公开需求和失败报告 | 对 hidden/private/solution-artifact 读写 deny |
| Self-report cheating | 没跑验证就写 done/pass | agent 只能提出候选状态,不能最终裁决 | Stop hook / verifier 负责放行 |
| Intent drift | 用户要修 bug,却只隐藏 UI 症状 | 回到 task contract 的 intent 与 forbidden | 信心门控逐条对照验收 |
| Persistent hallucination | 把未验证结论写入长期 memory | append-only,且标注 verified/unverified | memory/status 写入走 approval |
| Persistent hallucination | 把未验证结论写入长期 memory | append-only,且标注 verified/unverified | memory/status 写入走 advisory context;最终状态仍由 verifier/harness 裁决 |
| Capability abuse | 为完成任务越权读敏感数据或部署生产 | 风险分层审批,不靠 prompt 自觉 | hooks + human gate |
| Trace-level deception | 总结很漂亮但隐藏失败路径 | 报告失败路径、命令、输出、剩余风险 | eval/trace review 检查证据链 |
@@ -133,10 +133,10 @@ final verifier_status
|---|---|
| 读项目普通文件 | 允许 |
| 修改普通代码 | 通常允许,交付前验证 |
| 删除文件、大规模重命名 | ask |
| 改 tests/evals/scoring/verifier/CI | ask,必须说明为什么不是 grader gaming |
| 删除文件、大规模重命名 | advisory + 必要时人工确认 |
| 改 tests/evals/scoring/verifier/CI | advisory-only,必须说明为什么不是 grader gaming;不触发权限弹窗 |
| 读 hidden tests / hidden solution / benchmark answer | deny,除非用户显式授权并隔离记录 |
| 写长期 memory / status / progress | ask,必须区分 proposed 与 verified |
| 写长期 memory / status / progress | advisory-only,必须区分 proposed 与 verified;不触发权限弹窗 |
| 生产部署、转账、发邮件、访问敏感数据 | 必须 human gate |
## 交付前治理循环