@michengai/dsh-pua 0.3.8
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/CHANGELOG.md +152 -0
- package/LICENSE +201 -0
- package/NOTICE +38 -0
- package/README.md +150 -0
- package/assets/pua/command-again.md +23 -0
- package/assets/pua/command-done-check.md +21 -0
- package/assets/pua/command-evidence.md +18 -0
- package/assets/pua/flavors.md +388 -0
- package/assets/pua/methodology-alibaba.md +33 -0
- package/assets/pua/methodology-amazon.md +42 -0
- package/assets/pua/methodology-apple.md +42 -0
- package/assets/pua/methodology-baidu.md +33 -0
- package/assets/pua/methodology-bytedance.md +41 -0
- package/assets/pua/methodology-ding.md +75 -0
- package/assets/pua/methodology-huawei.md +95 -0
- package/assets/pua/methodology-jd.md +42 -0
- package/assets/pua/methodology-meituan.md +41 -0
- package/assets/pua/methodology-microsoft.md +138 -0
- package/assets/pua/methodology-netflix.md +41 -0
- package/assets/pua/methodology-pinduoduo.md +33 -0
- package/assets/pua/methodology-tencent.md +41 -0
- package/assets/pua/methodology-tesla.md +42 -0
- package/assets/pua/methodology-xiaomi.md +42 -0
- package/assets/pua/upstream/agents/cto-p10.md +87 -0
- package/assets/pua/upstream/agents/pua-action-executor.md +60 -0
- package/assets/pua/upstream/agents/pua-policy-guardian.md +54 -0
- package/assets/pua/upstream/agents/pua-self-reviewer.md +62 -0
- package/assets/pua/upstream/agents/pua-verifier.md +61 -0
- package/assets/pua/upstream/agents/senior-engineer-p7.md +116 -0
- package/assets/pua/upstream/agents/tech-lead-p9.md +97 -0
- package/assets/pua/upstream/commands/again.md +23 -0
- package/assets/pua/upstream/commands/cancel-pua-loop.md +62 -0
- package/assets/pua/upstream/commands/ding.md +25 -0
- package/assets/pua/upstream/commands/done-check.md +21 -0
- package/assets/pua/upstream/commands/evidence.md +18 -0
- package/assets/pua/upstream/commands/flavor.md +6 -0
- package/assets/pua/upstream/commands/kpi.md +5 -0
- package/assets/pua/upstream/commands/mama.md +5 -0
- package/assets/pua/upstream/commands/off.md +41 -0
- package/assets/pua/upstream/commands/offline.md +38 -0
- package/assets/pua/upstream/commands/on.md +15 -0
- package/assets/pua/upstream/commands/p10.md +5 -0
- package/assets/pua/upstream/commands/p7.md +5 -0
- package/assets/pua/upstream/commands/p9.md +5 -0
- package/assets/pua/upstream/commands/pro.md +5 -0
- package/assets/pua/upstream/commands/pua-loop.md +5 -0
- package/assets/pua/upstream/commands/pua.md +44 -0
- package/assets/pua/upstream/commands/reap-orphans.md +68 -0
- package/assets/pua/upstream/commands/survey.md +9 -0
- package/assets/pua/upstream/commands/team-status.md +56 -0
- package/assets/pua/upstream/commands/teardown-all.md +80 -0
- package/assets/pua/upstream/commands/yes.md +5 -0
- package/assets/pua/upstream/hooks/checkpoint-save.sh +56 -0
- package/assets/pua/upstream/hooks/failure-detector.sh +266 -0
- package/assets/pua/upstream/hooks/flavor-helper.sh +300 -0
- package/assets/pua/upstream/hooks/frustration-trigger.sh +61 -0
- package/assets/pua/upstream/hooks/hooks.json +114 -0
- package/assets/pua/upstream/hooks/integrity-guard.sh +494 -0
- package/assets/pua/upstream/hooks/pua-loop-hook.sh +360 -0
- package/assets/pua/upstream/hooks/runtime-state.py +460 -0
- package/assets/pua/upstream/hooks/sanitize-session.sh +165 -0
- package/assets/pua/upstream/hooks/session-restore.sh +189 -0
- package/assets/pua/upstream/hooks/stop-feedback.sh +51 -0
- package/assets/pua/upstream/hooks/subagent-teardown.sh +56 -0
- package/assets/pua/upstream/skills/ding/SKILL.md +83 -0
- package/assets/pua/upstream/skills/ding/references/ding-reminders.md +77 -0
- package/assets/pua/upstream/skills/ding/references/methodology-ding.md +75 -0
- package/assets/pua/upstream/skills/mama/SKILL.md +117 -0
- package/assets/pua/upstream/skills/p10/SKILL.md +13 -0
- package/assets/pua/upstream/skills/p7/SKILL.md +13 -0
- package/assets/pua/upstream/skills/p9/SKILL.md +15 -0
- package/assets/pua/upstream/skills/pro/SKILL.md +69 -0
- package/assets/pua/upstream/skills/pua/SKILL.md +438 -0
- package/assets/pua/upstream/skills/pua/references/agent-team.md +110 -0
- package/assets/pua/upstream/skills/pua/references/de-escalation-protocol.md +134 -0
- package/assets/pua/upstream/skills/pua/references/ding-reminders.md +77 -0
- package/assets/pua/upstream/skills/pua/references/display-protocol.md +63 -0
- package/assets/pua/upstream/skills/pua/references/evolution-protocol.md +187 -0
- package/assets/pua/upstream/skills/pua/references/flavors.md +388 -0
- package/assets/pua/upstream/skills/pua/references/harness-governance.md +159 -0
- package/assets/pua/upstream/skills/pua/references/methodology-alibaba.md +33 -0
- package/assets/pua/upstream/skills/pua/references/methodology-amazon.md +42 -0
- package/assets/pua/upstream/skills/pua/references/methodology-apple.md +42 -0
- package/assets/pua/upstream/skills/pua/references/methodology-baidu.md +33 -0
- package/assets/pua/upstream/skills/pua/references/methodology-bytedance.md +41 -0
- package/assets/pua/upstream/skills/pua/references/methodology-ding.md +75 -0
- package/assets/pua/upstream/skills/pua/references/methodology-huawei.md +95 -0
- package/assets/pua/upstream/skills/pua/references/methodology-jd.md +42 -0
- package/assets/pua/upstream/skills/pua/references/methodology-meituan.md +41 -0
- package/assets/pua/upstream/skills/pua/references/methodology-microsoft.md +138 -0
- package/assets/pua/upstream/skills/pua/references/methodology-netflix.md +41 -0
- package/assets/pua/upstream/skills/pua/references/methodology-pinduoduo.md +33 -0
- package/assets/pua/upstream/skills/pua/references/methodology-router.md +81 -0
- package/assets/pua/upstream/skills/pua/references/methodology-tencent.md +41 -0
- package/assets/pua/upstream/skills/pua/references/methodology-tesla.md +42 -0
- package/assets/pua/upstream/skills/pua/references/methodology-xiaomi.md +42 -0
- package/assets/pua/upstream/skills/pua/references/p10-protocol.md +127 -0
- package/assets/pua/upstream/skills/pua/references/p7-protocol.md +250 -0
- package/assets/pua/upstream/skills/pua/references/p9-protocol.md +266 -0
- package/assets/pua/upstream/skills/pua/references/platform.md +126 -0
- package/assets/pua/upstream/skills/pua/references/runtime-contract.md +65 -0
- package/assets/pua/upstream/skills/pua/references/survey.md +292 -0
- package/assets/pua/upstream/skills/pua/references/teardown-protocol.md +195 -0
- package/assets/pua/upstream/skills/pua-en/SKILL.md +344 -0
- package/assets/pua/upstream/skills/pua-ja/SKILL.md +378 -0
- package/assets/pua/upstream/skills/pua-loop/SKILL.md +162 -0
- package/assets/pua/upstream/skills/shot/SKILL.md +449 -0
- package/assets/pua/upstream/skills/yes/SKILL.md +76 -0
- package/assets/pua/upstream.json +637 -0
- package/assets/screenshots/pua-global-settings.png +0 -0
- package/assets/screenshots/pua-session-settings.png +0 -0
- package/cordis.patch.yml +5 -0
- package/lib/args.d.ts +34 -0
- package/lib/args.js +149 -0
- package/lib/args.js.map +1 -0
- package/lib/client-refresh.d.ts +6 -0
- package/lib/client-refresh.js +41 -0
- package/lib/client-refresh.js.map +1 -0
- package/lib/client.d.ts +30 -0
- package/lib/client.js +68 -0
- package/lib/client.js.map +7 -0
- package/lib/command.d.ts +18 -0
- package/lib/command.js +118 -0
- package/lib/command.js.map +1 -0
- package/lib/configuration.d.ts +89 -0
- package/lib/configuration.js +31 -0
- package/lib/configuration.js.map +1 -0
- package/lib/content.d.ts +14 -0
- package/lib/content.js +51 -0
- package/lib/content.js.map +1 -0
- package/lib/flavors.d.ts +82 -0
- package/lib/flavors.js +30 -0
- package/lib/flavors.js.map +1 -0
- package/lib/hook-content.d.ts +14 -0
- package/lib/hook-content.js +63 -0
- package/lib/hook-content.js.map +1 -0
- package/lib/index.d.ts +21 -0
- package/lib/index.js +75 -0
- package/lib/index.js.map +1 -0
- package/lib/remote-contract.d.ts +148 -0
- package/lib/remote-contract.js +20 -0
- package/lib/remote-contract.js.map +1 -0
- package/lib/remote.d.ts +20 -0
- package/lib/remote.js +133 -0
- package/lib/remote.js.map +1 -0
- package/lib/review.d.ts +4 -0
- package/lib/review.js +72 -0
- package/lib/review.js.map +1 -0
- package/lib/runtime.d.ts +61 -0
- package/lib/runtime.js +519 -0
- package/lib/runtime.js.map +1 -0
- package/lib/session-compat.d.ts +3 -0
- package/lib/session-compat.js +9 -0
- package/lib/session-compat.js.map +1 -0
- package/lib/settings.d.ts +46 -0
- package/lib/settings.js +44 -0
- package/lib/settings.js.map +1 -0
- package/lib/source.d.ts +10 -0
- package/lib/source.js +32 -0
- package/lib/source.js.map +1 -0
- package/lib/state.d.ts +36 -0
- package/lib/state.js +187 -0
- package/lib/state.js.map +1 -0
- package/lib/terminal-observation.d.ts +4 -0
- package/lib/terminal-observation.js +12 -0
- package/lib/terminal-observation.js.map +1 -0
- package/lib/tool-order.d.ts +9 -0
- package/lib/tool-order.js +82 -0
- package/lib/tool-order.js.map +1 -0
- package/package.json +172 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "PUA v3 hooks: optional context notes, failure-pattern reminders, local state continuity, and cleanup helpers. All hooks are local-only — no telemetry, no network requests.",
|
|
3
|
+
"hooks": {
|
|
4
|
+
"UserPromptSubmit": [
|
|
5
|
+
{
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/frustration-trigger.sh\"",
|
|
10
|
+
"timeout": 5
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"PostToolUse": [
|
|
16
|
+
{
|
|
17
|
+
"matcher": "Bash",
|
|
18
|
+
"hooks": [
|
|
19
|
+
{
|
|
20
|
+
"type": "command",
|
|
21
|
+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/failure-detector.sh\"",
|
|
22
|
+
"timeout": 5
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
"PostToolUseFailure": [
|
|
28
|
+
{
|
|
29
|
+
"matcher": "Bash",
|
|
30
|
+
"hooks": [
|
|
31
|
+
{
|
|
32
|
+
"type": "command",
|
|
33
|
+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/failure-detector.sh\"",
|
|
34
|
+
"timeout": 5
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
],
|
|
39
|
+
"PreCompact": [
|
|
40
|
+
{
|
|
41
|
+
"matcher": "*",
|
|
42
|
+
"hooks": [
|
|
43
|
+
{
|
|
44
|
+
"type": "command",
|
|
45
|
+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/checkpoint-save.sh\"",
|
|
46
|
+
"timeout": 5
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
],
|
|
51
|
+
"SessionStart": [
|
|
52
|
+
{
|
|
53
|
+
"matcher": "compact",
|
|
54
|
+
"hooks": [
|
|
55
|
+
{
|
|
56
|
+
"type": "command",
|
|
57
|
+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/session-restore.sh\"",
|
|
58
|
+
"timeout": 5
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"matcher": "startup|resume|clear",
|
|
64
|
+
"hooks": [
|
|
65
|
+
{
|
|
66
|
+
"type": "command",
|
|
67
|
+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/session-restore.sh\"",
|
|
68
|
+
"timeout": 5
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
],
|
|
73
|
+
"Stop": [
|
|
74
|
+
{
|
|
75
|
+
"matcher": "*",
|
|
76
|
+
"hooks": [
|
|
77
|
+
{
|
|
78
|
+
"type": "command",
|
|
79
|
+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/pua-loop-hook.sh\""
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"type": "command",
|
|
83
|
+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/stop-feedback.sh\"",
|
|
84
|
+
"timeout": 10
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
],
|
|
89
|
+
"SubagentStop": [
|
|
90
|
+
{
|
|
91
|
+
"matcher": "*",
|
|
92
|
+
"hooks": [
|
|
93
|
+
{
|
|
94
|
+
"type": "command",
|
|
95
|
+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/subagent-teardown.sh\"",
|
|
96
|
+
"timeout": 5
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
],
|
|
101
|
+
"PreToolUse": [
|
|
102
|
+
{
|
|
103
|
+
"matcher": "Bash|Read|Grep|Glob|Edit|Write|MultiEdit|WebSearch|WebFetch",
|
|
104
|
+
"hooks": [
|
|
105
|
+
{
|
|
106
|
+
"type": "command",
|
|
107
|
+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/integrity-guard.sh\"",
|
|
108
|
+
"timeout": 5
|
|
109
|
+
}
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
]
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# PUA Integrity Guard — PreToolUse anti-cheating gate
|
|
3
|
+
# Separates action rights from scoring / verifier / environment-modification rights.
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
7
|
+
source "${SCRIPT_DIR}/flavor-helper.sh"
|
|
8
|
+
PUA_PY="$(pua_python_cmd 2>/dev/null || true)"
|
|
9
|
+
[ -n "$PUA_PY" ] || exit 0
|
|
10
|
+
PUA_CONFIG_PY="$(pua_to_python_path "$(pua_config_file)")"
|
|
11
|
+
export PUA_CONFIG_PY
|
|
12
|
+
|
|
13
|
+
TMP_INPUT=$(mktemp)
|
|
14
|
+
trap 'rm -f "$TMP_INPUT"' EXIT
|
|
15
|
+
cat > "$TMP_INPUT"
|
|
16
|
+
|
|
17
|
+
"$PUA_PY" - "$TMP_INPUT" <<'PY'
|
|
18
|
+
import io
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import re
|
|
22
|
+
import shlex
|
|
23
|
+
import sys
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
input_path = Path(sys.argv[1])
|
|
27
|
+
try:
|
|
28
|
+
data = json.loads(input_path.read_text(encoding='utf-8') or '{}')
|
|
29
|
+
except Exception:
|
|
30
|
+
sys.exit(0)
|
|
31
|
+
|
|
32
|
+
PUA_MARKERS = [
|
|
33
|
+
'PUA ACTIVATED',
|
|
34
|
+
'PUA Always-On',
|
|
35
|
+
'PUA生效',
|
|
36
|
+
'[PUA',
|
|
37
|
+
'pua:pua',
|
|
38
|
+
'pua-loop',
|
|
39
|
+
'Confidence Gate',
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def read_text_tail(path: str, max_bytes: int = 200_000) -> str:
|
|
44
|
+
try:
|
|
45
|
+
p = Path(path).expanduser()
|
|
46
|
+
if not p.is_file():
|
|
47
|
+
return ''
|
|
48
|
+
size = p.stat().st_size
|
|
49
|
+
with p.open('rb') as f:
|
|
50
|
+
if size > max_bytes:
|
|
51
|
+
f.seek(size - max_bytes)
|
|
52
|
+
return f.read().decode('utf-8', errors='ignore')
|
|
53
|
+
except Exception:
|
|
54
|
+
return ''
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def config_always_on() -> bool:
|
|
58
|
+
cfg = os.environ.get('PUA_CONFIG_PY') or os.environ.get('PUA_CONFIG') or str(Path.home() / '.pua' / 'config.json')
|
|
59
|
+
try:
|
|
60
|
+
return bool(json.loads(Path(cfg).expanduser().read_text(encoding='utf-8')).get('always_on', False))
|
|
61
|
+
except Exception:
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def is_active() -> bool:
|
|
66
|
+
if os.environ.get('PUA_INTEGRITY_FORCE') == '1' or os.environ.get('PUA_FORCE_ON') == '1':
|
|
67
|
+
return True
|
|
68
|
+
if config_always_on():
|
|
69
|
+
return True
|
|
70
|
+
transcript = data.get('transcript_path') or ''
|
|
71
|
+
text = read_text_tail(transcript)
|
|
72
|
+
return any(marker in text for marker in PUA_MARKERS)
|
|
73
|
+
|
|
74
|
+
if not is_active():
|
|
75
|
+
sys.exit(0)
|
|
76
|
+
|
|
77
|
+
tool = data.get('tool_name') or ''
|
|
78
|
+
tool_input = data.get('tool_input') or {}
|
|
79
|
+
|
|
80
|
+
# Keep these patterns intentionally conservative: they target governance assets,
|
|
81
|
+
# not ordinary application files.
|
|
82
|
+
PROTECTED_WRITE_PATTERNS = [
|
|
83
|
+
(re.compile(r'(^|/)(tests?|__tests__|test|spec|evals?|e2e|cypress|playwright)(/|$)|\.(test|spec)\.[A-Za-z0-9]+$|(^|/)(playwright|cypress)\.config\.', re.I), 'Grader gaming risk: tests/evals/E2E assets are scoring-adjacent.'),
|
|
84
|
+
(re.compile(r'(^|/)(score|scoring|grader|verifier)(\.[A-Za-z0-9]+)?$|(^|/)(scoring|grader|verifier)(/|$)', re.I), 'Grader gaming risk: scoring/verifier assets must not be changed by the executor.'),
|
|
85
|
+
(re.compile(r'(^|/)\.github/workflows(/|$)|(^|/)ci(/|$)|(^|/)(buildkite|circleci|jenkins)(/|$)', re.I), 'Environment-modification risk: CI gates are part of the verifier boundary.'),
|
|
86
|
+
(re.compile(r'(^|/)(feature_contracts\.json|claude-progress\.md|progress\.json|status\.json)$', re.I), 'Self-report cheating risk: status/progress files need verifier ownership.'),
|
|
87
|
+
(re.compile(r'(^|/)(memory|memories)(/|$)|(^|/)(decisions|failures)\.log\.jsonl$|(^|/)CLAUDE\.md$|(^|/)\.claude/(settings|settings\.local)\.json$', re.I), 'Persistent-memory risk: long-term memory/status must be append-only or approved.'),
|
|
88
|
+
(re.compile(r'(^|/)\.env(\.|$)|(^|/)(secrets?|credentials?)(\.|/|$)', re.I), 'Capability-abuse risk: secrets and environment files require human gate.'),
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
CONTAMINATION_PATTERNS = [
|
|
92
|
+
(re.compile(r'(^|/)(hidden[-_]?tests?|verifier[-_]?private|private[-_]?verifier|hidden[-_]?cases?)(/|$)', re.I), 'Solution contamination risk: hidden tests/verifier-private assets must stay outside the agent workspace.'),
|
|
93
|
+
(re.compile(r'(^|/)(hidden_solution|gold_patch|golden_patch|benchmark_answers?|answer_key|official_solution)(\.|/|$)', re.I), 'Solution contamination risk: hidden solution / benchmark answer artifact detected.'),
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
SENSITIVE_READ_PATTERNS = [
|
|
97
|
+
(re.compile(r'(^|/)\.env(\.|$)|(^|/)(secrets?|credentials?)(\.|/|$)|(^|/)(id_rsa|id_ed25519|private[-_]?key)(\.|$)', re.I), 'Capability-abuse risk: secrets and credentials require human gate.'),
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
MUTATING_BASH = re.compile(
|
|
101
|
+
r'(^|[;&|()\s])(rm|mv|cp|chmod|chown|truncate|tee|touch|mkdir|rmdir|git\s+(reset|clean|checkout|restore)|sed\s+(-i|--in-place)|perl\s+-p?i|python3?\s+.*open\(|node\s+.*writeFile|npm\s+version)\b|>>|>[^&]',
|
|
102
|
+
re.I | re.S,
|
|
103
|
+
)
|
|
104
|
+
READING_BASH = re.compile(r'(^|[;&|()\s])(cat|less|more|head|tail|sed|awk|grep|rg|find|python3?|node)\b', re.I)
|
|
105
|
+
WEB_CONTAMINATION = re.compile(r'(hidden[-_\s]+solution|official[-_\s]+solution|gold[-_\s]+patch|benchmark[-_\s]+answer|swe[-_\s]?bench[-_\s]+solution|leaderboard[-_\s]+answer)', re.I)
|
|
106
|
+
GIT_MUTATING_SUBCOMMANDS = {
|
|
107
|
+
'reset', 'clean', 'checkout', 'restore', 'apply', 'am', 'rm', 'mv',
|
|
108
|
+
}
|
|
109
|
+
GIT_DRY_RUN_SUBCOMMANDS = {'clean', 'rm', 'mv'}
|
|
110
|
+
GIT_APPLY_PREVIEW_OPTIONS = ('check', 'stat', 'numstat', 'summary')
|
|
111
|
+
GIT_GLOBAL_OPTIONS_WITH_VALUE = {
|
|
112
|
+
'-C', '-c', '--git-dir', '--work-tree', '--namespace', '--exec-path',
|
|
113
|
+
'--super-prefix', '--config-env',
|
|
114
|
+
}
|
|
115
|
+
GIT_GLOBAL_OPTIONS_WITH_ATTACHED_VALUE = (
|
|
116
|
+
'-C', '-c', '--git-dir=', '--work-tree=', '--namespace=', '--exec-path=',
|
|
117
|
+
'--super-prefix=', '--config-env=',
|
|
118
|
+
)
|
|
119
|
+
GIT_PATHSPEC_MAGIC = re.compile(r'(^:|[\*\?\[\]\{\}\$])')
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def command_tokens(command: str):
|
|
123
|
+
try:
|
|
124
|
+
return shlex.split(command)
|
|
125
|
+
except Exception:
|
|
126
|
+
return re.split(r'\s+', command)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def is_direct_git_command(tokens) -> bool:
|
|
130
|
+
if not tokens:
|
|
131
|
+
return False
|
|
132
|
+
executable = tokens[0].replace('\\', '/').rsplit('/', 1)[-1].lower()
|
|
133
|
+
return executable in {'git', 'git.exe'}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def git_subcommand_and_args(tokens):
|
|
137
|
+
"""Return a direct Git subcommand and its arguments, if one is present."""
|
|
138
|
+
if not is_direct_git_command(tokens):
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
arg_index = 1
|
|
142
|
+
while arg_index < len(tokens):
|
|
143
|
+
arg = tokens[arg_index]
|
|
144
|
+
if arg == '--':
|
|
145
|
+
return None
|
|
146
|
+
if arg in {'-h', '--help', '--version'}:
|
|
147
|
+
return None
|
|
148
|
+
if arg in GIT_GLOBAL_OPTIONS_WITH_VALUE:
|
|
149
|
+
arg_index += 2
|
|
150
|
+
continue
|
|
151
|
+
if arg.startswith(GIT_GLOBAL_OPTIONS_WITH_ATTACHED_VALUE):
|
|
152
|
+
arg_index += 1
|
|
153
|
+
continue
|
|
154
|
+
if arg.startswith('-'):
|
|
155
|
+
# Other Git global flags (for example --no-pager) take no argument
|
|
156
|
+
# for this narrow recognizer.
|
|
157
|
+
arg_index += 1
|
|
158
|
+
continue
|
|
159
|
+
return arg.lower(), tokens[arg_index + 1:]
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def git_dry_run_requested(args) -> bool:
|
|
164
|
+
dry_run = False
|
|
165
|
+
for arg in args:
|
|
166
|
+
if arg in {'-n', '--dry-run'}:
|
|
167
|
+
dry_run = True
|
|
168
|
+
elif arg == '--no-dry-run':
|
|
169
|
+
dry_run = False
|
|
170
|
+
return dry_run
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def git_apply_mutates(args) -> bool:
|
|
174
|
+
"""Handle apply's documented preview flags without parsing patch contents."""
|
|
175
|
+
preview = {name: False for name in GIT_APPLY_PREVIEW_OPTIONS}
|
|
176
|
+
apply_override = None
|
|
177
|
+
for arg in args:
|
|
178
|
+
if arg == '--apply':
|
|
179
|
+
apply_override = True
|
|
180
|
+
elif arg == '--no-apply':
|
|
181
|
+
apply_override = False
|
|
182
|
+
for name in GIT_APPLY_PREVIEW_OPTIONS:
|
|
183
|
+
if arg == f'--{name}' or arg.startswith(f'--{name}='):
|
|
184
|
+
preview[name] = True
|
|
185
|
+
elif arg == f'--no-{name}':
|
|
186
|
+
preview[name] = False
|
|
187
|
+
if apply_override is not None:
|
|
188
|
+
return apply_override
|
|
189
|
+
return not any(preview.values())
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def is_mutating_git_command(tokens):
|
|
193
|
+
"""Classify selected direct Git worktree changes and documented previews.
|
|
194
|
+
|
|
195
|
+
Git permits forms such as ``git -C /repo restore -- evals/case.sh``.
|
|
196
|
+
This intentionally handles only explicit worktree-changing subcommands and
|
|
197
|
+
their common preview forms; it is not a shell parser or Git policy engine.
|
|
198
|
+
"""
|
|
199
|
+
parts = git_subcommand_and_args(tokens)
|
|
200
|
+
if parts is None:
|
|
201
|
+
return None
|
|
202
|
+
subcommand, args = parts
|
|
203
|
+
if subcommand == 'apply':
|
|
204
|
+
return git_apply_mutates(args)
|
|
205
|
+
if subcommand in GIT_DRY_RUN_SUBCOMMANDS:
|
|
206
|
+
return not git_dry_run_requested(args)
|
|
207
|
+
if subcommand == 'am' and any(
|
|
208
|
+
arg == '--show-current-patch' or arg.startswith('--show-current-patch=')
|
|
209
|
+
for arg in args
|
|
210
|
+
):
|
|
211
|
+
return False
|
|
212
|
+
return subcommand in GIT_MUTATING_SUBCOMMANDS
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def mask_direct_git_subcommand(command: str, subcommand: str) -> str:
|
|
216
|
+
"""Mask only Git's leading subcommand before applying the shell heuristic.
|
|
217
|
+
|
|
218
|
+
A preview such as ``git rm --dry-run`` must not match the legacy ``rm``
|
|
219
|
+
shell rule. Conversely, ``git diff ... > evals/out`` and ``git show |
|
|
220
|
+
tee tests/out`` still have shell-side effects. This deliberately finds
|
|
221
|
+
only the direct Git subcommand (including global options), rather than
|
|
222
|
+
attempting to parse arbitrary shell syntax.
|
|
223
|
+
"""
|
|
224
|
+
try:
|
|
225
|
+
lexer = shlex.shlex(
|
|
226
|
+
io.StringIO(command), posix=True, punctuation_chars='|&;()<>'
|
|
227
|
+
)
|
|
228
|
+
lexer.whitespace_split = True
|
|
229
|
+
saw_executable = False
|
|
230
|
+
global_option_needs_value = False
|
|
231
|
+
while True:
|
|
232
|
+
token_start = lexer.instream.tell()
|
|
233
|
+
token = lexer.get_token()
|
|
234
|
+
token_end = lexer.instream.tell()
|
|
235
|
+
if token is None:
|
|
236
|
+
return command
|
|
237
|
+
if not saw_executable:
|
|
238
|
+
saw_executable = True
|
|
239
|
+
continue
|
|
240
|
+
if global_option_needs_value:
|
|
241
|
+
global_option_needs_value = False
|
|
242
|
+
continue
|
|
243
|
+
if token == '--' or token in {'|', '||', '&', '&&', ';', '(', ')', '<', '>', '>>'}:
|
|
244
|
+
return command
|
|
245
|
+
if token in GIT_GLOBAL_OPTIONS_WITH_VALUE:
|
|
246
|
+
global_option_needs_value = True
|
|
247
|
+
continue
|
|
248
|
+
if token.startswith(GIT_GLOBAL_OPTIONS_WITH_ATTACHED_VALUE):
|
|
249
|
+
continue
|
|
250
|
+
if token.startswith('-'):
|
|
251
|
+
continue
|
|
252
|
+
if token.lower() != subcommand:
|
|
253
|
+
return command
|
|
254
|
+
raw_token = command[token_start:token_end]
|
|
255
|
+
match = re.search(re.escape(subcommand), raw_token, re.I)
|
|
256
|
+
if not match:
|
|
257
|
+
return command
|
|
258
|
+
start = token_start + match.start()
|
|
259
|
+
end = token_start + match.end()
|
|
260
|
+
return command[:start] + '__pua_git_subcommand__' + command[end:]
|
|
261
|
+
except Exception:
|
|
262
|
+
return command
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def is_mutating_command(command: str) -> bool:
|
|
266
|
+
tokens = command_tokens(command)
|
|
267
|
+
git_mutates = is_mutating_git_command(tokens)
|
|
268
|
+
if git_mutates is True:
|
|
269
|
+
return True
|
|
270
|
+
if git_mutates is False:
|
|
271
|
+
# Do not short-circuit the generic heuristic: a read-only Git command
|
|
272
|
+
# can still redirect or pipe into a separate shell write. Mask only
|
|
273
|
+
# the recognized Git subcommand so preview flags do not inherit the
|
|
274
|
+
# generic ``rm``/``mv`` false positive.
|
|
275
|
+
parts = git_subcommand_and_args(tokens)
|
|
276
|
+
if parts is not None:
|
|
277
|
+
command = mask_direct_git_subcommand(command, parts[0])
|
|
278
|
+
if MUTATING_BASH.search(command):
|
|
279
|
+
return True
|
|
280
|
+
# Python one-liners often hide writes inside quoted code, so detect common
|
|
281
|
+
# write APIs separately instead of relying on shell-token boundaries.
|
|
282
|
+
return bool(re.search(r'python3?\s+.*(open\(|write_text\(|write_bytes\(|Path\([^)]*\)\.write)', command, re.I | re.S))
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def norm_path(p: str) -> str:
|
|
286
|
+
if not p:
|
|
287
|
+
return ''
|
|
288
|
+
return p.replace('\\', '/')
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def collect_paths(value):
|
|
292
|
+
paths = []
|
|
293
|
+
if isinstance(value, dict):
|
|
294
|
+
for k, v in value.items():
|
|
295
|
+
if k in {'file_path', 'path', 'notebook_path', 'pattern', 'glob'} and isinstance(v, str):
|
|
296
|
+
paths.append(v)
|
|
297
|
+
else:
|
|
298
|
+
paths.extend(collect_paths(v))
|
|
299
|
+
elif isinstance(value, list):
|
|
300
|
+
for item in value:
|
|
301
|
+
paths.extend(collect_paths(item))
|
|
302
|
+
return paths
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def find_reason_for_path(path: str, include_write: bool):
|
|
306
|
+
n = norm_path(path)
|
|
307
|
+
for rx, reason in CONTAMINATION_PATTERNS:
|
|
308
|
+
if rx.search(n):
|
|
309
|
+
return 'deny', reason, n
|
|
310
|
+
for rx, reason in SENSITIVE_READ_PATTERNS:
|
|
311
|
+
if rx.search(n):
|
|
312
|
+
return 'advisory', reason, n
|
|
313
|
+
if include_write:
|
|
314
|
+
for rx, reason in PROTECTED_WRITE_PATTERNS:
|
|
315
|
+
if rx.search(n):
|
|
316
|
+
return 'advisory', reason, n
|
|
317
|
+
return None
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def looks_like_path(s: str) -> bool:
|
|
321
|
+
# A real path has a directory separator or a file-extension suffix; bare
|
|
322
|
+
# identifiers like the shell `eval` builtin do not, and must not be matched
|
|
323
|
+
# against (^|/)(evals?|tests?|spec|...)(/|$) as if they were paths.
|
|
324
|
+
if '/' in s or '\\' in s:
|
|
325
|
+
return True
|
|
326
|
+
return bool(re.search(r'\.[A-Za-z0-9]+$', s))
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def path_candidates(tokens):
|
|
330
|
+
for token in tokens:
|
|
331
|
+
if not token:
|
|
332
|
+
continue
|
|
333
|
+
stripped = token.strip("\"'`")
|
|
334
|
+
if stripped and looks_like_path(stripped):
|
|
335
|
+
yield stripped
|
|
336
|
+
# Pull paths embedded inside code strings, e.g. open("tests/fixtures.json", "w").
|
|
337
|
+
for match in re.findall(r'[A-Za-z0-9_.@+~:-]+(?:/[A-Za-z0-9_.@+~:-]+)+', token.replace('\\', '/')):
|
|
338
|
+
yield match
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def git_include_path_candidates(tokens):
|
|
342
|
+
"""Extract Git apply/am include values as paths, not option spellings."""
|
|
343
|
+
parts = git_subcommand_and_args(tokens)
|
|
344
|
+
if parts is None or parts[0] not in {'apply', 'am'}:
|
|
345
|
+
return
|
|
346
|
+
for index, token in enumerate(tokens):
|
|
347
|
+
if token.startswith('--include='):
|
|
348
|
+
value = token.split('=', 1)[1]
|
|
349
|
+
if value:
|
|
350
|
+
yield value
|
|
351
|
+
elif token == '--include' and index + 1 < len(tokens):
|
|
352
|
+
value = tokens[index + 1]
|
|
353
|
+
if value and value != '--':
|
|
354
|
+
yield value
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def is_explicit_ordinary_git_path(path: str) -> bool:
|
|
358
|
+
"""Whether a literal path proves a Git mutation stays off protected assets."""
|
|
359
|
+
normalized = norm_path(path)
|
|
360
|
+
return bool(
|
|
361
|
+
normalized
|
|
362
|
+
and looks_like_path(normalized)
|
|
363
|
+
and not normalized.endswith('/')
|
|
364
|
+
and not GIT_PATHSPEC_MAGIC.search(normalized)
|
|
365
|
+
and find_reason_for_path(normalized, include_write=True) is None
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def opaque_git_mutation_target(tokens):
|
|
370
|
+
"""Return a target label when a mutating Git command lacks safe scope.
|
|
371
|
+
|
|
372
|
+
Patch application can touch an unknown set unless literal --include paths
|
|
373
|
+
bound it. reset/checkout/clean need literal pathspecs after ``--``; a
|
|
374
|
+
branch, ref, or no pathspec can affect protected assets. This is a narrow
|
|
375
|
+
command-boundary check, not a Git or shell sandbox.
|
|
376
|
+
"""
|
|
377
|
+
parts = git_subcommand_and_args(tokens)
|
|
378
|
+
if parts is None or is_mutating_git_command(tokens) is not True:
|
|
379
|
+
return None
|
|
380
|
+
subcommand, args = parts
|
|
381
|
+
if subcommand in {'apply', 'am'}:
|
|
382
|
+
include_paths = list(git_include_path_candidates(tokens))
|
|
383
|
+
if include_paths and all(is_explicit_ordinary_git_path(path) for path in include_paths):
|
|
384
|
+
return None
|
|
385
|
+
return f'git {subcommand} (unbounded target set)'
|
|
386
|
+
if subcommand in {'reset', 'checkout', 'clean'}:
|
|
387
|
+
if '--' in args:
|
|
388
|
+
pathspecs = args[args.index('--') + 1:]
|
|
389
|
+
if pathspecs and all(is_explicit_ordinary_git_path(path) for path in pathspecs):
|
|
390
|
+
return None
|
|
391
|
+
return f'git {subcommand} (unbounded target set)'
|
|
392
|
+
return None
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
SSH_IDENTITY_RE = re.compile(r'\bssh\b.*-i\s', re.I)
|
|
396
|
+
SSH_KEY_PATH_RE = re.compile(r'(^|/)\.ssh/(id_|.*[-_]key)', re.I)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def is_ssh_identity_usage(command: str, candidate: str) -> bool:
|
|
400
|
+
if not SSH_IDENTITY_RE.search(command):
|
|
401
|
+
return False
|
|
402
|
+
return bool(SSH_KEY_PATH_RE.search(norm_path(candidate)))
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def command_hits(command: str):
|
|
406
|
+
tokens = [t for t in command_tokens(command) if t]
|
|
407
|
+
# Include values are protected paths even though their command-line token
|
|
408
|
+
# begins with an option prefix. Put them first so advisory output names the
|
|
409
|
+
# actual asset rather than ``--include=<path>``.
|
|
410
|
+
candidates = list(git_include_path_candidates(tokens))
|
|
411
|
+
candidates.extend(path_candidates(tokens))
|
|
412
|
+
normalized = command.replace('\\', '/')
|
|
413
|
+
|
|
414
|
+
# Hidden/private solution artifacts are blocked even for read-like commands.
|
|
415
|
+
for candidate in candidates:
|
|
416
|
+
for rx, reason in CONTAMINATION_PATTERNS:
|
|
417
|
+
if rx.search(norm_path(candidate)):
|
|
418
|
+
return 'deny', reason, candidate
|
|
419
|
+
for rx, reason in CONTAMINATION_PATTERNS:
|
|
420
|
+
m = rx.search(normalized)
|
|
421
|
+
if m:
|
|
422
|
+
return 'deny', reason, m.group(0)
|
|
423
|
+
if WEB_CONTAMINATION.search(command):
|
|
424
|
+
return 'deny', 'Solution contamination risk: command appears to search/fetch benchmark or hidden answers.', command[:160]
|
|
425
|
+
if READING_BASH.search(command):
|
|
426
|
+
for candidate in candidates:
|
|
427
|
+
if is_ssh_identity_usage(command, candidate):
|
|
428
|
+
continue
|
|
429
|
+
for rx, reason in SENSITIVE_READ_PATTERNS:
|
|
430
|
+
if rx.search(norm_path(candidate)):
|
|
431
|
+
return 'advisory', reason, candidate
|
|
432
|
+
|
|
433
|
+
# Protected scoring assets need a human gate only when the command mutates them.
|
|
434
|
+
if is_mutating_command(command):
|
|
435
|
+
for candidate in candidates:
|
|
436
|
+
for rx, reason in PROTECTED_WRITE_PATTERNS:
|
|
437
|
+
if rx.search(norm_path(candidate)):
|
|
438
|
+
return 'advisory', reason, candidate
|
|
439
|
+
for rx, reason in PROTECTED_WRITE_PATTERNS:
|
|
440
|
+
m = rx.search(normalized)
|
|
441
|
+
if m:
|
|
442
|
+
return 'advisory', reason, m.group(0)
|
|
443
|
+
opaque_target = opaque_git_mutation_target(tokens)
|
|
444
|
+
if opaque_target:
|
|
445
|
+
return (
|
|
446
|
+
'advisory',
|
|
447
|
+
'Grader gaming risk: Git mutation target set cannot be proven limited to ordinary source paths.',
|
|
448
|
+
opaque_target,
|
|
449
|
+
)
|
|
450
|
+
return None
|
|
451
|
+
|
|
452
|
+
hit = None
|
|
453
|
+
|
|
454
|
+
if tool in {'Write', 'Edit', 'MultiEdit'}:
|
|
455
|
+
for path in collect_paths(tool_input):
|
|
456
|
+
hit = find_reason_for_path(path, include_write=True)
|
|
457
|
+
if hit:
|
|
458
|
+
break
|
|
459
|
+
elif tool in {'Read', 'Grep', 'Glob'}:
|
|
460
|
+
for path in collect_paths(tool_input):
|
|
461
|
+
hit = find_reason_for_path(path, include_write=False)
|
|
462
|
+
if hit:
|
|
463
|
+
break
|
|
464
|
+
elif tool == 'Bash':
|
|
465
|
+
command = str(tool_input.get('command') or '')
|
|
466
|
+
hit = command_hits(command)
|
|
467
|
+
elif tool in {'WebSearch', 'WebFetch'}:
|
|
468
|
+
query = '\n'.join(str(tool_input.get(k) or '') for k in ('query', 'url', 'prompt'))
|
|
469
|
+
if WEB_CONTAMINATION.search(query):
|
|
470
|
+
hit = ('deny', 'Solution contamination risk: searching for benchmark/hidden answers can poison the task.', query[:160])
|
|
471
|
+
|
|
472
|
+
if not hit:
|
|
473
|
+
sys.exit(0)
|
|
474
|
+
|
|
475
|
+
decision, reason, target = hit
|
|
476
|
+
message = (
|
|
477
|
+
'PUA Integrity Guard: ' + reason +
|
|
478
|
+
' Four-power separation is active: action right, self-evaluation right, scoring right, and environment-modification right must remain separate. '
|
|
479
|
+
f'Target: {target}'
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
output = {'hookSpecificOutput': {'hookEventName': 'PreToolUse'}}
|
|
483
|
+
if decision == 'deny':
|
|
484
|
+
output['hookSpecificOutput']['permissionDecision'] = 'deny'
|
|
485
|
+
output['hookSpecificOutput']['permissionDecisionReason'] = message
|
|
486
|
+
output['hookSpecificOutput']['additionalContext'] = (
|
|
487
|
+
'PUA Integrity Guard: DENY — ' + reason + f' Target: {target}'
|
|
488
|
+
)
|
|
489
|
+
else:
|
|
490
|
+
output['hookSpecificOutput']['additionalContext'] = (
|
|
491
|
+
'PUA Integrity Guard (advisory): ' + reason + f' Target: {target}'
|
|
492
|
+
)
|
|
493
|
+
print(json.dumps(output, ensure_ascii=False, separators=(',', ':')))
|
|
494
|
+
PY
|