@chrono-meta/fh-gate 3.0.0 → 3.1.0
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/regression/probes_live.yaml +137 -0
- package/.claude/rules/.residency-patterns.defaults +7 -0
- package/.claude/rules/fh_4axis_gate.md +50 -1
- package/.claude-plugin/marketplace.json +8 -2
- package/AGENTS.md +27 -0
- package/CATALOG.md +17 -0
- package/CLAUDE.md +12 -2
- package/README.ja.md +51 -7
- package/README.ko.md +48 -7
- package/README.md +37 -5
- package/README.zh.md +45 -8
- package/docs/STANDARDS_ALIGNMENT.md +61 -0
- package/docs/USER_GUIDE.md +3 -0
- package/docs/USE_CASES.md +50 -0
- package/docs/model_tier_expectations.md +60 -0
- package/knowledge/shared/harness-core/fh_three_layer_canon.md +30 -10
- package/knowledge/shared/harness-core/field_verdict_crossfamily_gate.md +8 -0
- package/knowledge/shared/harness-core/harness_incubator_doctrine.md +9 -0
- package/knowledge/shared/harness-core/iso_ai_standards_crosswalk.md +139 -0
- package/knowledge/shared/harness-core/measurement-integrity-checklist.md +20 -0
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +441 -4
- package/package.json +40 -2
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-commons/skills/preprep/README.md +4 -1
- package/plugins/fh-commons/skills/preprep/SKILL.md +99 -3
- package/plugins/fh-commons/skills/preprep/diagram_from_json.py +154 -0
- package/plugins/fh-commons/skills/preprep/fixtures/fixture_R3_negative.pptx +0 -0
- package/plugins/fh-commons/skills/preprep/fixtures/fixture_R3_positive.pptx +0 -0
- package/plugins/fh-commons/skills/preprep/fixtures/mk_slide_fixtures.py +179 -0
- package/plugins/fh-commons/skills/preprep/interslide_deps.py +98 -9
- package/plugins/fh-commons/skills/preprep/lane_adjacent_dup.py +4 -1
- package/plugins/fh-commons/skills/preprep/lane_diagram.py +95 -0
- package/plugins/fh-commons/skills/preprep/lane_geometry.py +181 -0
- package/plugins/fh-commons/skills/preprep/lane_promise.py +4 -1
- package/plugins/fh-commons/skills/preprep/lane_slide_refs.py +134 -0
- package/plugins/fh-commons/skills/preprep/lane_slide_relations.py +300 -0
- package/plugins/fh-commons/skills/preprep/preprep.py +129 -8
- package/plugins/fh-commons/skills/preprep/preprep_wire.py +210 -0
- package/plugins/fh-commons/skills/preprep/presentation_checklist.md +4 -0
- package/plugins/fh-commons/skills/preprep/surfaces.example.yaml +18 -0
- package/plugins/fh-commons/skills/preprep/test_preprep_lanes_rp.py +184 -0
- package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/CHANGELOG.md +43 -1
- package/plugins/fh-meta/skills/auto-decorrelation/SKILL.md +87 -2
- package/plugins/fh-meta/skills/frontier-digest/SKILL.md +2 -2
- package/plugins/fh-meta/skills/frontier-digest/SKILL_detail.md +25 -5
- package/plugins/fh-meta/skills/hub-cc-pr-reviewer/SKILL.md +11 -0
- package/plugins/fh-meta/skills/hub-cc-pr-reviewer/SKILL_detail.md +3 -0
- package/plugins/fh-qp/.claude-plugin/plugin.json +22 -0
- package/plugins/fh-qp/README.md +71 -0
- package/plugins/fh-qp/fixtures/evidence_known_clean.txt +3 -0
- package/plugins/fh-qp/fixtures/evidence_known_dirty.txt +6 -0
- package/plugins/fh-qp/fixtures/reach_known_partial.tsv +4 -0
- package/plugins/fh-qp/fixtures/reach_known_reached.tsv +4 -0
- package/plugins/fh-qp/fixtures/reach_known_wall.tsv +3 -0
- package/plugins/fh-qp/fixtures/verdicts_known_bad_branch.tsv +2 -0
- package/plugins/fh-qp/fixtures/verdicts_known_bad_vacuous_machine.tsv +2 -0
- package/plugins/fh-qp/fixtures/verdicts_known_good.tsv +4 -0
- package/plugins/fh-qp/fixtures/verdicts_verify_only.tsv +3 -0
- package/plugins/fh-qp/qp_profile.example.yaml +29 -0
- package/plugins/fh-qp/scripts/qp_tools.sh +216 -0
- package/plugins/fh-qp/skills/qp/SKILL.md +65 -0
- package/plugins/fh-qp/skills/qp-plan/SKILL.md +42 -0
- package/plugins/fh-qp/skills/qp-regress/SKILL.md +45 -0
- package/plugins/fh-qp/skills/qp-run/SKILL.md +49 -0
- package/scripts/chamber_run.sh +14 -5
- package/scripts/com.forge-harness.live-eval.plist +84 -0
- package/scripts/compaction_probe.sh +9 -35
- package/scripts/directional_diff_gate.sh +14 -2
- package/scripts/frontier_digest_autopilot.sh +4 -1
- package/scripts/map_postprocess.py +90 -0
- package/scripts/outbound_query_guard.sh +131 -0
- package/scripts/outbound_query_hook.sh +373 -0
- package/scripts/package_coverage_check.sh +55 -16
- package/scripts/pipe_verdict_guard.sh +41 -1
- package/scripts/probe_live_eval.sh +240 -0
- package/scripts/probe_live_eval_lib.py +579 -0
- package/scripts/proposal_hook.sh +120 -17
- package/scripts/push_zone_check.sh +78 -0
- package/scripts/residency_closure_scan.py +252 -0
- package/scripts/selfcheck.sh +41 -1
- package/scripts/session_close_check.sh +100 -0
- package/scripts/sim_isolated_run.sh +98 -2
- package/scripts/test_action_yml_lanes.sh +97 -0
- package/scripts/test_fh_qp_lanes.sh +105 -0
- package/scripts/test_gate_two_verdicts_lanes.sh +139 -0
- package/scripts/test_map_postprocess_lanes.sh +143 -0
- package/scripts/test_marker_affected_lanes.sh +93 -0
- package/scripts/test_marker_crossfamily_lanes.sh +90 -6
- package/scripts/test_marker_oracle_lanes.sh +136 -0
- package/scripts/test_outbound_query_hook_lanes.sh +433 -0
- package/scripts/test_outbound_query_lanes.sh +87 -0
- package/scripts/test_pipe_verdict_guard_lanes.sh +26 -0
- package/scripts/test_preprep_diagram_lanes.sh +87 -0
- package/scripts/test_preprep_drift_anchor.sh +3 -3
- package/scripts/test_preprep_slide_refs_lanes.sh +169 -0
- package/scripts/test_probe_live_eval_lanes.sh +437 -0
- package/scripts/test_proposal_hook_lanes.sh +22 -1
- package/scripts/test_push_zone_lanes.sh +304 -0
- package/scripts/test_residency_closure_lanes.sh +70 -0
- package/scripts/test_sim_isolated_run_lanes.sh +119 -0
- package/scripts/test_utterance_intake_lanes.sh +414 -0
- package/scripts/test_worktree_reclaim_lanes.sh +70 -0
- package/scripts/transcript_utterances.py +222 -0
- package/scripts/utterance_intake.sh +424 -0
- package/scripts/validate_yaml.sh +27 -0
- package/scripts/worktree_reclaim.sh +95 -0
- package/templates/.git-hooks/pre-commit +353 -1
- package/templates/.git-hooks/pre-push +91 -0
- package/templates/RED_TEAM_REPORT.md +49 -0
- package/templates/settings.PreToolUse.snippet.json +65 -1
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_push_zone_lanes.sh — known-pair anchor for the pre-push hook's Push-Zone verdict, and for
|
|
3
|
+
# the session-close ①-f advisory that mirrors it.
|
|
4
|
+
#
|
|
5
|
+
# ── WHY THIS FILE EXISTS ───────────────────────────────────────────────────────────────────────
|
|
6
|
+
# 2026-09-05: templates/.git-hooks/pre-push gained a new verdict path and scripts/session_close_
|
|
7
|
+
# check.sh gained a new ①-f advisory, both implementing CLAUDE.local.md §REST API push 계정규칙
|
|
8
|
+
# mechanically — a push to a github.com remote whose owner is NOT on the operator's owner-account
|
|
9
|
+
# list needs the REST Contents API channel, not plain git push. Before this suite the new code had
|
|
10
|
+
# no known-pair anchor at all. Per CLAUDE.md §Instrument Calibration, a gate is not trusted until
|
|
11
|
+
# calibrated on at least one known-positive and one known-negative, hand-verified.
|
|
12
|
+
#
|
|
13
|
+
# ── DISCIPLINE INHERITED FROM THE SIBLING SUITE (test_prepush_destructive_lanes.sh) ────────────
|
|
14
|
+
# (a) Run the hook FOR REAL — `bash -n` is not an instrument (a runtime fault can parse clean,
|
|
15
|
+
# abort the hook, and still exit 0).
|
|
16
|
+
# (b) A runtime fault is checked BEFORE the verdict, else an aborted hook scores as a clean pass.
|
|
17
|
+
# (c) "It blocked" is NOT "it blocked correctly" — a block must be ATTRIBUTED to a named FH gate.
|
|
18
|
+
# (d) NEVER read or assert on the operator's real owner-account list
|
|
19
|
+
# (.claude/rules/.push-zone-owners). Every lane below builds its own throwaway fixture list
|
|
20
|
+
# via the PUSH_ZONE_OWNERS override, with fictitious account names. The real file, if present
|
|
21
|
+
# on this machine, is never opened by this suite in either direction.
|
|
22
|
+
#
|
|
23
|
+
# Runs entirely in mktemp throwaway repos: never touches this repo's index, worktree, refs, or its
|
|
24
|
+
# real .claude/rules/.push-zone-owners.
|
|
25
|
+
#
|
|
26
|
+
# Usage: bash scripts/test_push_zone_lanes.sh
|
|
27
|
+
# FH_PUSH_ZONE_HOOK_SRC=<path> → test a DIFFERENT copy of the pre-push hook (fail-before proof).
|
|
28
|
+
# FH_PUSH_ZONE_SCC_SRC=<path> → same, for session_close_check.sh.
|
|
29
|
+
set -uo pipefail
|
|
30
|
+
. "$(dirname "${BASH_SOURCE[0]}")/fixture_guard_lib.sh"
|
|
31
|
+
|
|
32
|
+
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
|
33
|
+
HOOK_SRC="${FH_PUSH_ZONE_HOOK_SRC:-$REPO_ROOT/templates/.git-hooks/pre-push}"
|
|
34
|
+
SCC_SRC="${FH_PUSH_ZONE_SCC_SRC:-$REPO_ROOT/scripts/session_close_check.sh}"
|
|
35
|
+
DEF_SRC="$REPO_ROOT/.claude/rules/.public-surface-patterns.defaults"
|
|
36
|
+
ZERO="0000000000000000000000000000000000000000"
|
|
37
|
+
|
|
38
|
+
if [ ! -f "$HOOK_SRC" ]; then
|
|
39
|
+
echo "HARNESS_ERROR — pre-push hook not found at $HOOK_SRC (subject absent; NOT a pass)"; exit 1
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
WORK="$(fh_fixture_root "$(mktemp -d)")" || exit 1
|
|
43
|
+
: "${WORK:?fixture root unset — refusing to run git in cwd}"
|
|
44
|
+
trap 'rm -rf "$WORK"' EXIT
|
|
45
|
+
cp "$HOOK_SRC" "$WORK/hook" || exit 1
|
|
46
|
+
HOOK="$WORK/hook"
|
|
47
|
+
|
|
48
|
+
# Fictitious owner-account fixture — NEVER the operator's real list. The second entry
|
|
49
|
+
# (acme-shared-org) is asserted to never appear in any captured output — it is a decoy for the
|
|
50
|
+
# "list content is never printed" requirement, not used by any URL in this suite.
|
|
51
|
+
OWNERS_OK="$WORK/owners-ok.txt"
|
|
52
|
+
printf '# fixture owner list — test only, not the operator real list\nknown-owner\nacme-shared-org\n' > "$OWNERS_OK"
|
|
53
|
+
|
|
54
|
+
PASSED=0; FAILED=0; ALL_OUT=""
|
|
55
|
+
|
|
56
|
+
# ── fixture: same shape as test_prepush_destructive_lanes.sh's newrepo() ───────────────────────
|
|
57
|
+
newrepo() {
|
|
58
|
+
local d; d="$(fh_fixture_root "$(mktemp -d)")" || return 1
|
|
59
|
+
: "${d:?fixture root unset — refusing to run git in cwd}"
|
|
60
|
+
mkdir -p "$d/.claude/rules" "$d/templates/.git-hooks" "$d/tracks/_meta" "$d/scripts" || return 1
|
|
61
|
+
[ -f "$DEF_SRC" ] && cp "$DEF_SRC" "$d/.claude/rules/.public-surface-patterns.defaults"
|
|
62
|
+
[ -f "$REPO_ROOT/scripts/psa_scan_lib.sh" ] && cp "$REPO_ROOT/scripts/psa_scan_lib.sh" "$d/scripts/psa_scan_lib.sh"
|
|
63
|
+
printf '.claude/rules/.public-surface-patterns\n' > "$d/.gitignore"
|
|
64
|
+
cp "$HOOK" "$d/templates/.git-hooks/pre-push" || return 1
|
|
65
|
+
printf 'base\n' > "$d/a.txt"
|
|
66
|
+
(
|
|
67
|
+
cd "$d" || exit 1
|
|
68
|
+
git init -q || exit 1
|
|
69
|
+
git symbolic-ref HEAD refs/heads/main || exit 1
|
|
70
|
+
git config user.email t@example.com || exit 1
|
|
71
|
+
git config user.name t || exit 1
|
|
72
|
+
git add -A >/dev/null 2>&1 || exit 1
|
|
73
|
+
git commit -qm base >/dev/null 2>&1 || exit 1
|
|
74
|
+
git update-ref refs/remotes/origin/main HEAD || exit 1
|
|
75
|
+
) || return 1
|
|
76
|
+
printf '%s' "$d"
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
require_repo() { # $1 = candidate repo path, $2 = lane name
|
|
80
|
+
if [ -z "${1:-}" ] || [ ! -d "$1/.git" ]; then
|
|
81
|
+
echo "HARNESS_ERROR — fixture repo not built for $2 (setup failed; NOT a pass)"
|
|
82
|
+
exit 1
|
|
83
|
+
fi
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# check_zone <name> <repo> <owners-file|""> <url> <push_zone_ok:0|1> <expect:block|pass> <cause-regex|""> <refline>
|
|
87
|
+
check_zone() {
|
|
88
|
+
local name="$1" repo="$2" owners="$3" url="$4" pzok="$5" expect="$6" cause="$7"; shift 7
|
|
89
|
+
local out rc got _pzsrc
|
|
90
|
+
require_repo "$repo" "$name"
|
|
91
|
+
_pzsrc="${owners:-$WORK/nonexistent-owners-file-for-zone-test}"
|
|
92
|
+
out=$(printf '%s\n' "$@" | ( cd "$repo" && PUSH_ZONE_OWNERS="$_pzsrc" PUSH_ZONE_OK="$pzok" \
|
|
93
|
+
bash templates/.git-hooks/pre-push origin "$url" 2>&1 )); rc=$?
|
|
94
|
+
ALL_OUT="$ALL_OUT
|
|
95
|
+
$out"
|
|
96
|
+
|
|
97
|
+
if printf '%s' "$out" | grep -qiE 'bad substitution|unbound variable|syntax error|command not found'; then
|
|
98
|
+
echo " ❌ $name — RUNTIME FAULT in the hook (aborted, not passed)"
|
|
99
|
+
printf '%s\n' "$out" | grep -iE 'bad substitution|unbound|syntax error|command not found' | sed 's/^/ /' | head -3
|
|
100
|
+
FAILED=$((FAILED+1)); return
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
if [ "$rc" -ne 0 ]; then
|
|
104
|
+
if printf '%s' "$out" | grep -qE 'FH (Push-Zone Gate|Destructive-Op Gate)'; then
|
|
105
|
+
got=block
|
|
106
|
+
else
|
|
107
|
+
echo " ❌ $name — blocked, but NOT by a named FH gate (harness reason ≠ finding)"
|
|
108
|
+
printf '%s\n' "$out" | sed 's/^/ | /' | head -8
|
|
109
|
+
FAILED=$((FAILED+1)); return
|
|
110
|
+
fi
|
|
111
|
+
else
|
|
112
|
+
got=pass
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
if [ "$got" != "$expect" ]; then
|
|
116
|
+
echo " ❌ $name — expected $expect, got $got"
|
|
117
|
+
printf '%s\n' "$out" | sed 's/^/ | /' | head -10
|
|
118
|
+
FAILED=$((FAILED+1)); return
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
if [ -n "$cause" ] && ! printf '%s' "$out" | grep -qE "$cause"; then
|
|
122
|
+
echo " ❌ $name — verdict $got is correct but UNATTRIBUTED (missing: $cause)"
|
|
123
|
+
printf '%s\n' "$out" | sed 's/^/ | /' | head -10
|
|
124
|
+
FAILED=$((FAILED+1)); return
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
echo " ✅ $name (expected $expect)"
|
|
128
|
+
PASSED=$((PASSED+1))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
echo "[push-zone] known-pair anchor for the pre-push Push-Zone verdict (hook: $HOOK_SRC)"
|
|
132
|
+
echo ""
|
|
133
|
+
|
|
134
|
+
# Z0 — no owner-account list at all → UNCALIBRATED, plain push allowed.
|
|
135
|
+
R=$(newrepo); require_repo "$R" "Z0"
|
|
136
|
+
BASESHA=$(cd "$R" && git rev-parse HEAD)
|
|
137
|
+
NEWSHA=$(cd "$R" && printf 'x\n' >> a.txt && git commit -qam ff >/dev/null && git rev-parse HEAD)
|
|
138
|
+
check_zone "Z0 no owner-account list → PASS " "$R" "" \
|
|
139
|
+
"https://github.com/known-owner/repo.git" 0 pass 'push-zone: UNCALIBRATED' \
|
|
140
|
+
"refs/heads/feat $NEWSHA refs/heads/feat $BASESHA"
|
|
141
|
+
rm -rf "$R"
|
|
142
|
+
|
|
143
|
+
# Z1 — owner IN the list, https → silent pass; the rest of the hook (confidentiality) still runs.
|
|
144
|
+
R=$(newrepo); require_repo "$R" "Z1"
|
|
145
|
+
BASESHA=$(cd "$R" && git rev-parse HEAD)
|
|
146
|
+
NEWSHA=$(cd "$R" && printf 'x\n' >> a.txt && git commit -qam ff >/dev/null && git rev-parse HEAD)
|
|
147
|
+
check_zone "Z1 owner IN list, https → PASS " "$R" "$OWNERS_OK" \
|
|
148
|
+
"https://github.com/known-owner/repo.git" 0 pass 'FH Pre-Publish' \
|
|
149
|
+
"refs/heads/feat $NEWSHA refs/heads/feat $BASESHA"
|
|
150
|
+
rm -rf "$R"
|
|
151
|
+
|
|
152
|
+
# Z2 — owner OUT of the list, https → BLOCK, attributed, names the URL's (fictitious) owner.
|
|
153
|
+
R=$(newrepo); require_repo "$R" "Z2"
|
|
154
|
+
BASESHA=$(cd "$R" && git rev-parse HEAD)
|
|
155
|
+
NEWSHA=$(cd "$R" && printf 'x\n' >> a.txt && git commit -qam ff >/dev/null && git rev-parse HEAD)
|
|
156
|
+
check_zone "Z2 owner OUT of list, https → BLOCK" "$R" "$OWNERS_OK" \
|
|
157
|
+
"https://github.com/someone-else/repo.git" 0 block 'FH Push-Zone Gate' \
|
|
158
|
+
"refs/heads/feat $NEWSHA refs/heads/feat $BASESHA"
|
|
159
|
+
if printf '%s' "$ALL_OUT" | grep -qF 'github.com/someone-else'; then
|
|
160
|
+
echo " ✅ Z2b block message names the URL's owner"; PASSED=$((PASSED+1))
|
|
161
|
+
else
|
|
162
|
+
echo " ❌ Z2b — block message did not name the URL's owner (someone-else)"; FAILED=$((FAILED+1))
|
|
163
|
+
fi
|
|
164
|
+
rm -rf "$R"
|
|
165
|
+
|
|
166
|
+
# Z3 — owner OUT of the list, scp-like ssh (git@github.com:owner/repo) → BLOCK.
|
|
167
|
+
R=$(newrepo); require_repo "$R" "Z3"
|
|
168
|
+
BASESHA=$(cd "$R" && git rev-parse HEAD)
|
|
169
|
+
NEWSHA=$(cd "$R" && printf 'x\n' >> a.txt && git commit -qam ff >/dev/null && git rev-parse HEAD)
|
|
170
|
+
check_zone "Z3 owner OUT of list, ssh scp → BLOCK" "$R" "$OWNERS_OK" \
|
|
171
|
+
"git@github.com:someone-else/repo.git" 0 block 'FH Push-Zone Gate' \
|
|
172
|
+
"refs/heads/feat $NEWSHA refs/heads/feat $BASESHA"
|
|
173
|
+
rm -rf "$R"
|
|
174
|
+
|
|
175
|
+
# Z4 — remote host is not github.com → out of scope for this axis, pass allowed, says so.
|
|
176
|
+
R=$(newrepo); require_repo "$R" "Z4"
|
|
177
|
+
BASESHA=$(cd "$R" && git rev-parse HEAD)
|
|
178
|
+
NEWSHA=$(cd "$R" && printf 'x\n' >> a.txt && git commit -qam ff >/dev/null && git rev-parse HEAD)
|
|
179
|
+
check_zone "Z4 non-github host → PASS " "$R" "$OWNERS_OK" \
|
|
180
|
+
"https://ghe.example.com/x/y" 0 pass 'not github.com' \
|
|
181
|
+
"refs/heads/feat $NEWSHA refs/heads/feat $BASESHA"
|
|
182
|
+
rm -rf "$R"
|
|
183
|
+
|
|
184
|
+
# Z5 — PUSH_ZONE_OK=1 override on an out-of-list push → allowed, conscious, LOGGED.
|
|
185
|
+
R=$(newrepo); require_repo "$R" "Z5"
|
|
186
|
+
BASESHA=$(cd "$R" && git rev-parse HEAD)
|
|
187
|
+
NEWSHA=$(cd "$R" && printf 'x\n' >> a.txt && git commit -qam ff >/dev/null && git rev-parse HEAD)
|
|
188
|
+
check_zone "Z5 PUSH_ZONE_OK=1 override → PASS " "$R" "$OWNERS_OK" \
|
|
189
|
+
"https://github.com/someone-else/repo.git" 1 pass 'PUSH_ZONE_OK=1' \
|
|
190
|
+
"refs/heads/feat $NEWSHA refs/heads/feat $BASESHA"
|
|
191
|
+
LOG="$R/tracks/_meta/.push_zone_override_log"
|
|
192
|
+
if [ -f "$LOG" ] && grep -qF 'owner:someone-else' "$LOG"; then
|
|
193
|
+
echo " ✅ Z5b override logged to tracks/_meta/.push_zone_override_log"; PASSED=$((PASSED+1))
|
|
194
|
+
else
|
|
195
|
+
echo " ❌ Z5b override NOT logged (wanted a line inside $LOG)"; FAILED=$((FAILED+1))
|
|
196
|
+
fi
|
|
197
|
+
rm -rf "$R"
|
|
198
|
+
|
|
199
|
+
# Z6 — existing destructive verdict (branch DELETE, unique paths → REVIEW → BLOCK) still stands
|
|
200
|
+
# AFTER a silent zone PASS. Same shape as the sibling suite's P2, through an owner-matching
|
|
201
|
+
# github.com URL — proves the zone block, inserted near the TOP of the hook, does not shadow
|
|
202
|
+
# or reorder the destructive checks further down.
|
|
203
|
+
R=$(newrepo); require_repo "$R" "Z6"
|
|
204
|
+
( cd "$R" && git checkout -q -b unique-work && printf 'only here\n' > unique.txt \
|
|
205
|
+
&& git add unique.txt && git commit -qm unique >/dev/null && git checkout -q main )
|
|
206
|
+
TIP=$(cd "$R" && git rev-parse unique-work)
|
|
207
|
+
check_zone "Z6 zone PASS, then BRANCH DELETE → BLOCK" "$R" "$OWNERS_OK" \
|
|
208
|
+
"https://github.com/known-owner/repo.git" 0 block 'REVIEW: .* unique path' \
|
|
209
|
+
"(delete) $ZERO refs/heads/unique-work $TIP"
|
|
210
|
+
rm -rf "$R"
|
|
211
|
+
|
|
212
|
+
# ── Residency: the owner-list's OTHER entries must never appear in any captured output ─────────
|
|
213
|
+
if printf '%s' "$ALL_OUT" | grep -qF 'acme-shared-org'; then
|
|
214
|
+
echo " ❌ RESIDENCY — the fixture owner-list's second entry leaked into hook output"
|
|
215
|
+
FAILED=$((FAILED+1))
|
|
216
|
+
else
|
|
217
|
+
echo " ✅ RESIDENCY — owner-list content never appears in any captured hook output"
|
|
218
|
+
PASSED=$((PASSED+1))
|
|
219
|
+
fi
|
|
220
|
+
|
|
221
|
+
echo ""
|
|
222
|
+
# ── Z7–Z14 (cross-family codex gpt-5.5, 2026-09-05): the first draft accepted three literal URL
|
|
223
|
+
# prefixes and read every other github.com shape as "unrecognized → allow" — four real shapes were
|
|
224
|
+
# failing OPEN; an inline comment in the owner list authorised pushes; CRLF / trailing slash blocked
|
|
225
|
+
# the operator's own remotes. Each lane below is one of those exact inputs.
|
|
226
|
+
OWNERS_CRLF="$WORK/owners-crlf.txt"; printf 'known-owner\r\nacme-shared-org\r\n' > "$OWNERS_CRLF"
|
|
227
|
+
OWNERS_COMMENT="$WORK/owners-comment.txt"; printf 'known-owner # someone-else is NOT allowed\nacme-shared-org\n' > "$OWNERS_COMMENT"
|
|
228
|
+
OWNERS_SLASH="$WORK/owners-slash.txt"; printf 'known-owner/\nacme-shared-org\n' > "$OWNERS_SLASH"
|
|
229
|
+
|
|
230
|
+
_zone_ff_lane() { # $1=id $2=desc $3=owners $4=url $5=expect $6=cause
|
|
231
|
+
local R BASESHA NEWSHA
|
|
232
|
+
R=$(newrepo); require_repo "$R" "$1"
|
|
233
|
+
BASESHA=$(cd "$R" && git rev-parse HEAD)
|
|
234
|
+
NEWSHA=$(cd "$R" && printf 'x\n' >> a.txt && git commit -qam ff >/dev/null && git rev-parse HEAD)
|
|
235
|
+
check_zone "$1 $2" "$R" "$3" "$4" 0 "$5" "$6" "refs/heads/feat $NEWSHA refs/heads/feat $BASESHA"
|
|
236
|
+
rm -rf "$R"
|
|
237
|
+
}
|
|
238
|
+
_zone_ff_lane Z7 "userinfo https (user@github.com), owner OUT → BLOCK" "$OWNERS_OK" "https://user@github.com/someone-else/repo.git" block 'FH Push-Zone Gate'
|
|
239
|
+
_zone_ff_lane Z8 "https with :443 port, owner OUT → BLOCK" "$OWNERS_OK" "https://github.com:443/someone-else/repo.git" block 'FH Push-Zone Gate'
|
|
240
|
+
_zone_ff_lane Z9 "ssh:// with :22 port, owner OUT → BLOCK" "$OWNERS_OK" "ssh://git@github.com:22/someone-else/repo.git" block 'FH Push-Zone Gate'
|
|
241
|
+
_zone_ff_lane Z10 "scp form with leading slash (git@github.com:/o/r), owner OUT → BLOCK" "$OWNERS_OK" "git@github.com:/someone-else/repo.git" block 'FH Push-Zone Gate'
|
|
242
|
+
_zone_ff_lane Z11 "inline comment in owner list must NOT authorise the commented name → BLOCK" "$OWNERS_COMMENT" "https://github.com/someone-else/repo.git" block 'FH Push-Zone Gate'
|
|
243
|
+
_zone_ff_lane Z12 "CRLF owner list, owner IN → pass (no over-block)" "$OWNERS_CRLF" "https://github.com/known-owner/repo.git" pass ''
|
|
244
|
+
_zone_ff_lane Z13 "trailing-slash owner entry, owner IN → pass" "$OWNERS_SLASH" "https://github.com/known-owner/repo.git" pass ''
|
|
245
|
+
_zone_ff_lane Z13b "uppercase host/owner in URL, owner IN → pass (case-insensitive)" "$OWNERS_OK" "https://GitHub.com/Known-Owner/repo.git" pass ''
|
|
246
|
+
# Z14 — $2 is a bare remote NAME (alias/insteadOf edge): the hook must resolve it, not read it as a host.
|
|
247
|
+
R=$(newrepo); require_repo "$R" "Z14"
|
|
248
|
+
( cd "$R" && git remote add origin https://github.com/someone-else/repo.git ) 2>/dev/null
|
|
249
|
+
BASESHA=$(cd "$R" && git rev-parse HEAD)
|
|
250
|
+
NEWSHA=$(cd "$R" && printf 'x\n' >> a.txt && git commit -qam ff >/dev/null && git rev-parse HEAD)
|
|
251
|
+
check_zone "Z14 \$2 is the remote NAME → resolved via git remote get-url → BLOCK" "$R" "$OWNERS_OK" \
|
|
252
|
+
"origin" 0 block 'FH Push-Zone Gate' "refs/heads/feat $NEWSHA refs/heads/feat $BASESHA"
|
|
253
|
+
rm -rf "$R"
|
|
254
|
+
|
|
255
|
+
echo "── session-close ①-f wiring (scripts/session_close_check.sh) ──"
|
|
256
|
+
if [ ! -f "$SCC_SRC" ]; then
|
|
257
|
+
echo "HARNESS_ERROR — session_close_check.sh not found at $SCC_SRC"; FAILED=$((FAILED+1))
|
|
258
|
+
else
|
|
259
|
+
SR="$(fh_fixture_root "$(mktemp -d)")" || exit 1
|
|
260
|
+
mkdir -p "$SR/tracks/_meta" "$SR/scripts" "$SR/.claude/rules"
|
|
261
|
+
cp "$REPO_ROOT/scripts/push_zone_check.sh" "$SR/scripts/push_zone_check.sh" 2>/dev/null
|
|
262
|
+
chmod +x "$SR/scripts/push_zone_check.sh" 2>/dev/null
|
|
263
|
+
( cd "$SR" && git init -q && git symbolic-ref HEAD refs/heads/main \
|
|
264
|
+
&& git config user.email t@example.com && git config user.name t \
|
|
265
|
+
&& git commit -q --allow-empty -m base >/dev/null ) >/dev/null 2>&1
|
|
266
|
+
|
|
267
|
+
if [ -x "$SR/scripts/push_zone_check.sh" ]; then
|
|
268
|
+
SC_OUT=$(PUSH_ZONE_OWNERS="$OWNERS_OK" bash "$SCC_SRC" "$SR" 2>&1)
|
|
269
|
+
if printf '%s' "$SC_OUT" | grep -qE '①-f push-zone: '; then
|
|
270
|
+
echo " ✅ ①-f prints advisory-prefixed push_zone_check.sh output when both files exist"
|
|
271
|
+
PASSED=$((PASSED+1))
|
|
272
|
+
else
|
|
273
|
+
echo " ❌ ①-f did not print the expected advisory prefix"; FAILED=$((FAILED+1))
|
|
274
|
+
fi
|
|
275
|
+
# RESIDENCY (codex #9, 2026-09-05): the enumerator prints owner-account names, and a close's
|
|
276
|
+
# output lands in transcripts and session cards. ①-f therefore prints COUNTS only — the decoy
|
|
277
|
+
# list entry must never appear, and the summary line must.
|
|
278
|
+
if printf '%s' "$SC_OUT" | grep -qF 'acme-shared-org'; then
|
|
279
|
+
echo " ❌ ①-f leaked an owner-account list entry into the close output"; FAILED=$((FAILED+1))
|
|
280
|
+
else
|
|
281
|
+
echo " ✅ ①-f withholds owner-account names (counts only)"; PASSED=$((PASSED+1))
|
|
282
|
+
fi
|
|
283
|
+
if printf '%s' "$SC_OUT" | grep -qE '①-f push-zone: [0-9]+ repo\(s\) enumerated'; then
|
|
284
|
+
echo " ✅ ①-f prints the enumeration summary (repo count · outside-list count)"; PASSED=$((PASSED+1))
|
|
285
|
+
else
|
|
286
|
+
echo " ❌ ①-f summary line missing"; FAILED=$((FAILED+1))
|
|
287
|
+
fi
|
|
288
|
+
else
|
|
289
|
+
echo " ❌ Z-SC setup — fixture push_zone_check.sh not executable"; FAILED=$((FAILED+1))
|
|
290
|
+
fi
|
|
291
|
+
|
|
292
|
+
# Owner-account list absent → UNCALIBRATED, one line, never silent.
|
|
293
|
+
SC_OUT2=$(PUSH_ZONE_OWNERS="$SR/no-such-owners-file" bash "$SCC_SRC" "$SR" 2>&1)
|
|
294
|
+
if printf '%s' "$SC_OUT2" | grep -qE '①-f push-zone UNCALIBRATED'; then
|
|
295
|
+
echo " ✅ ①-f UNCALIBRATED when the owner-account list is absent"; PASSED=$((PASSED+1))
|
|
296
|
+
else
|
|
297
|
+
echo " ❌ ①-f did not report UNCALIBRATED with no owner-account list"; FAILED=$((FAILED+1))
|
|
298
|
+
fi
|
|
299
|
+
rm -rf "$SR"
|
|
300
|
+
fi
|
|
301
|
+
|
|
302
|
+
echo ""
|
|
303
|
+
echo "[push-zone] $PASSED passed, $FAILED failed"
|
|
304
|
+
[ "$FAILED" -eq 0 ]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# residency_closure_scan.py 레인 테스트 — known-pair 교정.
|
|
3
|
+
#
|
|
4
|
+
# 이 파일이 있는 이유: 초판이 cross-family 적대검증(gpt-5.5, 2026-08-09)에서 **false-CLEAN
|
|
5
|
+
# 다수**로 반증됐다. 아래 L1~L3 은 그때 재현한 실제 우회 경로이고, 되돌리면 이 레인들이
|
|
6
|
+
# 적색이 된다. 「고쳤다」를 주장으로 두지 않고 실행으로 남긴다.
|
|
7
|
+
#
|
|
8
|
+
# 각 레인 = (입력, 기대 rc). rc 계약: 0=CLEAN(스크리닝) · 1=TAINTED · 10=HARNESS(fail-closed)
|
|
9
|
+
set -uo pipefail
|
|
10
|
+
|
|
11
|
+
HERE=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
12
|
+
SCAN="$HERE/residency_closure_scan.py"
|
|
13
|
+
TMP=$(mktemp -d) || exit 10
|
|
14
|
+
trap 'rm -rf "$TMP"' EXIT
|
|
15
|
+
|
|
16
|
+
PASS=0; FAIL=0
|
|
17
|
+
lane() { # lane <이름> <기대rc> <모듈> [패턴파일]
|
|
18
|
+
local name=$1 want=$2 mod=$3 pat=${4:-$TMP/pat}
|
|
19
|
+
RESIDENCY_PATTERNS="$pat" python3 "$SCAN" --repo "$TMP/repo" "$mod" >/dev/null 2>&1
|
|
20
|
+
local got=$?
|
|
21
|
+
if [ "$got" = "$want" ]; then PASS=$((PASS+1)); printf ' ✅ %-42s rc=%s\n' "$name" "$got"
|
|
22
|
+
else FAIL=$((FAIL+1)); printf ' ❌ %-42s rc=%s (기대 %s)\n' "$name" "$got" "$want"; fi
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
R="$TMP/repo"
|
|
26
|
+
mkdir -p "$R"/{src/pkg,myapp,app,clean,deep/sub}
|
|
27
|
+
: > "$R/src/pkg/__init__.py"; : > "$R/app/__init__.py"; : > "$R/deep/__init__.py"
|
|
28
|
+
: > "$R/deep/sub/__init__.py"
|
|
29
|
+
|
|
30
|
+
# L1 상대 임포트 — 초판이 `level == 0` 으로 통째로 버렸다
|
|
31
|
+
printf 'from .secrets import HOST\n' > "$R/src/pkg/main.py"
|
|
32
|
+
printf 'HOST = "api.acme.corp"\n' > "$R/src/pkg/secrets.py"
|
|
33
|
+
# L2 루트 화이트리스트 밖 레이아웃 — 초판은 ("src","scripts",…) 만 따라갔다
|
|
34
|
+
printf 'import myapp.config\n' > "$R/myapp/main.py"
|
|
35
|
+
printf 'HOST = "api.acme.corp"\n' > "$R/myapp/config.py"
|
|
36
|
+
# L3 `from pkg import sub` — 초판은 pkg/__init__.py 만 훑었다
|
|
37
|
+
printf 'from app import secrets\n' > "$R/app/main.py"
|
|
38
|
+
printf 'HOST = "api.acme.corp"\n' > "$R/app/secrets.py"
|
|
39
|
+
# L4 부모 패키지 __init__.py 오염 — payload 에 같이 나간다
|
|
40
|
+
printf 'HOST = "api.acme.corp"\n' > "$R/deep/__init__.py"
|
|
41
|
+
printf 'X = 1\n' > "$R/deep/sub/leaf.py"
|
|
42
|
+
# 과교정 컨트롤 — 진짜 깨끗한 모듈
|
|
43
|
+
printf 'X = 1\n' > "$R/clean/ok.py"
|
|
44
|
+
|
|
45
|
+
printf 'acme\n' > "$TMP/pat"
|
|
46
|
+
: > "$TMP/pat_empty"
|
|
47
|
+
printf '# 주석뿐\n' > "$TMP/pat_comment"
|
|
48
|
+
|
|
49
|
+
echo "== residency 폐포 스캔 레인 =="
|
|
50
|
+
lane "L1 상대 임포트 → TAINTED" 1 src.pkg.main
|
|
51
|
+
lane "L2 루트 화이트리스트 밖 → TAINTED" 1 myapp.main
|
|
52
|
+
lane "L3 from pkg import sub → TAINTED" 1 app.main
|
|
53
|
+
lane "L4 부모 __init__ 오염 → TAINTED" 1 deep.sub.leaf
|
|
54
|
+
lane "★컨트롤: 깨끗한 모듈 → CLEAN" 0 clean.ok
|
|
55
|
+
lane "빈 운영자 패턴 → HARNESS" 10 clean.ok "$TMP/pat_empty"
|
|
56
|
+
lane "주석뿐 운영자 패턴 → HARNESS" 10 clean.ok "$TMP/pat_comment"
|
|
57
|
+
lane "운영자 패턴 부재 → HARNESS" 10 clean.ok "$TMP/nonexistent"
|
|
58
|
+
lane "모듈 부재 → HARNESS" 10 no.such.module
|
|
59
|
+
|
|
60
|
+
# --files 모드: 실제 송신 목록만 잰다
|
|
61
|
+
RESIDENCY_PATTERNS="$TMP/pat" python3 "$SCAN" --repo "$R" --files src/pkg/secrets.py >/dev/null 2>&1
|
|
62
|
+
[ $? = 1 ] && { PASS=$((PASS+1)); echo " ✅ --files 오염 파일 → TAINTED rc=1"; } \
|
|
63
|
+
|| { FAIL=$((FAIL+1)); echo " ❌ --files 오염 파일"; }
|
|
64
|
+
RESIDENCY_PATTERNS="$TMP/pat" python3 "$SCAN" --repo "$R" --files clean/ok.py >/dev/null 2>&1
|
|
65
|
+
[ $? = 0 ] && { PASS=$((PASS+1)); echo " ✅ --files 깨끗한 파일 → CLEAN rc=0"; } \
|
|
66
|
+
|| { FAIL=$((FAIL+1)); echo " ❌ --files 깨끗한 파일"; }
|
|
67
|
+
|
|
68
|
+
echo "── PASS $PASS · FAIL $FAIL"
|
|
69
|
+
[ "$FAIL" -eq 0 ] || exit 1
|
|
70
|
+
exit 0
|
|
@@ -56,6 +56,10 @@ case "${FH_STUB_MODE:-say}" in
|
|
|
56
56
|
# 🟥 stdin 을 그대로 받아 적는다 — 실물 `claude -p` 가 하는 짓을 재현한다.
|
|
57
57
|
# 진짜 CLI 는 stdin 이 TTY 가 아니면 그것을 «읽어 프롬프트 뒤에 붙인다».
|
|
58
58
|
stdin) echo "stub answer"; cat >> "${FH_STUB_STDIN_LOG:-/dev/null}" ;;
|
|
59
|
+
# L26 (2026-09-05) — a command that runs long past any sane --timeout, default SIGTERM
|
|
60
|
+
# disposition (no trap): dies as soon as the watchdog signals it, so a lane can tell "the
|
|
61
|
+
# deadline was enforced" from "we just waited for it to finish on its own".
|
|
62
|
+
hang) sleep 30 ;;
|
|
59
63
|
esac
|
|
60
64
|
exit 0
|
|
61
65
|
STUB
|
|
@@ -278,5 +282,120 @@ else
|
|
|
278
282
|
no "L25 meta.tsv not written at all ($META)"
|
|
279
283
|
fi
|
|
280
284
|
|
|
285
|
+
# ── L26 timeout(1) resolution (2026-09-05) — the launchd-PATH incident this exists for ──────────
|
|
286
|
+
# WHY: launchd's PATH ($HOME/.local/bin:/usr/local/bin:/usr/bin:/bin, per this repo's own plist
|
|
287
|
+
# templates prior to this patch) has neither `timeout` nor `gtimeout` on it — both live under
|
|
288
|
+
# Homebrew's prefix. The first real launchd live-eval run (2026-09-05 02:30) hit exactly this:
|
|
289
|
+
# every rep of every arm died at `timeout: command not found` and the run scored 12/12
|
|
290
|
+
# FAILED-TO-RUN with no clue why. This lane shadows whichever CURRENT PATH dir(s) actually hold
|
|
291
|
+
# `timeout`/`gtimeout` (symlinks to everything else in them) — computed, not hardcoded to
|
|
292
|
+
# /opt/homebrew/bin, so the same lane works on Intel, Homebrew or a Linux /usr/bin — and proves
|
|
293
|
+
# two things a prose comment cannot: the bash-native fallback
|
|
294
|
+
# actually answers (not just "doesn't crash"), and it actually enforces the deadline rather than
|
|
295
|
+
# silently never firing.
|
|
296
|
+
_shadow_timeout_dirs() { # prints $PATH with every dir holding timeout/gtimeout replaced by a symlink shadow of it MINUS those two
|
|
297
|
+
# 🟥 Measured 2026-09-05 on CI (ubuntu): `timeout` lives in /usr/bin next to bash and git, so
|
|
298
|
+
# DROPPING that directory (the previous form of this fixture) also dropped bash/git, and the
|
|
299
|
+
# potency check below scored the lane red on every Linux box while macOS (Homebrew prefix,
|
|
300
|
+
# separate dir) stayed green. A shadow directory keeps every other name resolvable and removes
|
|
301
|
+
# only the two — the same fixture on both layouts.
|
|
302
|
+
local IFS=':' d out="" shadow n=0 f b
|
|
303
|
+
for d in $PATH; do
|
|
304
|
+
if [ -x "$d/timeout" ] || [ -x "$d/gtimeout" ]; then
|
|
305
|
+
n=$((n+1)); shadow="$WORKROOT/notimeout_shadow_$n"; rm -rf "$shadow"; mkdir -p "$shadow"
|
|
306
|
+
for f in "$d"/*; do
|
|
307
|
+
[ -x "$f" ] && [ ! -d "$f" ] || continue
|
|
308
|
+
b="${f##*/}"
|
|
309
|
+
case "$b" in timeout|gtimeout) continue ;; esac
|
|
310
|
+
ln -s "$f" "$shadow/$b"
|
|
311
|
+
done
|
|
312
|
+
d="$shadow"
|
|
313
|
+
fi
|
|
314
|
+
out="${out:+$out:}$d"
|
|
315
|
+
done
|
|
316
|
+
printf '%s' "$out"
|
|
317
|
+
}
|
|
318
|
+
NOTIMEOUT_PATH="$(_shadow_timeout_dirs)"
|
|
319
|
+
|
|
320
|
+
echo ""
|
|
321
|
+
echo "── L26 timeout(1) resolution ───────────────────────────────────────"
|
|
322
|
+
|
|
323
|
+
# Fixture-potency check FIRST — a stripped PATH that still resolves timeout/gtimeout proves
|
|
324
|
+
# nothing ([[feedback_fixture_must_use_the_breaking_spelling]]), and a strip that ALSO removed
|
|
325
|
+
# bash/git would confound every assertion below with an unrelated clone failure. Both are named
|
|
326
|
+
# distinctly rather than let a downstream lane fail for the wrong reason. Per this file's own
|
|
327
|
+
# convention (see L24's "검사 못 함(스킵 아님)"), an unusable fixture on this machine is scored
|
|
328
|
+
# as a failure, not silently skipped — it is loud precisely because CI does not gate this file
|
|
329
|
+
# (verified: no .github/workflows/*.yml references it), so a quiet skip would never be noticed.
|
|
330
|
+
if PATH="$NOTIMEOUT_PATH" command -v timeout >/dev/null 2>&1 || PATH="$NOTIMEOUT_PATH" command -v gtimeout >/dev/null 2>&1; then
|
|
331
|
+
no "L26-FIXTURE stripped PATH still resolves timeout/gtimeout — cannot run L26 on this machine"
|
|
332
|
+
elif ! PATH="$NOTIMEOUT_PATH" command -v bash >/dev/null 2>&1 || ! PATH="$NOTIMEOUT_PATH" command -v git >/dev/null 2>&1; then
|
|
333
|
+
no "L26-FIXTURE stripping timeout/gtimeout also removed bash or git — cannot run L26 on this machine"
|
|
334
|
+
else
|
|
335
|
+
ok "L26-FIXTURE stripped PATH lacks timeout+gtimeout; bash/git still resolve"
|
|
336
|
+
|
|
337
|
+
# L26a — no timeout/gtimeout at all: the runner must still answer (via the bash fallback) and
|
|
338
|
+
# must NAME which control it used in its own header, rather than leaving that to be inferred.
|
|
339
|
+
OUTDIR="$WORKROOT/o26a"
|
|
340
|
+
OUT=$( cd "$SRC" && PATH="$STUBBIN:$NOTIMEOUT_PATH" HOME="$FAKEHOME" FH_STUB_MODE=say \
|
|
341
|
+
bash "$SUT" --arm a --reps 1 --prompt p --out "$OUTDIR" 2>&1 )
|
|
342
|
+
if printf '%s' "$OUT" | grep -q "timeout_tool=bash-fallback" && printf '%s' "$OUT" | grep -q "captured"; then
|
|
343
|
+
ok "L26a no timeout/gtimeout on PATH -> bash-fallback answers and names itself in the header"
|
|
344
|
+
else
|
|
345
|
+
no "L26a bash-fallback did not fire/name itself: $(printf '%s' "$OUT" | grep -E 'timeout_tool|captured|UNMEASURED|EMPTY' | head -3 | tr '\n' ';')"
|
|
346
|
+
fi
|
|
347
|
+
|
|
348
|
+
# L26b — the fallback must actually ENFORCE the deadline, not just silently never fire. A stub
|
|
349
|
+
# that would otherwise run 30s under --timeout 2 must come back UNMEASURED well under 30s.
|
|
350
|
+
OUTDIR="$WORKROOT/o26b"
|
|
351
|
+
_t0=$(date +%s)
|
|
352
|
+
OUT=$( cd "$SRC" && PATH="$STUBBIN:$NOTIMEOUT_PATH" HOME="$FAKEHOME" FH_STUB_MODE=hang \
|
|
353
|
+
bash "$SUT" --arm a --reps 1 --prompt p --timeout 2 --out "$OUTDIR" 2>&1 )
|
|
354
|
+
_t1=$(date +%s); _elapsed=$(( _t1 - _t0 ))
|
|
355
|
+
if printf '%s' "$OUT" | grep -q "UNMEASURED" && [ "$_elapsed" -le 10 ]; then
|
|
356
|
+
ok "L26b bash-fallback enforces the deadline (UNMEASURED in ${_elapsed}s of a 30s hang, timeout=2s)"
|
|
357
|
+
else
|
|
358
|
+
no "L26b fallback did not enforce the timeout (elapsed=${_elapsed}s): $(printf '%s' "$OUT" | grep -E 'UNMEASURED|captured' | head -1)"
|
|
359
|
+
fi
|
|
360
|
+
|
|
361
|
+
# L26c control — SAME hang stub, DEFAULT (unstripped) PATH: real timeout/gtimeout must still be
|
|
362
|
+
# preferred over the fallback (header says gnu/gtimeout, not bash-fallback), AND must enforce
|
|
363
|
+
# the same deadline. Without this, L26a/L26b could be passing because the fallback is used
|
|
364
|
+
# unconditionally, not because resolution correctly prefers the real tool when it is present
|
|
365
|
+
# ([[feedback_control_presence_is_not_discrimination]]).
|
|
366
|
+
OUTDIR="$WORKROOT/o26c"
|
|
367
|
+
OUT=$( cd "$SRC" && PATH="$STUBBIN:$PATH" HOME="$FAKEHOME" FH_STUB_MODE=hang \
|
|
368
|
+
bash "$SUT" --arm a --reps 1 --prompt p --timeout 2 --out "$OUTDIR" 2>&1 )
|
|
369
|
+
case "$OUT" in
|
|
370
|
+
*"timeout_tool=bash-fallback"*)
|
|
371
|
+
no "L26c control — default PATH used bash-fallback instead of a real timeout/gtimeout" ;;
|
|
372
|
+
*"timeout_tool=gnu"*|*"timeout_tool=gtimeout"*)
|
|
373
|
+
printf '%s' "$OUT" | grep -q "UNMEASURED" \
|
|
374
|
+
&& ok "L26c control — default PATH prefers the real timeout tool and still enforces the deadline" \
|
|
375
|
+
|| no "L26c control — real timeout tool resolved but did not enforce the deadline"
|
|
376
|
+
;;
|
|
377
|
+
*) no "L26c control — no timeout_tool= line in header at all: $(printf '%s' "$OUT" | grep timeout_tool)" ;;
|
|
378
|
+
esac
|
|
379
|
+
fi
|
|
380
|
+
|
|
381
|
+
# ── L27 MCP isolation (2026-09-05) — the QP chamber floor sim saw the operator's user-scope MCP servers ─
|
|
382
|
+
# in a headless arm's tool list. The runner must pass --strict-mcp-config ALWAYS (no --mcp-config ⇒
|
|
383
|
+
# no servers), and pass through --mcp-config only when the caller names a file. Both directions.
|
|
384
|
+
OUTDIR="$WORKROOT/o27"; LOG="$WORKROOT/argv27.log"; : > "$LOG"
|
|
385
|
+
( cd "$SRC" && PATH="$STUBBIN:$PATH" HOME="$FAKEHOME" FH_STUB_MODE=say FH_STUB_ARGV_LOG="$LOG" \
|
|
386
|
+
bash "$SUT" --arm a --reps 1 --prompt p --out "$OUTDIR" ) >/dev/null 2>&1
|
|
387
|
+
grep -q -- "--strict-mcp-config" "$LOG" \
|
|
388
|
+
&& ok "L27a default passes --strict-mcp-config (user-scope MCP cannot reach a blind arm)" \
|
|
389
|
+
|| no "L27a default did NOT pass --strict-mcp-config — operator MCP servers leak into headless arms"
|
|
390
|
+
grep -q -- "--mcp-config" "$LOG" \
|
|
391
|
+
&& no "L27b default passed --mcp-config without a file (would point at nothing)" \
|
|
392
|
+
|| ok "L27b default passes no --mcp-config (strict + none = no servers)"
|
|
393
|
+
OUTDIR="$WORKROOT/o27c"; LOG2="$WORKROOT/argv27c.log"; : > "$LOG2"; printf '{"mcpServers":{}}\n' > "$WORKROOT/mcp27.json"
|
|
394
|
+
( cd "$SRC" && PATH="$STUBBIN:$PATH" HOME="$FAKEHOME" FH_STUB_MODE=say FH_STUB_ARGV_LOG="$LOG2" \
|
|
395
|
+
bash "$SUT" --arm a --reps 1 --prompt p --mcp-config "$WORKROOT/mcp27.json" --out "$OUTDIR" ) >/dev/null 2>&1
|
|
396
|
+
grep -q -- "--mcp-config $WORKROOT/mcp27.json" "$LOG2" && grep -q -- "--strict-mcp-config" "$LOG2" \
|
|
397
|
+
&& ok "L27c --mcp-config <file> reaches the CLI together with --strict-mcp-config (explicit allow, still strict)" \
|
|
398
|
+
|| no "L27c --mcp-config not plumbed (or strict dropped when a file is given)"
|
|
399
|
+
|
|
281
400
|
echo "sim_isolated_run lanes: $pass passed, $fail failed"
|
|
282
401
|
[ "$fail" -eq 0 ] || exit 1
|