mirror of
https://github.com/tanweai/pua.git
synced 2026-08-28 17:42:11 +08:00
fix(hooks): make integrity guard advisory-only
This commit is contained in:
@@ -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": "探微安全实验室",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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": "./"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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
@@ -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 语义混乱。现在统一为:
|
||||
|
||||
@@ -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
@@ -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,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
@@ -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"
|
||||
|
||||
@@ -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` 对高风险路径注入 advisory;hidden/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 |
|
||||
|
||||
## 交付前治理循环
|
||||
|
||||
Reference in New Issue
Block a user