planning-with-files 3.10.2 → 3.11.2
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/extensions/planning-with-files/__tests__/plan-anchor.test.ts +228 -228
- package/extensions/planning-with-files/package.json +17 -17
- package/extensions/planning-with-files/runtime.ts +788 -788
- package/package.json +1 -1
- package/scripts/attest-plan.ps1 +137 -137
- package/scripts/attest-plan.sh +206 -206
- package/scripts/check-complete.ps1 +253 -253
- package/scripts/check-complete.sh +253 -253
- package/scripts/gate-stop.sh +32 -32
- package/scripts/ledger-append.ps1 +180 -180
- package/scripts/ledger-append.sh +337 -337
- package/scripts/ledger-summary.ps1 +128 -128
- package/scripts/phase-status.ps1 +175 -175
- package/scripts/phase-status.sh +158 -158
- package/scripts/session-catchup.py +876 -876
- package/scripts/set-active-plan.ps1 +51 -51
- package/scripts/set-active-plan.sh +50 -50
package/scripts/attest-plan.sh
CHANGED
|
@@ -1,206 +1,206 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
# planning-with-files: lock the current task_plan.md content with a SHA-256 attestation.
|
|
3
|
-
#
|
|
4
|
-
# Use after you finalise (or intentionally edit) a plan. The hooks then refuse
|
|
5
|
-
# to inject plan content into the model context if the file diverges from the
|
|
6
|
-
# attested hash, surfacing a "[PLAN TAMPERED]" warning instead.
|
|
7
|
-
#
|
|
8
|
-
# Resolution:
|
|
9
|
-
# 1. $PLAN_ID env var → ./.planning/$PLAN_ID/
|
|
10
|
-
# 2. ./.planning/.active_plan
|
|
11
|
-
# 3. Newest ./.planning/<dir>/ by mtime
|
|
12
|
-
# 4. Legacy ./task_plan.md at project root
|
|
13
|
-
#
|
|
14
|
-
# Usage:
|
|
15
|
-
# sh scripts/attest-plan.sh # attest the active plan
|
|
16
|
-
# sh scripts/attest-plan.sh --show # print the stored hash
|
|
17
|
-
# sh scripts/attest-plan.sh --clear # remove the attestation (re-open the plan)
|
|
18
|
-
|
|
19
|
-
set -u
|
|
20
|
-
|
|
21
|
-
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
22
|
-
RESOLVER="${SCRIPT_DIR}/resolve-plan-dir.sh"
|
|
23
|
-
|
|
24
|
-
resolve_plan_file() {
|
|
25
|
-
plan_dir=""
|
|
26
|
-
if [ -f "${RESOLVER}" ]; then
|
|
27
|
-
plan_dir="$(sh "${RESOLVER}" 2>/dev/null)"
|
|
28
|
-
fi
|
|
29
|
-
if [ -n "${plan_dir}" ] && [ -f "${plan_dir}/task_plan.md" ]; then
|
|
30
|
-
printf "%s\n" "${plan_dir}/task_plan.md"
|
|
31
|
-
return 0
|
|
32
|
-
fi
|
|
33
|
-
if [ -f "./task_plan.md" ]; then
|
|
34
|
-
printf "%s\n" "./task_plan.md"
|
|
35
|
-
return 0
|
|
36
|
-
fi
|
|
37
|
-
return 1
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
attestation_path_for() {
|
|
41
|
-
plan_file="$1"
|
|
42
|
-
plan_dir="$(dirname "${plan_file}")"
|
|
43
|
-
if [ "${plan_dir}" = "." ]; then
|
|
44
|
-
# Legacy mode: store at project root.
|
|
45
|
-
printf "%s\n" "./.plan-attestation"
|
|
46
|
-
else
|
|
47
|
-
printf "%s\n" "${plan_dir}/.attestation"
|
|
48
|
-
fi
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
compute_hash() {
|
|
52
|
-
target="$1"
|
|
53
|
-
if command -v sha256sum >/dev/null 2>&1; then
|
|
54
|
-
sha256sum "${target}" | awk '{print $1}'
|
|
55
|
-
elif command -v shasum >/dev/null 2>&1; then
|
|
56
|
-
shasum -a 256 "${target}" | awk '{print $1}'
|
|
57
|
-
else
|
|
58
|
-
printf "ERROR: no sha256 utility available\n" >&2
|
|
59
|
-
return 1
|
|
60
|
-
fi
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
mode="attest"
|
|
64
|
-
case "${1:-}" in
|
|
65
|
-
--show) mode="show" ;;
|
|
66
|
-
--clear) mode="clear" ;;
|
|
67
|
-
"") mode="attest" ;;
|
|
68
|
-
*)
|
|
69
|
-
printf "Usage: %s [--show|--clear]\n" "$0" >&2
|
|
70
|
-
exit 2
|
|
71
|
-
;;
|
|
72
|
-
esac
|
|
73
|
-
|
|
74
|
-
plan_file="$(resolve_plan_file)" || {
|
|
75
|
-
printf "[plan-attest] No task_plan.md found. Create a plan first.\n" >&2
|
|
76
|
-
exit 1
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
attestation_file="$(attestation_path_for "${plan_file}")"
|
|
80
|
-
|
|
81
|
-
case "${mode}" in
|
|
82
|
-
show)
|
|
83
|
-
if [ -f "${attestation_file}" ]; then
|
|
84
|
-
printf "Plan: %s\n" "${plan_file}"
|
|
85
|
-
printf "Attestation: %s\n" "${attestation_file}"
|
|
86
|
-
printf "SHA-256: %s\n" "$(cat "${attestation_file}")"
|
|
87
|
-
# Nonce (security A1.4): if init-session generated a per-plan nonce
|
|
88
|
-
# next to the attestation, surface it. Informational only here; the
|
|
89
|
-
# hooks consume it to build collision-proof BEGIN/END delimiters.
|
|
90
|
-
nonce_file="$(dirname "${attestation_file}")/.nonce"
|
|
91
|
-
if [ -f "${nonce_file}" ]; then
|
|
92
|
-
printf "Nonce: %s\n" "$(tr -d '\r\n[:space:]' < "${nonce_file}" 2>/dev/null)"
|
|
93
|
-
fi
|
|
94
|
-
else
|
|
95
|
-
printf "[plan-attest] No attestation set for %s.\n" "${plan_file}"
|
|
96
|
-
exit 1
|
|
97
|
-
fi
|
|
98
|
-
;;
|
|
99
|
-
clear)
|
|
100
|
-
if [ -f "${attestation_file}" ]; then
|
|
101
|
-
rm -f "${attestation_file}"
|
|
102
|
-
printf "[plan-attest] Cleared attestation for %s.\n" "${plan_file}"
|
|
103
|
-
else
|
|
104
|
-
printf "[plan-attest] No attestation to clear.\n"
|
|
105
|
-
fi
|
|
106
|
-
;;
|
|
107
|
-
attest)
|
|
108
|
-
hash_val="$(compute_hash "${plan_file}")" || exit 1
|
|
109
|
-
|
|
110
|
-
# v2.40: protect the write with an advisory flock when available so
|
|
111
|
-
# concurrent legacy-mode sessions (no PLAN_ID, both at the same project
|
|
112
|
-
# root) cannot corrupt the .plan-attestation file mid-write. Atomic
|
|
113
|
-
# rename of a temp file is the real guarantee on POSIX; flock is the
|
|
114
|
-
# cooperative gate around the rename for slow-disk writes.
|
|
115
|
-
#
|
|
116
|
-
# Note: legacy single-file mode is inherently racey across concurrent
|
|
117
|
-
# sessions because both can edit task_plan.md without coordination. The
|
|
118
|
-
# canonical parallel-session pattern is slug-mode under
|
|
119
|
-
# .planning/<slug>/, where each session pins PLAN_ID and gets its own
|
|
120
|
-
# .attestation file. We surface a hint when concurrent activity is
|
|
121
|
-
# detected.
|
|
122
|
-
if [ -f "${attestation_file}" ]; then
|
|
123
|
-
mtime_now="$(date +%s 2>/dev/null || echo 0)"
|
|
124
|
-
mtime_prev="$(stat -c '%Y' "${attestation_file}" 2>/dev/null \
|
|
125
|
-
|| stat -f '%m' "${attestation_file}" 2>/dev/null \
|
|
126
|
-
|| echo 0)"
|
|
127
|
-
age=$((mtime_now - mtime_prev))
|
|
128
|
-
if [ "${age}" -ge 0 ] && [ "${age}" -lt 30 ] 2>/dev/null; then
|
|
129
|
-
# If we're in legacy mode (root .plan-attestation) and another
|
|
130
|
-
# session just wrote, warn. Slug-mode files in .planning/<slug>/
|
|
131
|
-
# are per-session by construction; no need to warn there.
|
|
132
|
-
case "${attestation_file}" in
|
|
133
|
-
*./.plan-attestation|*/.plan-attestation)
|
|
134
|
-
case "${attestation_file}" in
|
|
135
|
-
*./.planning/*) : ;; # slug-mode, ignore
|
|
136
|
-
*)
|
|
137
|
-
printf "[plan-attest] Note: %s was modified %ss ago by another process.\n" \
|
|
138
|
-
"${attestation_file}" "${age}" >&2
|
|
139
|
-
printf "[plan-attest] For parallel sessions, prefer slug-mode (init-session.sh <name>) so each session gets its own .attestation file.\n" >&2
|
|
140
|
-
;;
|
|
141
|
-
esac
|
|
142
|
-
;;
|
|
143
|
-
esac
|
|
144
|
-
fi
|
|
145
|
-
fi
|
|
146
|
-
|
|
147
|
-
tmp_file="${attestation_file}.tmp.$$"
|
|
148
|
-
printf "%s\n" "${hash_val}" > "${tmp_file}" 2>/dev/null || {
|
|
149
|
-
printf "[plan-attest] Failed to write %s\n" "${tmp_file}" >&2
|
|
150
|
-
exit 1
|
|
151
|
-
}
|
|
152
|
-
mv_ok=1
|
|
153
|
-
if command -v flock >/dev/null 2>&1; then
|
|
154
|
-
# Advisory lock around the rename. lock_dir is the dir containing
|
|
155
|
-
# the target file. The {} subshell pattern keeps the lock scoped to
|
|
156
|
-
# the mv call.
|
|
157
|
-
lock_dir="$(dirname "${attestation_file}")"
|
|
158
|
-
(
|
|
159
|
-
flock -w 5 9 || true
|
|
160
|
-
mv -f "${tmp_file}" "${attestation_file}"
|
|
161
|
-
) 9>"${lock_dir}/.attestation.lock" 2>/dev/null || mv_ok=0
|
|
162
|
-
rm -f "${lock_dir}/.attestation.lock" 2>/dev/null
|
|
163
|
-
else
|
|
164
|
-
mv -f "${tmp_file}" "${attestation_file}" 2>/dev/null || mv_ok=0
|
|
165
|
-
fi
|
|
166
|
-
|
|
167
|
-
# Integrity gap fix (security A2.1): a failed atomic rename must not be
|
|
168
|
-
# allowed to silently leave a stale attestation when the target already
|
|
169
|
-
# existed. The old fallback only wrote when the file was absent, so a
|
|
170
|
-
# cross-device or permission-denied mv on an existing attestation left
|
|
171
|
-
# the OLD hash in place with a success exit. On mv failure we re-write
|
|
172
|
-
# the intended hash through a second atomic rename (never a bare
|
|
173
|
-
# redirect onto the live file, which would expose torn reads to
|
|
174
|
-
# concurrent verifiers), then verify the on-disk content.
|
|
175
|
-
if [ "${mv_ok}" -eq 0 ] || [ ! -f "${attestation_file}" ]; then
|
|
176
|
-
fb_tmp="${attestation_file}.fb.$$"
|
|
177
|
-
printf "%s\n" "${hash_val}" > "${fb_tmp}" 2>/dev/null \
|
|
178
|
-
&& mv -f "${fb_tmp}" "${attestation_file}" 2>/dev/null || {
|
|
179
|
-
rm -f "${fb_tmp}" "${tmp_file}" 2>/dev/null
|
|
180
|
-
printf "[plan-attest] Failed to write attestation %s\n" "${attestation_file}" >&2
|
|
181
|
-
exit 1
|
|
182
|
-
}
|
|
183
|
-
fi
|
|
184
|
-
rm -f "${tmp_file}" 2>/dev/null
|
|
185
|
-
|
|
186
|
-
# Read-back verification. Both write paths above are atomic renames, so
|
|
187
|
-
# a concurrent verifier always reads a complete 64-hex hash — either our
|
|
188
|
-
# own or an identical one from a peer attesting the same plan content.
|
|
189
|
-
# A mismatch here therefore means our intended hash genuinely did not
|
|
190
|
-
# land (stale content, failed write); fail loudly with a nonzero exit so
|
|
191
|
-
# callers never trust a stale attestation.
|
|
192
|
-
stored_hash="$(tr -d '\r\n[:space:]' < "${attestation_file}" 2>/dev/null)"
|
|
193
|
-
if [ "${stored_hash}" != "${hash_val}" ]; then
|
|
194
|
-
printf "[plan-attest] Attestation write verification FAILED for %s\n" "${attestation_file}" >&2
|
|
195
|
-
printf "[plan-attest] Expected %s, found %s. The plan is NOT attested.\n" "${hash_val}" "${stored_hash}" >&2
|
|
196
|
-
exit 1
|
|
197
|
-
fi
|
|
198
|
-
|
|
199
|
-
short_hash="$(printf "%s" "${hash_val}" | cut -c1-12)"
|
|
200
|
-
printf "[plan-attest] Locked %s\n" "${plan_file}"
|
|
201
|
-
printf "[plan-attest] SHA-256: %s... (stored in %s)\n" "${short_hash}" "${attestation_file}"
|
|
202
|
-
printf "[plan-attest] Hooks will block injection if the file is modified without re-running this command.\n"
|
|
203
|
-
;;
|
|
204
|
-
esac
|
|
205
|
-
|
|
206
|
-
exit 0
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# planning-with-files: lock the current task_plan.md content with a SHA-256 attestation.
|
|
3
|
+
#
|
|
4
|
+
# Use after you finalise (or intentionally edit) a plan. The hooks then refuse
|
|
5
|
+
# to inject plan content into the model context if the file diverges from the
|
|
6
|
+
# attested hash, surfacing a "[PLAN TAMPERED]" warning instead.
|
|
7
|
+
#
|
|
8
|
+
# Resolution:
|
|
9
|
+
# 1. $PLAN_ID env var → ./.planning/$PLAN_ID/
|
|
10
|
+
# 2. ./.planning/.active_plan
|
|
11
|
+
# 3. Newest ./.planning/<dir>/ by mtime
|
|
12
|
+
# 4. Legacy ./task_plan.md at project root
|
|
13
|
+
#
|
|
14
|
+
# Usage:
|
|
15
|
+
# sh scripts/attest-plan.sh # attest the active plan
|
|
16
|
+
# sh scripts/attest-plan.sh --show # print the stored hash
|
|
17
|
+
# sh scripts/attest-plan.sh --clear # remove the attestation (re-open the plan)
|
|
18
|
+
|
|
19
|
+
set -u
|
|
20
|
+
|
|
21
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
22
|
+
RESOLVER="${SCRIPT_DIR}/resolve-plan-dir.sh"
|
|
23
|
+
|
|
24
|
+
resolve_plan_file() {
|
|
25
|
+
plan_dir=""
|
|
26
|
+
if [ -f "${RESOLVER}" ]; then
|
|
27
|
+
plan_dir="$(sh "${RESOLVER}" 2>/dev/null)"
|
|
28
|
+
fi
|
|
29
|
+
if [ -n "${plan_dir}" ] && [ -f "${plan_dir}/task_plan.md" ]; then
|
|
30
|
+
printf "%s\n" "${plan_dir}/task_plan.md"
|
|
31
|
+
return 0
|
|
32
|
+
fi
|
|
33
|
+
if [ -f "./task_plan.md" ]; then
|
|
34
|
+
printf "%s\n" "./task_plan.md"
|
|
35
|
+
return 0
|
|
36
|
+
fi
|
|
37
|
+
return 1
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
attestation_path_for() {
|
|
41
|
+
plan_file="$1"
|
|
42
|
+
plan_dir="$(dirname "${plan_file}")"
|
|
43
|
+
if [ "${plan_dir}" = "." ]; then
|
|
44
|
+
# Legacy mode: store at project root.
|
|
45
|
+
printf "%s\n" "./.plan-attestation"
|
|
46
|
+
else
|
|
47
|
+
printf "%s\n" "${plan_dir}/.attestation"
|
|
48
|
+
fi
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
compute_hash() {
|
|
52
|
+
target="$1"
|
|
53
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
54
|
+
sha256sum "${target}" | awk '{print $1}'
|
|
55
|
+
elif command -v shasum >/dev/null 2>&1; then
|
|
56
|
+
shasum -a 256 "${target}" | awk '{print $1}'
|
|
57
|
+
else
|
|
58
|
+
printf "ERROR: no sha256 utility available\n" >&2
|
|
59
|
+
return 1
|
|
60
|
+
fi
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
mode="attest"
|
|
64
|
+
case "${1:-}" in
|
|
65
|
+
--show) mode="show" ;;
|
|
66
|
+
--clear) mode="clear" ;;
|
|
67
|
+
"") mode="attest" ;;
|
|
68
|
+
*)
|
|
69
|
+
printf "Usage: %s [--show|--clear]\n" "$0" >&2
|
|
70
|
+
exit 2
|
|
71
|
+
;;
|
|
72
|
+
esac
|
|
73
|
+
|
|
74
|
+
plan_file="$(resolve_plan_file)" || {
|
|
75
|
+
printf "[plan-attest] No task_plan.md found. Create a plan first.\n" >&2
|
|
76
|
+
exit 1
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
attestation_file="$(attestation_path_for "${plan_file}")"
|
|
80
|
+
|
|
81
|
+
case "${mode}" in
|
|
82
|
+
show)
|
|
83
|
+
if [ -f "${attestation_file}" ]; then
|
|
84
|
+
printf "Plan: %s\n" "${plan_file}"
|
|
85
|
+
printf "Attestation: %s\n" "${attestation_file}"
|
|
86
|
+
printf "SHA-256: %s\n" "$(cat "${attestation_file}")"
|
|
87
|
+
# Nonce (security A1.4): if init-session generated a per-plan nonce
|
|
88
|
+
# next to the attestation, surface it. Informational only here; the
|
|
89
|
+
# hooks consume it to build collision-proof BEGIN/END delimiters.
|
|
90
|
+
nonce_file="$(dirname "${attestation_file}")/.nonce"
|
|
91
|
+
if [ -f "${nonce_file}" ]; then
|
|
92
|
+
printf "Nonce: %s\n" "$(tr -d '\r\n[:space:]' < "${nonce_file}" 2>/dev/null)"
|
|
93
|
+
fi
|
|
94
|
+
else
|
|
95
|
+
printf "[plan-attest] No attestation set for %s.\n" "${plan_file}"
|
|
96
|
+
exit 1
|
|
97
|
+
fi
|
|
98
|
+
;;
|
|
99
|
+
clear)
|
|
100
|
+
if [ -f "${attestation_file}" ]; then
|
|
101
|
+
rm -f "${attestation_file}"
|
|
102
|
+
printf "[plan-attest] Cleared attestation for %s.\n" "${plan_file}"
|
|
103
|
+
else
|
|
104
|
+
printf "[plan-attest] No attestation to clear.\n"
|
|
105
|
+
fi
|
|
106
|
+
;;
|
|
107
|
+
attest)
|
|
108
|
+
hash_val="$(compute_hash "${plan_file}")" || exit 1
|
|
109
|
+
|
|
110
|
+
# v2.40: protect the write with an advisory flock when available so
|
|
111
|
+
# concurrent legacy-mode sessions (no PLAN_ID, both at the same project
|
|
112
|
+
# root) cannot corrupt the .plan-attestation file mid-write. Atomic
|
|
113
|
+
# rename of a temp file is the real guarantee on POSIX; flock is the
|
|
114
|
+
# cooperative gate around the rename for slow-disk writes.
|
|
115
|
+
#
|
|
116
|
+
# Note: legacy single-file mode is inherently racey across concurrent
|
|
117
|
+
# sessions because both can edit task_plan.md without coordination. The
|
|
118
|
+
# canonical parallel-session pattern is slug-mode under
|
|
119
|
+
# .planning/<slug>/, where each session pins PLAN_ID and gets its own
|
|
120
|
+
# .attestation file. We surface a hint when concurrent activity is
|
|
121
|
+
# detected.
|
|
122
|
+
if [ -f "${attestation_file}" ]; then
|
|
123
|
+
mtime_now="$(date +%s 2>/dev/null || echo 0)"
|
|
124
|
+
mtime_prev="$(stat -c '%Y' "${attestation_file}" 2>/dev/null \
|
|
125
|
+
|| stat -f '%m' "${attestation_file}" 2>/dev/null \
|
|
126
|
+
|| echo 0)"
|
|
127
|
+
age=$((mtime_now - mtime_prev))
|
|
128
|
+
if [ "${age}" -ge 0 ] && [ "${age}" -lt 30 ] 2>/dev/null; then
|
|
129
|
+
# If we're in legacy mode (root .plan-attestation) and another
|
|
130
|
+
# session just wrote, warn. Slug-mode files in .planning/<slug>/
|
|
131
|
+
# are per-session by construction; no need to warn there.
|
|
132
|
+
case "${attestation_file}" in
|
|
133
|
+
*./.plan-attestation|*/.plan-attestation)
|
|
134
|
+
case "${attestation_file}" in
|
|
135
|
+
*./.planning/*) : ;; # slug-mode, ignore
|
|
136
|
+
*)
|
|
137
|
+
printf "[plan-attest] Note: %s was modified %ss ago by another process.\n" \
|
|
138
|
+
"${attestation_file}" "${age}" >&2
|
|
139
|
+
printf "[plan-attest] For parallel sessions, prefer slug-mode (init-session.sh <name>) so each session gets its own .attestation file.\n" >&2
|
|
140
|
+
;;
|
|
141
|
+
esac
|
|
142
|
+
;;
|
|
143
|
+
esac
|
|
144
|
+
fi
|
|
145
|
+
fi
|
|
146
|
+
|
|
147
|
+
tmp_file="${attestation_file}.tmp.$$"
|
|
148
|
+
printf "%s\n" "${hash_val}" > "${tmp_file}" 2>/dev/null || {
|
|
149
|
+
printf "[plan-attest] Failed to write %s\n" "${tmp_file}" >&2
|
|
150
|
+
exit 1
|
|
151
|
+
}
|
|
152
|
+
mv_ok=1
|
|
153
|
+
if command -v flock >/dev/null 2>&1; then
|
|
154
|
+
# Advisory lock around the rename. lock_dir is the dir containing
|
|
155
|
+
# the target file. The {} subshell pattern keeps the lock scoped to
|
|
156
|
+
# the mv call.
|
|
157
|
+
lock_dir="$(dirname "${attestation_file}")"
|
|
158
|
+
(
|
|
159
|
+
flock -w 5 9 || true
|
|
160
|
+
mv -f "${tmp_file}" "${attestation_file}"
|
|
161
|
+
) 9>"${lock_dir}/.attestation.lock" 2>/dev/null || mv_ok=0
|
|
162
|
+
rm -f "${lock_dir}/.attestation.lock" 2>/dev/null
|
|
163
|
+
else
|
|
164
|
+
mv -f "${tmp_file}" "${attestation_file}" 2>/dev/null || mv_ok=0
|
|
165
|
+
fi
|
|
166
|
+
|
|
167
|
+
# Integrity gap fix (security A2.1): a failed atomic rename must not be
|
|
168
|
+
# allowed to silently leave a stale attestation when the target already
|
|
169
|
+
# existed. The old fallback only wrote when the file was absent, so a
|
|
170
|
+
# cross-device or permission-denied mv on an existing attestation left
|
|
171
|
+
# the OLD hash in place with a success exit. On mv failure we re-write
|
|
172
|
+
# the intended hash through a second atomic rename (never a bare
|
|
173
|
+
# redirect onto the live file, which would expose torn reads to
|
|
174
|
+
# concurrent verifiers), then verify the on-disk content.
|
|
175
|
+
if [ "${mv_ok}" -eq 0 ] || [ ! -f "${attestation_file}" ]; then
|
|
176
|
+
fb_tmp="${attestation_file}.fb.$$"
|
|
177
|
+
printf "%s\n" "${hash_val}" > "${fb_tmp}" 2>/dev/null \
|
|
178
|
+
&& mv -f "${fb_tmp}" "${attestation_file}" 2>/dev/null || {
|
|
179
|
+
rm -f "${fb_tmp}" "${tmp_file}" 2>/dev/null
|
|
180
|
+
printf "[plan-attest] Failed to write attestation %s\n" "${attestation_file}" >&2
|
|
181
|
+
exit 1
|
|
182
|
+
}
|
|
183
|
+
fi
|
|
184
|
+
rm -f "${tmp_file}" 2>/dev/null
|
|
185
|
+
|
|
186
|
+
# Read-back verification. Both write paths above are atomic renames, so
|
|
187
|
+
# a concurrent verifier always reads a complete 64-hex hash — either our
|
|
188
|
+
# own or an identical one from a peer attesting the same plan content.
|
|
189
|
+
# A mismatch here therefore means our intended hash genuinely did not
|
|
190
|
+
# land (stale content, failed write); fail loudly with a nonzero exit so
|
|
191
|
+
# callers never trust a stale attestation.
|
|
192
|
+
stored_hash="$(tr -d '\r\n[:space:]' < "${attestation_file}" 2>/dev/null)"
|
|
193
|
+
if [ "${stored_hash}" != "${hash_val}" ]; then
|
|
194
|
+
printf "[plan-attest] Attestation write verification FAILED for %s\n" "${attestation_file}" >&2
|
|
195
|
+
printf "[plan-attest] Expected %s, found %s. The plan is NOT attested.\n" "${hash_val}" "${stored_hash}" >&2
|
|
196
|
+
exit 1
|
|
197
|
+
fi
|
|
198
|
+
|
|
199
|
+
short_hash="$(printf "%s" "${hash_val}" | cut -c1-12)"
|
|
200
|
+
printf "[plan-attest] Locked %s\n" "${plan_file}"
|
|
201
|
+
printf "[plan-attest] SHA-256: %s... (stored in %s)\n" "${short_hash}" "${attestation_file}"
|
|
202
|
+
printf "[plan-attest] Hooks will block injection if the file is modified without re-running this command.\n"
|
|
203
|
+
;;
|
|
204
|
+
esac
|
|
205
|
+
|
|
206
|
+
exit 0
|