@manhquy/flow-skill 0.4.1 → 0.5.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.
@@ -0,0 +1,1365 @@
1
+ # attestations.sh — risk, receipts, fingerprints, auto policy (sourced by flow.sh)
2
+ # Namespace: _att_* helpers + cmd_attest. No side effects on source.
3
+ # Bash 3.2 + Git Bash portable. No jq/Python required for gate path.
4
+
5
+ # ---------- constants ----------
6
+ # Globals touched across helpers (set -u safe defaults)
7
+ _ATT_LAST_FP=""
8
+ _ATT_SUBJ_BASE=""; _ATT_SUBJ_REV=""; _ATT_SUBJ_TREE=""
9
+ _ATT_R_subject_fingerprint=""; _ATT_R_verdict=""
10
+ _ATT_FIELD_ERR=""
11
+ _ATT_SCHEMA="flow-attestation/v1"
12
+ _ATT_OWNER_SCHEMA="flow-attestation-owner/v1"
13
+ _ATT_SEM_RESULT_SCHEMA="flow-semantic-result/v1"
14
+ _ATT_AUTO_SCHEMA="flow-auto/v1"
15
+ _ATT_STAGES="00-idea 01-research 02-scope 03-prd 04-adr 05-contract"
16
+ # Defaults only; _att_run_supervised re-reads FLOW_ATTEST_* at call time.
17
+
18
+ # ---------- small utils ----------
19
+ _att_trim() { printf '%s' "${1:-}" | tr -d '\r' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//'; }
20
+ _att_lc() { printf '%s' "${1:-}" | tr 'A-Z' 'a-z'; }
21
+ _att_now_rfc3339() { date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo "1970-01-01T00:00:00Z"; }
22
+ _att_now_s() { date +%s 2>/dev/null || echo 0; }
23
+
24
+ _att_sanitize_label() { # $1 raw -> sanitized actor/engine or empty
25
+ local s; s="$(printf '%s' "${1:-}" | tr -d '\r\n' | cut -c1-128)"
26
+ case "$s" in
27
+ [A-Za-z0-9][A-Za-z0-9._@/-]*) printf '%s' "$s" ;;
28
+ *) printf '' ;;
29
+ esac
30
+ }
31
+
32
+ _att_is_stage() {
33
+ case " $_ATT_STAGES " in *" $1 "*) return 0 ;; esac
34
+ return 1
35
+ }
36
+
37
+ _att_norm_card_id() { # $1 -> C-NNN or empty
38
+ local arg="$1" num
39
+ num="${arg#C-}"; num="${num#c-}"
40
+ case "$num" in (*[!0-9]*) echo ""; return 1 ;; esac
41
+ num=$((10#$num))
42
+ printf 'C-%03d' "$num"
43
+ }
44
+
45
+ _att_valid_subject_id() {
46
+ local id="$1"
47
+ _att_is_stage "$id" && return 0
48
+ case "$id" in C-[0-9][0-9][0-9]) return 0 ;; esac
49
+ return 1
50
+ }
51
+
52
+ _att_valid_repo_ref() { # repo:relpath — no abs, no ..
53
+ local r="$1" p
54
+ case "$r" in
55
+ none) return 0 ;;
56
+ repo:*)
57
+ p="${r#repo:}"
58
+ case "$p" in
59
+ ""|/*|*..*|*"//"*|*\\*) return 1 ;;
60
+ esac
61
+ # reject empty segments
62
+ case "/$p/" in *"//"*|*"/./*"|*"/../"*) return 1 ;; esac
63
+ [ "${#p}" -le 240 ] || return 1
64
+ return 0
65
+ ;;
66
+ *) return 1 ;;
67
+ esac
68
+ }
69
+
70
+ _att_valid_target_id() {
71
+ case "$1" in
72
+ none) return 0 ;;
73
+ [a-z0-9][a-z0-9._-]*) [ "${#1}" -le 64 ] && return 0 ;;
74
+ esac
75
+ return 1
76
+ }
77
+
78
+ _att_valid_actor() {
79
+ case "$1" in [A-Za-z0-9][A-Za-z0-9._@/-]*) [ "${#1}" -le 128 ] && return 0 ;; esac
80
+ return 1
81
+ }
82
+
83
+ # ---------- git / state root ----------
84
+ _att_git_ok() { command -v git >/dev/null 2>&1 && git -C "$ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1; }
85
+
86
+ _att_object_format() {
87
+ local f
88
+ f="$(git -C "$ROOT" rev-parse --show-object-format=storage 2>/dev/null || echo sha1)"
89
+ case "$f" in sha1|sha256) printf '%s' "$f" ;; *) printf 'sha1' ;; esac
90
+ }
91
+
92
+ _att_oid_re() {
93
+ case "$(_att_object_format)" in
94
+ sha256) printf '^[0-9a-f]{64}$' ;;
95
+ *) printf '^[0-9a-f]{40}$' ;;
96
+ esac
97
+ }
98
+
99
+ _att_full_commit() { # $1 rev -> full oid or empty
100
+ git -C "$ROOT" rev-parse --verify "${1}^{commit}" 2>/dev/null | tr -d '\r\n'
101
+ }
102
+
103
+ _att_is_ancestor() { # $1 possible-ancestor $2 descendant
104
+ git -C "$ROOT" merge-base --is-ancestor "$1" "$2" 2>/dev/null
105
+ }
106
+
107
+ _att_strip_git_env() {
108
+ unset GIT_DIR GIT_WORK_TREE GIT_COMMON_DIR GIT_OBJECT_DIRECTORY GIT_INDEX_FILE 2>/dev/null || true
109
+ }
110
+
111
+ # Main state root: linked worktree -> main equiv; submodule/separate-git-dir stay local.
112
+ _att_main_state_root() {
113
+ local root="$ROOT" git_dir common_dir worktree_top main_top rr wtop rel
114
+ if ! _att_git_ok; then printf '%s' "$root"; return 0; fi
115
+ _att_strip_git_env
116
+ # Prefer path-format=absolute (git ≥2.31); fall back.
117
+ local out
118
+ out="$(git -C "$root" rev-parse --path-format=absolute --git-dir --git-common-dir --show-toplevel 2>/dev/null)" || out=""
119
+ if [ -n "$out" ]; then
120
+ git_dir="$(printf '%s\n' "$out" | sed -n '1p')"
121
+ common_dir="$(printf '%s\n' "$out" | sed -n '2p')"
122
+ worktree_top="$(printf '%s\n' "$out" | sed -n '3p')"
123
+ else
124
+ git_dir="$(git -C "$root" rev-parse --absolute-git-dir 2>/dev/null || true)"
125
+ common_dir="$(git -C "$root" rev-parse --git-common-dir 2>/dev/null || true)"
126
+ worktree_top="$(git -C "$root" rev-parse --show-toplevel 2>/dev/null || true)"
127
+ case "$common_dir" in /*) : ;; "") : ;; *) common_dir="$(cd "$root" && cd "$common_dir" 2>/dev/null && pwd)" ;; esac
128
+ fi
129
+ if [ -z "$git_dir" ] || [ -z "$common_dir" ] || [ -z "$worktree_top" ]; then
130
+ printf '%s' "$root"; return 0
131
+ fi
132
+ # realpath-ish: prefer readlink -f / realpath; else leave
133
+ _rp() { (cd "$1" 2>/dev/null && pwd -P) || printf '%s' "$1"; }
134
+ git_dir="$(_rp "$git_dir")"
135
+ common_dir="$(_rp "$common_dir")"
136
+ worktree_top="$(_rp "$worktree_top")"
137
+ rr="$(_rp "$root")"
138
+ if [ "$git_dir" = "$common_dir" ]; then
139
+ # main, submodule, or separate-git-dir — do not translate
140
+ printf '%s' "$rr"; return 0
141
+ fi
142
+ main_top="$(git -C "$root" worktree list --porcelain 2>/dev/null | awk '/^worktree /{print substr($0,10); exit}')"
143
+ main_top="$(printf '%s' "$main_top" | tr -d '\r')"
144
+ if [ -z "$main_top" ] || [ ! -e "$main_top/.git" ]; then
145
+ printf '%s' "$rr"; return 0
146
+ fi
147
+ main_top="$(_rp "$main_top")"
148
+ wtop="$worktree_top"
149
+ case "$rr" in
150
+ "$wtop"|"$wtop"/*)
151
+ rel="${rr#"$wtop"}"
152
+ rel="${rel#/}"
153
+ if [ -z "$rel" ]; then printf '%s' "$main_top"; else printf '%s/%s' "$main_top" "$rel"; fi
154
+ return 0
155
+ ;;
156
+ esac
157
+ printf '%s' "$rr"
158
+ }
159
+
160
+ _att_dir() {
161
+ local base; base="$(_att_main_state_root)"
162
+ printf '%s/.flow/attestations' "$base"
163
+ }
164
+
165
+ _att_auto_state_path() {
166
+ printf '%s/.flow/auto-state' "$(_att_main_state_root)"
167
+ }
168
+
169
+ _att_receipt_path() { # $1 kind $2 subject_type $3 subject_id
170
+ local kind="$1" st="$2" sid="$3" name
171
+ case "$st" in
172
+ stage) name="stage-${sid}.receipt" ;;
173
+ card) name="card-${sid}.receipt" ;;
174
+ *) return 1 ;;
175
+ esac
176
+ printf '%s/%s/%s' "$(_att_dir)" "$kind" "$name"
177
+ }
178
+
179
+ _att_attempt_path() { # card id
180
+ printf '%s/live_verify/card-%s.attempt' "$(_att_dir)" "$1"
181
+ }
182
+
183
+ # ---------- locks ----------
184
+ _att_lock_dir() {
185
+ printf '%s/.flow/locks' "$(_att_main_state_root)"
186
+ }
187
+
188
+ _att_lock_acquire() { # $1 name (mkdir atomic)
189
+ local d name="$1" i=0 opid
190
+ d="$(_att_lock_dir)"
191
+ mkdir -p "$d" 2>/dev/null || true
192
+ while ! mkdir "$d/$name.lock.d" 2>/dev/null; do
193
+ i=$((i + 1))
194
+ opid="$(cat "$d/$name.lock.d/pid" 2>/dev/null || true)"
195
+ if [ -n "$opid" ] && ! kill -0 "$opid" 2>/dev/null; then
196
+ rm -rf "$d/$name.lock.d" 2>/dev/null || true
197
+ continue
198
+ fi
199
+ [ "$i" -gt 100 ] && { echo "FAIL: could not acquire attestation lock '$name'" >&2; return 1; }
200
+ sleep 0.05 2>/dev/null || sleep 1
201
+ done
202
+ printf '%s\n' "$$" > "$d/$name.lock.d/pid" 2>/dev/null || true
203
+ return 0
204
+ }
205
+
206
+ _att_lock_release() {
207
+ local d name="$1"
208
+ d="$(_att_lock_dir)"
209
+ rm -rf "$d/$name.lock.d" 2>/dev/null || true
210
+ }
211
+
212
+ _att_auto_lock_acquire() { _att_lock_acquire "auto-policy"; }
213
+ _att_auto_lock_release() { _att_lock_release "auto-policy"; }
214
+
215
+ _att_subject_lock_name() { # kind subject_id
216
+ printf 'subj-%s-%s' "$2" "$1"
217
+ }
218
+
219
+ # ---------- cleanliness ----------
220
+ # Returns 0 if clean enough for attestation (ignore .flow/ only via pathspec magic).
221
+ _att_git_clean_for_attest() {
222
+ local porcelain
223
+ porcelain="$(git -C "$ROOT" status --porcelain=v1 --untracked-files=all 2>/dev/null || echo FAIL)"
224
+ [ "$porcelain" = "FAIL" ] && return 1
225
+ [ -z "$porcelain" ] && return 0
226
+ # Allow only changes under .flow/
227
+ local line path
228
+ while IFS= read -r line; do
229
+ [ -z "$line" ] && continue
230
+ # format: XY PATH or XY ORIG -> PATH
231
+ path="${line#?? }"
232
+ case "$path" in
233
+ *" -> "*) path="${path##* -> }" ;;
234
+ esac
235
+ path="${path#\"}"; path="${path%\"}"
236
+ case "$path" in
237
+ .flow|.flow/*) continue ;;
238
+ *) return 1 ;;
239
+ esac
240
+ done <<EOF
241
+ $porcelain
242
+ EOF
243
+ return 0
244
+ }
245
+
246
+ # ---------- card field parsing ----------
247
+ _att_field_count() { # $1 file $2 field
248
+ grep -cE "^${2}:" "$1" 2>/dev/null | tr -d '\r' || echo 0
249
+ }
250
+
251
+ _att_field_once() { # $1 file $2 field -> value or empty; sets _ATT_FIELD_ERR on dupe
252
+ _ATT_FIELD_ERR=""
253
+ local c v
254
+ c="$(_att_field_count "$1" "$2")"
255
+ case "$c" in
256
+ 0) printf ''; return 0 ;;
257
+ 1)
258
+ v="$(grep -m1 -E "^${2}:" "$1" 2>/dev/null | sed "s/^${2}:[[:space:]]*//" | tr -d '\r')"
259
+ printf '%s' "$v"; return 0
260
+ ;;
261
+ *) _ATT_FIELD_ERR="duplicate"; printf ''; return 1 ;;
262
+ esac
263
+ }
264
+
265
+ card_risk() {
266
+ local v
267
+ v="$(_att_field_once "$1" risk)" || { printf 'invalid'; return 1; }
268
+ [ -z "$v" ] && { printf 'unknown'; return 0; }
269
+ printf '%s' "$v"
270
+ }
271
+
272
+ card_risk_reason() {
273
+ _att_field_once "$1" risk-reason || true
274
+ }
275
+
276
+ card_risk_ack() {
277
+ local v
278
+ v="$(_att_field_once "$1" risk-ack)" || { printf 'invalid'; return 1; }
279
+ [ -z "$v" ] && { printf 'none'; return 0; }
280
+ printf '%s' "$v"
281
+ }
282
+
283
+ _att_reason_placeholder() {
284
+ case "$(_att_lc "$1")" in
285
+ ""|"[fill]"*|*"[fill]"*|"tbd"|"todo"|"n/a"|"none"|"-"|"—"|"x") return 0 ;;
286
+ esac
287
+ [ "${#1}" -lt 1 ] && return 0
288
+ [ "${#1}" -gt 256 ] && return 0
289
+ case "$1" in *$'\n'*|*$'\r'*) return 0 ;; esac
290
+ return 1
291
+ }
292
+
293
+ # mode: manual|auto -> prints normalized risk or "invalid:..." ; exit 0 ok, 1 structural
294
+ _card_risk_validate() {
295
+ local file="$1" mode="${2:-manual}" risk reason ack
296
+ if [ ! -f "$file" ]; then echo "invalid:missing-file"; return 1; fi
297
+ risk="$(card_risk "$file")" || { echo "invalid:duplicate-risk"; return 1; }
298
+ reason="$(card_risk_reason "$file" 2>/dev/null || true)"
299
+ if [ "${_ATT_FIELD_ERR:-}" = "duplicate" ]; then echo "invalid:duplicate-risk-reason"; return 1; fi
300
+ # re-count reason for duplicates
301
+ if [ "$(_att_field_count "$file" risk-reason)" -gt 1 ]; then echo "invalid:duplicate-risk-reason"; return 1; fi
302
+ ack="$(card_risk_ack "$file")" || { echo "invalid:duplicate-risk-ack"; return 1; }
303
+ if [ "$(_att_field_count "$file" risk-ack)" -gt 1 ]; then echo "invalid:duplicate-risk-ack"; return 1; fi
304
+
305
+ case "$risk" in
306
+ standard|security-class|unknown) : ;;
307
+ invalid) echo "invalid:duplicate-risk"; return 1 ;;
308
+ *) echo "invalid:risk-value"; return 1 ;;
309
+ esac
310
+ case "$ack" in
311
+ none|git:[0-9a-f]*) : ;;
312
+ invalid) echo "invalid:duplicate-risk-ack"; return 1 ;;
313
+ *)
314
+ if [ -n "$(_att_field_once "$file" risk-ack 2>/dev/null)" ]; then
315
+ case "$ack" in git:*) echo "invalid:risk-ack-shape"; return 1 ;; *) echo "invalid:risk-ack-shape"; return 1 ;; esac
316
+ fi
317
+ ;;
318
+ esac
319
+
320
+ if [ "$risk" = "unknown" ]; then
321
+ [ "$ack" = "none" ] || { echo "invalid:unknown-with-ack"; return 1; }
322
+ echo "unknown"; return 0
323
+ fi
324
+ if _att_reason_placeholder "$reason"; then echo "invalid:risk-reason"; return 1; fi
325
+ if [ "$risk" = "standard" ]; then
326
+ [ "$ack" = "none" ] || { echo "invalid:standard-with-ack"; return 1; }
327
+ echo "standard"; return 0
328
+ fi
329
+ # security-class
330
+ if [ "$mode" = "manual" ]; then
331
+ echo "security-class"; return 0
332
+ fi
333
+ # auto: full ack
334
+ if [ "$ack" = "none" ] || [ -z "$ack" ]; then echo "halt:ack-missing"; return 1; fi
335
+ case "$ack" in git:*) : ;; *) echo "halt:ack-shape"; return 1 ;; esac
336
+ local oid; oid="${ack#git:}"
337
+ if ! printf '%s' "$oid" | grep -Eq "$(_att_oid_re)"; then echo "halt:ack-oid"; return 1; fi
338
+ if ! _att_git_ok; then echo "halt:no-git"; return 1; fi
339
+ if ! git -C "$ROOT" cat-file -e "${oid}^{commit}" 2>/dev/null; then echo "halt:ack-missing-commit"; return 1; fi
340
+ local head; head="$(_att_full_commit HEAD)"
341
+ if ! _att_is_ancestor "$oid" "$head"; then echo "halt:ack-not-ancestor"; return 1; fi
342
+ local id; id="$(basename "$file" .md)"
343
+ local prefix debt_blob line_no line author_mail exec_mail
344
+ prefix="$(git -C "$ROOT" rev-parse --show-prefix 2>/dev/null || true)"
345
+ debt_blob="$(git -C "$ROOT" show "${oid}:${prefix}DEBT.md" 2>/dev/null)" || { echo "halt:ack-no-debt"; return 1; }
346
+ # exact open grammar
347
+ line="$(printf '%s\n' "$debt_blob" | grep -nE "^- \[ \] DEBT: security-class ${id} --" || true)"
348
+ if [ -z "$line" ]; then echo "halt:ack-debt-line"; return 1; fi
349
+ if [ "$(printf '%s\n' "$line" | wc -l | tr -d ' ')" -ne 1 ]; then echo "halt:ack-debt-multi"; return 1; fi
350
+ line_no="${line%%:*}"
351
+ author_mail="$(git -C "$ROOT" blame --line-porcelain "$oid" -L "${line_no},${line_no}" -- "${prefix}DEBT.md" 2>/dev/null \
352
+ | awk '/^author-mail /{print $2; exit}' | tr -d '<>' | tr 'A-Z' 'a-z' | tr -d '\r')"
353
+ exec_mail="$(git -C "$ROOT" var GIT_AUTHOR_IDENT 2>/dev/null | sed -nE 's/.*<([^>]+)>.*/\1/p' | tr 'A-Z' 'a-z' | tr -d '\r')"
354
+ if [ -z "$author_mail" ] || [ -z "$exec_mail" ]; then echo "halt:ack-identity-ambiguous"; return 1; fi
355
+ if [ "$author_mail" = "$exec_mail" ]; then echo "halt:ack-same-identity"; return 1; fi
356
+ echo "security-class"; return 0
357
+ }
358
+
359
+ _att_all_card_files() {
360
+ ls -1 "$CARDS_DIR"/C-*.md 2>/dev/null | sort
361
+ }
362
+
363
+ _att_risk_fingerprint() {
364
+ local f id risk reason ack body=""
365
+ body=""
366
+ for f in $(_att_all_card_files); do
367
+ [ -f "$f" ] || continue
368
+ id="$(basename "$f" .md)"
369
+ risk="$(card_risk "$f" 2>/dev/null || echo invalid)"
370
+ reason="$(_att_field_once "$f" risk-reason 2>/dev/null || true)"
371
+ ack="$(card_risk_ack "$f" 2>/dev/null || echo invalid)"
372
+ body="${body}${id}|${risk}|${reason}|${ack}"$'\n'
373
+ done
374
+ local fmt oid
375
+ fmt="$(_att_object_format)"
376
+ oid="$(printf '%s' "$body" | git -C "$ROOT" hash-object --stdin 2>/dev/null | tr -d '\r\n')"
377
+ [ -n "$oid" ] || oid="0"
378
+ printf 'git-%s:%s' "$fmt" "$oid"
379
+ }
380
+
381
+ # ---------- owner manifest ----------
382
+ _att_parse_kv_file() { # $1 path — sets _ATT_KV_<key> via eval-safe names only for known keys
383
+ # shellcheck disable=SC2034
384
+ _ATT_KV_schema=""; _ATT_KV_kind=""; _ATT_KV_subject_id=""; _ATT_KV_target_id=""
385
+ _ATT_KV_command=""; _ATT_KV_revision_oracle=""
386
+ local line k v seen=""
387
+ [ -f "$1" ] || return 1
388
+ while IFS= read -r line || [ -n "$line" ]; do
389
+ line="$(printf '%s' "$line" | tr -d '\r')"
390
+ [ -z "$line" ] && continue
391
+ case "$line" in \#*) continue ;; esac
392
+ case "$line" in
393
+ *": "*) k="${line%%: *}"; v="${line#*: }" ;;
394
+ *:*) k="${line%%:*}"; v="${line#*:}"; v="${v# }" ;;
395
+ *) return 1 ;;
396
+ esac
397
+ case "$k" in
398
+ schema|kind|subject_id|target_id|command|revision_oracle) : ;;
399
+ *) return 1 ;;
400
+ esac
401
+ case " $seen " in *" $k "*) return 1 ;; esac
402
+ seen="$seen $k"
403
+ case "$k" in
404
+ schema) _ATT_KV_schema="$v" ;;
405
+ kind) _ATT_KV_kind="$v" ;;
406
+ subject_id) _ATT_KV_subject_id="$v" ;;
407
+ target_id) _ATT_KV_target_id="$v" ;;
408
+ command) _ATT_KV_command="$v" ;;
409
+ revision_oracle) _ATT_KV_revision_oracle="$v" ;;
410
+ esac
411
+ done < "$1"
412
+ [ "$_ATT_KV_schema" = "$_ATT_OWNER_SCHEMA" ] || return 1
413
+ return 0
414
+ }
415
+
416
+ _att_owner_fingerprint() { # $1 owner path relative, $2 revision
417
+ local rel="$1" rev="$2" cmd_path oracle_path mode blob o_mode o_blob body fmt oid
418
+ cmd_path="${_ATT_KV_command#repo:}"
419
+ blob="$(git -C "$ROOT" rev-parse "${rev}:${cmd_path}" 2>/dev/null | tr -d '\r\n')" || return 1
420
+ mode="$(git -C "$ROOT" ls-tree "$rev" -- "$cmd_path" 2>/dev/null | awk '{print $1}' | head -1)"
421
+ body="owner=${rel}"$'\n'"cmd=${cmd_path}:${mode}:${blob}"$'\n'
422
+ if [ "${_ATT_KV_revision_oracle}" != "none" ] && [ -n "${_ATT_KV_revision_oracle}" ]; then
423
+ oracle_path="${_ATT_KV_revision_oracle#repo:}"
424
+ o_blob="$(git -C "$ROOT" rev-parse "${rev}:${oracle_path}" 2>/dev/null | tr -d '\r\n')" || return 1
425
+ o_mode="$(git -C "$ROOT" ls-tree "$rev" -- "$oracle_path" 2>/dev/null | awk '{print $1}' | head -1)"
426
+ body="${body}oracle=${oracle_path}:${o_mode}:${o_blob}"$'\n'
427
+ fi
428
+ body="${body}target=${_ATT_KV_target_id}"$'\n'"kind=${_ATT_KV_kind}"$'\n'
429
+ fmt="$(_att_object_format)"
430
+ oid="$(printf '%s' "$body" | git -C "$ROOT" hash-object --stdin 2>/dev/null | tr -d '\r\n')"
431
+ printf 'git-%s:%s' "$fmt" "$oid"
432
+ }
433
+
434
+ # ---------- projections / fingerprints ----------
435
+ _att_card_contract_projection() { # $1 card file
436
+ local f="$1"
437
+ # Extract contract fields; exclude status/Evidence
438
+ {
439
+ grep -E '^(deps|implements|risk|risk-reason|risk-ack):' "$f" 2>/dev/null || true
440
+ awk '
441
+ /^## Scope/{p=1; print; next}
442
+ /^## Allowed files/{p=1; print; next}
443
+ /^## Verify/{p=1; print; next}
444
+ /^## Done-evidence/{p=1; print; next}
445
+ /^## Evidence/{p=0; next}
446
+ /^## /{ if($0 !~ /^## (Scope|Allowed files|Verify|Done-evidence)/) p=0 }
447
+ p{print}
448
+ ' "$f" 2>/dev/null || true
449
+ } | tr -d '\r'
450
+ }
451
+
452
+ _att_stage_fingerprint() { # $1 stage $2 revision -> fp (also sets _ATT_LAST_FP)
453
+ local stage="$1" rev="$2" path blob fmt
454
+ path="flow/${stage}.md"
455
+ blob="$(git -C "$ROOT" rev-parse "${rev}:${path}" 2>/dev/null | tr -d '\r\n')" || return 1
456
+ fmt="$(_att_object_format)"
457
+ _ATT_LAST_FP="$(printf 'git-%s:%s' "$fmt" "$blob")"
458
+ printf '%s' "$_ATT_LAST_FP"
459
+ }
460
+
461
+ _att_card_semantic_fingerprint() { # $1 card file $2 base $3 head -> prints fp; sets _ATT_SUBJ_*
462
+ local file="$1" base="$2" head="$3" id proj tree body fmt oid path_manifest
463
+ id="$(basename "$file" .md)"
464
+ base="$(_att_full_commit "$base")" || return 1
465
+ head="$(_att_full_commit "$head")" || return 1
466
+ tree="$(git -C "$ROOT" rev-parse "${head}^{tree}" 2>/dev/null | tr -d '\r\n')" || return 1
467
+ proj="$(_att_card_contract_projection "$file")"
468
+ # changed paths base..head excluding card path
469
+ path_manifest="$(git -C "$ROOT" diff-tree -r --no-commit-id --name-status "$base" "$head" 2>/dev/null \
470
+ | awk -v card="cards/${id}.md" '$2 != card && $3 != card {print}' \
471
+ | while read -r st p rest; do
472
+ case "$st" in
473
+ R*|C*) p="$rest" ;;
474
+ esac
475
+ [ -z "$p" ] && continue
476
+ mode_oid="$(git -C "$ROOT" ls-tree "$head" -- "$p" 2>/dev/null | awk '{print $1" "$3}')"
477
+ printf '%s %s\n' "$p" "$mode_oid"
478
+ done | sort)"
479
+ body="base=${base}"$'\n'"head=${head}"$'\n'"tree=${tree}"$'\n'"proj<<"$'\n'"${proj}"$'\n'">>"$'\n'"paths<<"$'\n'"${path_manifest}"$'\n'">>"$'\n'
480
+ fmt="$(_att_object_format)"
481
+ oid="$(printf '%s' "$body" | git -C "$ROOT" hash-object --stdin 2>/dev/null | tr -d '\r\n')"
482
+ _ATT_SUBJ_BASE="$base"
483
+ _ATT_SUBJ_REV="$head"
484
+ _ATT_SUBJ_TREE="$tree"
485
+ _ATT_LAST_FP="$(printf 'git-%s:%s' "$fmt" "$oid")"
486
+ printf '%s' "$_ATT_LAST_FP"
487
+ }
488
+
489
+ # ---------- receipt parse / write ----------
490
+ _att_receipt_parse() { # $1 path — sets _ATT_R_* ; return 0 valid schema structure
491
+ local f="$1" line k v seen="" n=0 size
492
+ _ATT_R_schema=""; _ATT_R_kind=""; _ATT_R_subject_type=""; _ATT_R_subject_id=""
493
+ _ATT_R_subject_fingerprint=""; _ATT_R_verdict=""; _ATT_R_actor=""; _ATT_R_engine=""
494
+ _ATT_R_evidence_ref=""; _ATT_R_timestamp=""; _ATT_R_override_ref=""
495
+ _ATT_R_owner_ref=""; _ATT_R_owner_fingerprint=""
496
+ _ATT_R_subject_revision=""; _ATT_R_subject_base=""; _ATT_R_subject_tree=""
497
+ _ATT_R_command_fingerprint=""; _ATT_R_result_code=""; _ATT_R_result_fingerprint=""
498
+ _ATT_R_duration_ms=""; _ATT_R_target_id=""
499
+ _ATT_R_revision_oracle_ref=""; _ATT_R_revision_oracle_fingerprint=""
500
+ [ -f "$f" ] || return 1
501
+ size="$(wc -c < "$f" 2>/dev/null | tr -d ' ')"
502
+ [ "${size:-0}" -le 16384 ] || return 1
503
+ while IFS= read -r line || [ -n "$line" ]; do
504
+ line="$(printf '%s' "$line" | tr -d '\r')"
505
+ [ -z "$line" ] && continue
506
+ n=$((n + 1))
507
+ [ "$n" -gt 64 ] && return 1
508
+ case "$line" in
509
+ *": "*) k="${line%%: *}"; v="${line#*: }" ;;
510
+ *) return 1 ;;
511
+ esac
512
+ case "$k" in
513
+ schema|kind|subject_type|subject_id|subject_fingerprint|verdict|actor|engine|evidence_ref|timestamp|override_ref|owner_ref|owner_fingerprint|subject_revision|subject_base|subject_tree|command_fingerprint|result_code|result_fingerprint|duration_ms|target_id|revision_oracle_ref|revision_oracle_fingerprint) : ;;
514
+ *) return 1 ;;
515
+ esac
516
+ case " $seen " in *" $k "*) return 1 ;; esac
517
+ seen="$seen $k"
518
+ case "$v" in *$'\n'*|*$'\t'*) return 1 ;; esac
519
+ [ "${#v}" -le 512 ] || return 1
520
+ case "$k" in
521
+ schema) _ATT_R_schema="$v" ;;
522
+ kind) _ATT_R_kind="$v" ;;
523
+ subject_type) _ATT_R_subject_type="$v" ;;
524
+ subject_id) _ATT_R_subject_id="$v" ;;
525
+ subject_fingerprint) _ATT_R_subject_fingerprint="$v" ;;
526
+ verdict) _ATT_R_verdict="$v" ;;
527
+ actor) _ATT_R_actor="$v" ;;
528
+ engine) _ATT_R_engine="$v" ;;
529
+ evidence_ref) _ATT_R_evidence_ref="$v" ;;
530
+ timestamp) _ATT_R_timestamp="$v" ;;
531
+ override_ref) _ATT_R_override_ref="$v" ;;
532
+ owner_ref) _ATT_R_owner_ref="$v" ;;
533
+ owner_fingerprint) _ATT_R_owner_fingerprint="$v" ;;
534
+ subject_revision) _ATT_R_subject_revision="$v" ;;
535
+ subject_base) _ATT_R_subject_base="$v" ;;
536
+ subject_tree) _ATT_R_subject_tree="$v" ;;
537
+ command_fingerprint) _ATT_R_command_fingerprint="$v" ;;
538
+ result_code) _ATT_R_result_code="$v" ;;
539
+ result_fingerprint) _ATT_R_result_fingerprint="$v" ;;
540
+ duration_ms) _ATT_R_duration_ms="$v" ;;
541
+ target_id) _ATT_R_target_id="$v" ;;
542
+ revision_oracle_ref) _ATT_R_revision_oracle_ref="$v" ;;
543
+ revision_oracle_fingerprint) _ATT_R_revision_oracle_fingerprint="$v" ;;
544
+ esac
545
+ done < "$f"
546
+ [ "$_ATT_R_schema" = "$_ATT_SCHEMA" ] || return 1
547
+ case "$_ATT_R_kind" in semantic_gate|live_verify) : ;; *) return 1 ;; esac
548
+ case "$_ATT_R_subject_type" in stage|card) : ;; *) return 1 ;; esac
549
+ _att_valid_subject_id "$_ATT_R_subject_id" || return 1
550
+ case "$_ATT_R_verdict" in pass|fail|override) : ;; *) return 1 ;; esac
551
+ [ "$_ATT_R_kind" = "live_verify" ] && [ "$_ATT_R_verdict" = "override" ] && return 1
552
+ _att_valid_actor "$_ATT_R_actor" || return 1
553
+ _att_valid_actor "$_ATT_R_engine" || return 1
554
+ _att_valid_repo_ref "$_ATT_R_evidence_ref" || return 1
555
+ _att_valid_repo_ref "$_ATT_R_owner_ref" || return 1
556
+ [ -n "$_ATT_R_subject_fingerprint" ] && [ -n "$_ATT_R_owner_fingerprint" ] && [ -n "$_ATT_R_timestamp" ] || return 1
557
+ return 0
558
+ }
559
+
560
+ _att_atomic_write() { # $1 dest $2 content
561
+ local dest="$1" dir tmp
562
+ dir="$(dirname "$dest")"
563
+ mkdir -p "$dir" 2>/dev/null || return 1
564
+ tmp="$dir/.tmp.$$.$(_att_now_s)"
565
+ printf '%s' "$2" > "$tmp" || { rm -f "$tmp"; return 1; }
566
+ chmod 600 "$tmp" 2>/dev/null || true
567
+ mv -f "$tmp" "$dest" || { rm -f "$tmp"; return 1; }
568
+ return 0
569
+ }
570
+
571
+ _att_write_receipt() { # builds from _ATT_W_* vars
572
+ local body dest kind st sid
573
+ kind="$_ATT_W_kind"; st="$_ATT_W_subject_type"; sid="$_ATT_W_subject_id"
574
+ dest="$(_att_receipt_path "$kind" "$st" "$sid")" || return 1
575
+ body="schema: ${_ATT_SCHEMA}
576
+ kind: ${kind}
577
+ subject_type: ${st}
578
+ subject_id: ${sid}
579
+ subject_fingerprint: ${_ATT_W_subject_fingerprint}
580
+ verdict: ${_ATT_W_verdict}
581
+ actor: ${_ATT_W_actor}
582
+ engine: ${_ATT_W_engine}
583
+ evidence_ref: ${_ATT_W_evidence_ref}
584
+ timestamp: ${_ATT_W_timestamp}
585
+ override_ref: ${_ATT_W_override_ref}
586
+ owner_ref: ${_ATT_W_owner_ref}
587
+ owner_fingerprint: ${_ATT_W_owner_fingerprint}
588
+ "
589
+ if [ "$kind" = "semantic_gate" ] && [ "$st" = "stage" ]; then
590
+ body="${body}subject_revision: ${_ATT_W_subject_revision}
591
+ "
592
+ fi
593
+ if [ "$kind" = "semantic_gate" ] && [ "$st" = "card" ]; then
594
+ body="${body}subject_base: ${_ATT_W_subject_base}
595
+ subject_revision: ${_ATT_W_subject_revision}
596
+ subject_tree: ${_ATT_W_subject_tree}
597
+ "
598
+ fi
599
+ if [ "$kind" = "live_verify" ]; then
600
+ body="${body}subject_revision: ${_ATT_W_subject_revision}
601
+ command_fingerprint: ${_ATT_W_command_fingerprint}
602
+ result_code: ${_ATT_W_result_code}
603
+ result_fingerprint: ${_ATT_W_result_fingerprint}
604
+ duration_ms: ${_ATT_W_duration_ms}
605
+ target_id: ${_ATT_W_target_id}
606
+ revision_oracle_ref: ${_ATT_W_revision_oracle_ref}
607
+ revision_oracle_fingerprint: ${_ATT_W_revision_oracle_fingerprint}
608
+ "
609
+ fi
610
+ _att_atomic_write "$dest" "$body"
611
+ }
612
+
613
+ # ---------- process supervisor ----------
614
+ # Probe: can we create a process group and kill it?
615
+ _att_supervisor_capable() {
616
+ if [ "${FLOW_ATTEST_SUPERVISOR:-}" = "force-unsupported" ]; then return 1; fi
617
+ if [ "${FLOW_ATTEST_SUPERVISOR:-}" = "force-ok" ]; then return 0; fi
618
+ # Prefer setsid or bash job control with set -m
619
+ if command -v setsid >/dev/null 2>&1; then return 0; fi
620
+ # bash with job control usually available
621
+ return 0
622
+ }
623
+
624
+ # Run argv array: _att_run_supervised cmd args...
625
+ # Sets: _ATT_RUN_RC (exit|timeout|output-cap|signal|spawn-error), _ATT_RUN_CODE (numeric),
626
+ # _ATT_RUN_OUT (bounded stdout), _ATT_RUN_MS
627
+ _att_run_supervised() {
628
+ # Re-read bounds each call (tests/operators export FLOW_ATTEST_* after source).
629
+ local timeout_s="${FLOW_ATTEST_TIMEOUT_S:-30}"
630
+ local cap="${FLOW_ATTEST_OUT_CAP:-65536}"
631
+ local comb="${FLOW_ATTEST_COMBINED_CAP:-98304}"
632
+ local grace_s="${FLOW_ATTEST_GRACE_S:-2}"
633
+ local outf errf start end rc=0 pid="" pgid_file
634
+ _ATT_RUN_RC="spawn-error"; _ATT_RUN_CODE=127; _ATT_RUN_OUT=""; _ATT_RUN_MS=0
635
+ if ! _att_supervisor_capable; then
636
+ _ATT_RUN_RC="spawn-error"; return 1
637
+ fi
638
+ case "$timeout_s" in ''|*[!0-9]*|0) timeout_s=30 ;; esac
639
+ case "$cap" in ''|*[!0-9]*|0) cap=65536 ;; esac
640
+ case "$comb" in ''|*[!0-9]*|0) comb=98304 ;; esac
641
+ outf="$(mktemp 2>/dev/null || echo "${TMPDIR:-/tmp}/.att-out.$$")"
642
+ errf="$(mktemp 2>/dev/null || echo "${TMPDIR:-/tmp}/.att-err.$$")"
643
+ pgid_file="$(mktemp 2>/dev/null || echo "${TMPDIR:-/tmp}/.att-pgid.$$")"
644
+ : > "$outf"; : > "$errf"; : > "$pgid_file"
645
+ start="$(_att_now_s)"
646
+ # Prefer setsid so the child owns a distinct session we can kill by PGID.
647
+ if command -v setsid >/dev/null 2>&1; then
648
+ (
649
+ cd "$ROOT" || exit 127
650
+ setsid "$@" >"$outf" 2>"$errf"
651
+ ) &
652
+ pid=$!
653
+ else
654
+ (
655
+ cd "$ROOT" || exit 127
656
+ set -m
657
+ "$@" >"$outf" 2>"$errf"
658
+ ) &
659
+ pid=$!
660
+ fi
661
+ # Best-effort record of process group for the wrapper
662
+ printf '%s\n' "$pid" > "$pgid_file"
663
+ _att_kill_tree() {
664
+ local sig="$1"
665
+ kill -"$sig" -"$pid" 2>/dev/null || kill -"$sig" "$pid" 2>/dev/null || true
666
+ # also try children via pkill when available
667
+ if command -v pkill >/dev/null 2>&1; then
668
+ pkill -"$sig" -P "$pid" 2>/dev/null || true
669
+ fi
670
+ }
671
+ # Watchdog
672
+ (
673
+ sleep "$timeout_s" 2>/dev/null
674
+ if kill -0 "$pid" 2>/dev/null; then
675
+ _att_kill_tree TERM
676
+ sleep "$grace_s" 2>/dev/null
677
+ _att_kill_tree KILL
678
+ echo timeout > "${outf}.to"
679
+ fi
680
+ ) &
681
+ local wd=$!
682
+ # Stream cap monitor
683
+ (
684
+ while kill -0 "$pid" 2>/dev/null; do
685
+ local os es
686
+ os="$(wc -c < "$outf" 2>/dev/null | tr -d ' ')"
687
+ es="$(wc -c < "$errf" 2>/dev/null | tr -d ' ')"
688
+ if [ "${os:-0}" -gt "$cap" ] || [ "${es:-0}" -gt "$cap" ] || [ $(( ${os:-0} + ${es:-0} )) -gt "$comb" ]; then
689
+ _att_kill_tree TERM
690
+ sleep 1
691
+ _att_kill_tree KILL
692
+ echo cap > "${outf}.cap"
693
+ break
694
+ fi
695
+ sleep 0.1 2>/dev/null || sleep 1
696
+ done
697
+ ) &
698
+ local capw=$!
699
+ wait "$pid" 2>/dev/null; rc=$?
700
+ kill "$wd" 2>/dev/null; wait "$wd" 2>/dev/null
701
+ kill "$capw" 2>/dev/null; wait "$capw" 2>/dev/null
702
+ end="$(_att_now_s)"
703
+ _ATT_RUN_MS=$(( (end - start) * 1000 ))
704
+ [ "$_ATT_RUN_MS" -lt 0 ] && _ATT_RUN_MS=0
705
+ if [ -f "${outf}.cap" ]; then
706
+ _ATT_RUN_RC="output-cap"; _ATT_RUN_CODE=1
707
+ rm -f "${outf}.cap"
708
+ elif [ -f "${outf}.to" ]; then
709
+ _ATT_RUN_RC="timeout"; _ATT_RUN_CODE=124
710
+ rm -f "${outf}.to"
711
+ elif [ "$rc" -eq 137 ] || [ "$rc" -eq 143 ]; then
712
+ if [ $((end - start)) -ge "$timeout_s" ]; then
713
+ _ATT_RUN_RC="timeout"; _ATT_RUN_CODE=124
714
+ else
715
+ _ATT_RUN_RC="signal"; _ATT_RUN_CODE="$rc"
716
+ fi
717
+ elif [ "$rc" -eq 127 ]; then
718
+ _ATT_RUN_RC="spawn-error"; _ATT_RUN_CODE=127
719
+ else
720
+ _ATT_RUN_RC="exit"; _ATT_RUN_CODE="$rc"
721
+ fi
722
+ _ATT_RUN_OUT="$(head -c "$cap" "$outf" 2>/dev/null | tr -d '\000')"
723
+ rm -f "$outf" "$errf" "$pgid_file" 2>/dev/null || true
724
+ return 0
725
+ }
726
+
727
+ _att_result_code_str() {
728
+ case "$_ATT_RUN_RC" in
729
+ exit) printf 'exit:%s' "$_ATT_RUN_CODE" ;;
730
+ timeout) printf 'timeout' ;;
731
+ output-cap) printf 'output-cap' ;;
732
+ signal) printf 'signal:%s' "$_ATT_RUN_CODE" ;;
733
+ *) printf 'spawn-error' ;;
734
+ esac
735
+ }
736
+
737
+ _att_hash_text() {
738
+ local fmt oid
739
+ fmt="$(_att_object_format)"
740
+ oid="$(printf '%s' "$1" | git -C "$ROOT" hash-object --stdin 2>/dev/null | tr -d '\r\n')"
741
+ printf 'git-%s:%s' "$fmt" "${oid:-0}"
742
+ }
743
+
744
+ # ---------- receipt currentness ----------
745
+ _att_receipt_status() { # $1 path -> current|missing|stale|invalid|red|blocked-attempt
746
+ local path="$1"
747
+ if [ ! -f "$path" ]; then echo "missing"; return 1; fi
748
+ if ! _att_receipt_parse "$path"; then echo "invalid"; return 1; fi
749
+ if [ "$_ATT_R_verdict" = "fail" ]; then echo "red"; return 1; fi
750
+ # attempt marker for live
751
+ if [ "$_ATT_R_kind" = "live_verify" ]; then
752
+ local ap; ap="$(_att_attempt_path "$_ATT_R_subject_id")"
753
+ if [ -f "$ap" ]; then echo "blocked-attempt"; return 1; fi
754
+ fi
755
+ # recompute subject fingerprint loosely
756
+ case "$_ATT_R_kind:$_ATT_R_subject_type" in
757
+ semantic_gate:stage)
758
+ local cur
759
+ if ! cur="$(_att_stage_fingerprint "$_ATT_R_subject_id" "${_ATT_R_subject_revision:-HEAD}")"; then
760
+ echo "stale"; return 1
761
+ fi
762
+ # compare to current HEAD blob too
763
+ local headfp
764
+ headfp="$(_att_stage_fingerprint "$_ATT_R_subject_id" HEAD 2>/dev/null || true)"
765
+ if [ "$_ATT_R_subject_fingerprint" != "$cur" ] && [ "$_ATT_R_subject_fingerprint" != "$headfp" ]; then
766
+ echo "stale"; return 1
767
+ fi
768
+ # if HEAD blob differs from recorded revision blob, stale unless same
769
+ if [ -n "$headfp" ] && [ "$headfp" != "$_ATT_R_subject_fingerprint" ]; then
770
+ echo "stale"; return 1
771
+ fi
772
+ ;;
773
+ semantic_gate:card)
774
+ local file; file="$(resolve_card_file "$_ATT_R_subject_id")"
775
+ [ -f "$file" ] || { echo "stale"; return 1; }
776
+ local head; head="$(_att_full_commit HEAD)"
777
+ [ -n "$head" ] && [ -n "$_ATT_R_subject_revision" ] && [ -n "$_ATT_R_subject_base" ] || { echo "stale"; return 1; }
778
+ # Same revision: recompute fingerprint against base..rev (must match receipt).
779
+ if [ "$head" = "$_ATT_R_subject_revision" ]; then
780
+ if ! _att_card_semantic_fingerprint "$file" "$_ATT_R_subject_base" "$_ATT_R_subject_revision" >/dev/null; then
781
+ echo "stale"; return 1
782
+ fi
783
+ [ "$_ATT_R_subject_fingerprint" = "$_ATT_LAST_FP" ] || { echo "stale"; return 1; }
784
+ else
785
+ # Ancestry-preserving integration path: always revalidate projection + path survival.
786
+ if ! _att_is_ancestor "$_ATT_R_subject_revision" "$head"; then
787
+ echo "stale"; return 1
788
+ fi
789
+ local proj_now proj_old tmpc
790
+ proj_now="$(_att_card_contract_projection "$file")"
791
+ tmpc="$(mktemp 2>/dev/null || echo "${TMPDIR:-/tmp}/.att-card.$$")"
792
+ if ! git -C "$ROOT" show "${_ATT_R_subject_revision}:cards/${_ATT_R_subject_id}.md" >"$tmpc" 2>/dev/null; then
793
+ rm -f "$tmpc"; echo "stale"; return 1
794
+ fi
795
+ proj_old="$(_att_card_contract_projection "$tmpc")"
796
+ rm -f "$tmpc"
797
+ [ "$proj_now" = "$proj_old" ] || { echo "stale"; return 1; }
798
+ if ! _att_card_paths_survive "$_ATT_R_subject_base" "$_ATT_R_subject_revision" "$head" "$_ATT_R_subject_id"; then
799
+ echo "stale"; return 1
800
+ fi
801
+ fi
802
+ ;;
803
+ live_verify:card)
804
+ [ "$_ATT_R_verdict" = "pass" ] || { echo "red"; return 1; }
805
+ local head file proj cur_fp
806
+ head="$(_att_full_commit HEAD)"
807
+ file="$(resolve_card_file "$_ATT_R_subject_id")"
808
+ [ -f "$file" ] || { echo "stale"; return 1; }
809
+ if [ -n "$_ATT_R_subject_revision" ] && [ "$_ATT_R_subject_revision" != "$head" ] \
810
+ && ! _att_is_ancestor "$_ATT_R_subject_revision" "$head"; then
811
+ echo "stale"; return 1
812
+ fi
813
+ # Recompute live subject fingerprint from current Verify/Done-evidence + recorded owner/target/rev.
814
+ proj="$(awk '/^## Verify/{p=1} /^## Done-evidence/{p=1} /^## Evidence/{p=0} /^## /{if($0!~/^## (Verify|Done-evidence)/)p=0} p{print}' "$file" | tr -d '\r')"
815
+ cur_fp="$(_att_hash_text "live|${_ATT_R_subject_revision}|${_ATT_R_target_id}|${_ATT_R_owner_fingerprint}|${proj}")"
816
+ [ "$cur_fp" = "$_ATT_R_subject_fingerprint" ] || { echo "stale"; return 1; }
817
+ ;;
818
+ esac
819
+ case "$_ATT_R_verdict" in
820
+ pass|override) echo "current"; return 0 ;;
821
+ fail) echo "red"; return 1 ;;
822
+ esac
823
+ echo "invalid"; return 1
824
+ }
825
+
826
+ _att_card_paths_survive() { # base rev head cardid
827
+ local base="$1" rev="$2" head="$3" id="$4" p mode_rev mode_head oid_rev oid_head
828
+ # name-only: each line is the path (not name-status $2)
829
+ while IFS= read -r p; do
830
+ p="$(printf '%s' "$p" | tr -d '\r')"
831
+ [ -z "$p" ] && continue
832
+ case "$p" in "cards/${id}.md") continue ;; esac
833
+ mode_rev="$(git -C "$ROOT" ls-tree "$rev" -- "$p" 2>/dev/null | awk '{print $1}')"
834
+ oid_rev="$(git -C "$ROOT" ls-tree "$rev" -- "$p" 2>/dev/null | awk '{print $3}')"
835
+ mode_head="$(git -C "$ROOT" ls-tree "$head" -- "$p" 2>/dev/null | awk '{print $1}')"
836
+ oid_head="$(git -C "$ROOT" ls-tree "$head" -- "$p" 2>/dev/null | awk '{print $3}')"
837
+ [ -n "$mode_rev" ] && [ -n "$oid_rev" ] || return 1
838
+ [ "$mode_rev" = "$mode_head" ] && [ "$oid_rev" = "$oid_head" ] || return 1
839
+ done <<EOF
840
+ $(git -C "$ROOT" diff-tree -r --name-only "$base" "$rev" 2>/dev/null)
841
+ EOF
842
+ return 0
843
+ }
844
+
845
+ _att_accepted_semantic() { # subject_type subject_id — exit 0 if current pass|override
846
+ local st="$1" sid="$2" path
847
+ path="$(_att_receipt_path semantic_gate "$st" "$sid")"
848
+ # Do not use command substitution: _att_receipt_status sets _ATT_R_* in this shell.
849
+ if ! _att_receipt_status "$path" >/dev/null 2>&1; then return 1; fi
850
+ case "${_ATT_R_verdict:-}" in pass|override) return 0 ;; *) return 1 ;; esac
851
+ }
852
+
853
+ _att_accepted_live() { # card id
854
+ local path
855
+ path="$(_att_receipt_path live_verify card "$1")"
856
+ if [ -f "$(_att_attempt_path "$1")" ]; then return 1; fi
857
+ if ! _att_receipt_status "$path" >/dev/null 2>&1; then return 1; fi
858
+ [ "${_ATT_R_verdict:-}" = "pass" ]
859
+ }
860
+
861
+ # ---------- auto state ----------
862
+ _att_auto_state_read() { # sets _ATT_A_*; echoes INACTIVE|ACTIVE|STALE|INVALID
863
+ local p; p="$(_att_auto_state_path)"
864
+ _ATT_A_schema=""; _ATT_A_status=""; _ATT_A_activated_at=""; _ATT_A_activated_by=""
865
+ _ATT_A_integration_branch=""; _ATT_A_risk_fingerprint=""; _ATT_A_contract_fingerprint=""
866
+ if [ ! -f "$p" ]; then echo "INACTIVE"; return 0; fi
867
+ if ! _att_parse_auto_file "$p"; then echo "INVALID"; return 0; fi
868
+ # revalidate
869
+ if ! _att_auto_current_ok; then echo "STALE"; return 0; fi
870
+ echo "ACTIVE"; return 0
871
+ }
872
+
873
+ _att_parse_auto_file() {
874
+ local f="$1" line k v seen=""
875
+ while IFS= read -r line || [ -n "$line" ]; do
876
+ line="$(printf '%s' "$line" | tr -d '\r')"
877
+ [ -z "$line" ] && continue
878
+ case "$line" in
879
+ *": "*) k="${line%%: *}"; v="${line#*: }" ;;
880
+ *) return 1 ;;
881
+ esac
882
+ case "$k" in
883
+ schema|status|activated_at|activated_by|integration_branch|risk_fingerprint|contract_fingerprint) : ;;
884
+ *) return 1 ;;
885
+ esac
886
+ case " $seen " in *" $k "*) return 1 ;; esac
887
+ seen="$seen $k"
888
+ case "$k" in
889
+ schema) _ATT_A_schema="$v" ;;
890
+ status) _ATT_A_status="$v" ;;
891
+ activated_at) _ATT_A_activated_at="$v" ;;
892
+ activated_by) _ATT_A_activated_by="$v" ;;
893
+ integration_branch) _ATT_A_integration_branch="$v" ;;
894
+ risk_fingerprint) _ATT_A_risk_fingerprint="$v" ;;
895
+ contract_fingerprint) _ATT_A_contract_fingerprint="$v" ;;
896
+ esac
897
+ done < "$f"
898
+ [ "$_ATT_A_schema" = "$_ATT_AUTO_SCHEMA" ] || return 1
899
+ [ "$_ATT_A_status" = "active" ] || return 1
900
+ return 0
901
+ }
902
+
903
+ _att_auto_current_ok() {
904
+ _att_git_ok || return 1
905
+ local br head rfp cfp f rv
906
+ br="$(git -C "$ROOT" rev-parse --abbrev-ref HEAD 2>/dev/null | tr -d '\r')"
907
+ [ "$br" = "$_ATT_A_integration_branch" ] || return 1
908
+ [ "$br" != "HEAD" ] || return 1
909
+ # risk set revalidation
910
+ for f in $(_att_all_card_files); do
911
+ rv="$(_card_risk_validate "$f" auto 2>/dev/null || echo bad)"
912
+ case "$rv" in standard|security-class) : ;; *) return 1 ;; esac
913
+ done
914
+ rfp="$(_att_risk_fingerprint)"
915
+ [ "$rfp" = "$_ATT_A_risk_fingerprint" ] || return 1
916
+ # stage 05 receipt
917
+ _att_accepted_semantic stage 05-contract || return 1
918
+ cfp="$_ATT_R_subject_fingerprint"
919
+ [ "$cfp" = "$_ATT_A_contract_fingerprint" ] || return 1
920
+ return 0
921
+ }
922
+
923
+ _att_auto_write() {
924
+ local path body
925
+ path="$(_att_auto_state_path)"
926
+ mkdir -p "$(dirname "$path")" 2>/dev/null || true
927
+ body="schema: ${_ATT_AUTO_SCHEMA}
928
+ status: active
929
+ activated_at: ${_ATT_W_activated_at}
930
+ activated_by: ${_ATT_W_activated_by}
931
+ integration_branch: ${_ATT_W_integration_branch}
932
+ risk_fingerprint: ${_ATT_W_risk_fingerprint}
933
+ contract_fingerprint: ${_ATT_W_contract_fingerprint}
934
+ "
935
+ _att_atomic_write "$path" "$body"
936
+ }
937
+
938
+ _att_auto_clear() {
939
+ rm -f "$(_att_auto_state_path)" 2>/dev/null || true
940
+ }
941
+
942
+ _att_auto_is_enforcing() {
943
+ # File existence is the policy latch; STALE/INVALID still hard-enforce (plan matrix).
944
+ local p; p="$(_att_auto_state_path)"
945
+ [ -f "$p" ]
946
+ }
947
+
948
+ _att_is_main_worktree() {
949
+ _att_git_ok || return 1
950
+ local git_dir common_dir
951
+ git_dir="$(git -C "$ROOT" rev-parse --absolute-git-dir 2>/dev/null || true)"
952
+ common_dir="$(git -C "$ROOT" rev-parse --git-common-dir 2>/dev/null || true)"
953
+ case "$common_dir" in /*) : ;; "") return 1 ;; *)
954
+ common_dir="$(cd "$ROOT" && cd "$common_dir" 2>/dev/null && pwd)"
955
+ ;; esac
956
+ [ -n "$git_dir" ] && [ -n "$common_dir" ] || return 1
957
+ # linked when git_dir != common_dir
958
+ [ "$(cd "$(dirname "$git_dir")" 2>/dev/null && pwd -P)/$(basename "$git_dir")" = \
959
+ "$(cd "$(dirname "$common_dir")" 2>/dev/null && pwd -P)/$(basename "$common_dir")" ] 2>/dev/null \
960
+ || {
961
+ # compare realpaths
962
+ local a b
963
+ a="$(cd "$git_dir" 2>/dev/null && pwd -P || echo "$git_dir")"
964
+ b="$(cd "$common_dir" 2>/dev/null && pwd -P || echo "$common_dir")"
965
+ [ "$a" = "$b" ]
966
+ }
967
+ }
968
+
969
+ _att_main_worktree_path() {
970
+ git -C "$ROOT" worktree list --porcelain 2>/dev/null | awk '/^worktree /{print substr($0,10); exit}'
971
+ }
972
+
973
+ # ---------- mint commands ----------
974
+ _att_default_actor() {
975
+ local a
976
+ a="$(_att_sanitize_label "${FLOW_SESSION_ID:-flow}")"
977
+ [ -n "$a" ] || a="flow"
978
+ printf '%s' "$a"
979
+ }
980
+
981
+ _att_default_engine() {
982
+ local e
983
+ e="$(_att_sanitize_label "${FLOW_ENGINE_TIER:-builtin}")"
984
+ [ -n "$e" ] || e="builtin"
985
+ printf '%s' "$e"
986
+ }
987
+
988
+ _att_mint_semantic_stage() {
989
+ local stage="$1" rev="$2" owner_rel="$3"
990
+ local full owner_abs fp ofp out_doc verdict crit high eref
991
+ _att_is_stage "$stage" || { echo "FAIL: invalid stage '$stage'"; return 2; }
992
+ _att_git_ok || { echo "FAIL: attest requires a Git repository"; return 2; }
993
+ full="$(_att_full_commit "$rev")" || { echo "FAIL: bad revision"; return 2; }
994
+ _att_git_clean_for_attest || { echo "FAIL: dirty worktree (only .flow/ excluded)"; return 1; }
995
+ owner_abs="$ROOT/$owner_rel"
996
+ [ -f "$owner_abs" ] || { echo "FAIL: owner manifest not found: $owner_rel"; return 2; }
997
+ # owner must be tracked at revision
998
+ git -C "$ROOT" cat-file -e "${full}:${owner_rel}" 2>/dev/null || { echo "FAIL: owner not committed at revision"; return 1; }
999
+ _att_parse_kv_file "$owner_abs" || { echo "FAIL: invalid owner manifest"; return 2; }
1000
+ [ "$_ATT_KV_kind" = "semantic_gate" ] || { echo "FAIL: owner kind must be semantic_gate"; return 2; }
1001
+ [ "$_ATT_KV_subject_id" = "$stage" ] || { echo "FAIL: owner subject_id mismatch"; return 2; }
1002
+ [ "$_ATT_KV_target_id" = "none" ] && [ "$_ATT_KV_revision_oracle" = "none" ] || { echo "FAIL: semantic owner must have target/oracle none"; return 2; }
1003
+ case "$_ATT_KV_command" in repo:*) : ;; *) echo "FAIL: command must be repo:..."; return 2 ;; esac
1004
+ local cmd_rel="${_ATT_KV_command#repo:}"
1005
+ [ -f "$ROOT/$cmd_rel" ] || { echo "FAIL: command not found"; return 2; }
1006
+ git -C "$ROOT" cat-file -e "${full}:${cmd_rel}" 2>/dev/null || { echo "FAIL: command not committed"; return 1; }
1007
+ if ! _att_stage_fingerprint "$stage" "$full" >/dev/null; then echo "FAIL: stage fingerprint"; return 1; fi
1008
+ fp="$_ATT_LAST_FP"
1009
+ ofp="$(_att_owner_fingerprint "$owner_rel" "$full")" || { echo "FAIL: owner fingerprint"; return 1; }
1010
+
1011
+ local lname; lname="$(_att_subject_lock_name semantic_gate "$stage")"
1012
+ _att_lock_acquire "$lname" || return 1
1013
+ # run producer
1014
+ chmod +x "$ROOT/$cmd_rel" 2>/dev/null || true
1015
+ _att_run_supervised "$ROOT/$cmd_rel" --subject-id "$stage" --subject-revision "$full" --subject-fingerprint "$fp"
1016
+ if [ "$_ATT_RUN_RC" != "exit" ] || [ "$_ATT_RUN_CODE" -ne 0 ]; then
1017
+ _ATT_W_kind=semantic_gate; _ATT_W_subject_type=stage; _ATT_W_subject_id="$stage"
1018
+ _ATT_W_subject_fingerprint="$fp"; _ATT_W_verdict=fail
1019
+ _ATT_W_actor="$(_att_default_actor)"; _ATT_W_engine="$(_att_default_engine)"
1020
+ _ATT_W_evidence_ref=none; _ATT_W_timestamp="$(_att_now_rfc3339)"; _ATT_W_override_ref=none
1021
+ _ATT_W_owner_ref="repo:${owner_rel}"; _ATT_W_owner_fingerprint="$ofp"
1022
+ _ATT_W_subject_revision="$full"
1023
+ _att_write_receipt || true
1024
+ _att_lock_release "$lname"
1025
+ echo "FAIL: semantic producer non-zero (${_ATT_RUN_RC}:${_ATT_RUN_CODE})"
1026
+ return 1
1027
+ fi
1028
+ # parse semantic result
1029
+ out_doc="$_ATT_RUN_OUT"
1030
+ verdict="$(printf '%s\n' "$out_doc" | awk -F': ' '/^verdict: /{print $2; exit}' | tr -d '\r')"
1031
+ local sfp
1032
+ sfp="$(printf '%s\n' "$out_doc" | awk -F': ' '/^subject_fingerprint: /{print $2; exit}' | tr -d '\r')"
1033
+ crit="$(printf '%s\n' "$out_doc" | awk -F': ' '/^critical_count: /{print $2; exit}' | tr -d '\r')"
1034
+ high="$(printf '%s\n' "$out_doc" | awk -F': ' '/^high_count: /{print $2; exit}' | tr -d '\r')"
1035
+ eref="$(printf '%s\n' "$out_doc" | awk -F': ' '/^evidence_ref: /{print $2; exit}' | tr -d '\r')"
1036
+ local sch
1037
+ sch="$(printf '%s\n' "$out_doc" | awk -F': ' '/^schema: /{print $2; exit}' | tr -d '\r')"
1038
+ if [ "$sch" != "$_ATT_SEM_RESULT_SCHEMA" ] || [ "$sfp" != "$fp" ]; then
1039
+ _att_lock_release "$lname"
1040
+ echo "FAIL: producer result schema/fingerprint mismatch"; return 1
1041
+ fi
1042
+ case "$verdict" in pass|fail) : ;; *) _att_lock_release "$lname"; echo "FAIL: bad producer verdict"; return 1 ;; esac
1043
+ if [ "$verdict" = "pass" ]; then
1044
+ [ "$crit" = "0" ] && [ "$high" = "0" ] || verdict=fail
1045
+ fi
1046
+ [ -n "$eref" ] || eref=none
1047
+ _att_valid_repo_ref "$eref" || eref=none
1048
+ _ATT_W_kind=semantic_gate; _ATT_W_subject_type=stage; _ATT_W_subject_id="$stage"
1049
+ _ATT_W_subject_fingerprint="$fp"; _ATT_W_verdict="$verdict"
1050
+ _ATT_W_actor="$(_att_default_actor)"; _ATT_W_engine="$(_att_default_engine)"
1051
+ _ATT_W_evidence_ref="$eref"; _ATT_W_timestamp="$(_att_now_rfc3339)"; _ATT_W_override_ref=none
1052
+ _ATT_W_owner_ref="repo:${owner_rel}"; _ATT_W_owner_fingerprint="$ofp"
1053
+ _ATT_W_subject_revision="$full"
1054
+ if ! _att_write_receipt; then _att_lock_release "$lname"; echo "FAIL: write receipt"; return 1; fi
1055
+ _att_lock_release "$lname"
1056
+ echo "PASS: semantic_gate stage $stage verdict=$verdict fp=$fp"
1057
+ [ "$verdict" = "pass" ] || [ "$verdict" = "override" ]
1058
+ }
1059
+
1060
+ _att_mint_semantic_card() {
1061
+ local id="$1" base="$2" rev="$3" owner_rel="$4"
1062
+ local file full_base full_head fp ofp
1063
+ id="$(_att_norm_card_id "$id")" || { echo "FAIL: bad card id"; return 2; }
1064
+ file="$(resolve_card_file "$id")"
1065
+ [ -f "$file" ] || { echo "FAIL: card not found"; return 2; }
1066
+ _att_git_ok || { echo "FAIL: attest requires Git"; return 2; }
1067
+ _att_git_clean_for_attest || { echo "FAIL: dirty worktree"; return 1; }
1068
+ full_base="$(_att_full_commit "$base")" || { echo "FAIL: bad base"; return 2; }
1069
+ full_head="$(_att_full_commit "$rev")" || { echo "FAIL: bad revision"; return 2; }
1070
+ local owner_abs="$ROOT/$owner_rel"
1071
+ [ -f "$owner_abs" ] || { echo "FAIL: owner missing"; return 2; }
1072
+ git -C "$ROOT" cat-file -e "${full_head}:${owner_rel}" 2>/dev/null || { echo "FAIL: owner not at revision"; return 1; }
1073
+ _att_parse_kv_file "$owner_abs" || { echo "FAIL: invalid owner"; return 2; }
1074
+ [ "$_ATT_KV_kind" = "semantic_gate" ] || { echo "FAIL: owner kind"; return 2; }
1075
+ [ "$_ATT_KV_subject_id" = "$id" ] || { echo "FAIL: subject_id mismatch"; return 2; }
1076
+ local cmd_rel="${_ATT_KV_command#repo:}"
1077
+ if ! _att_card_semantic_fingerprint "$file" "$full_base" "$full_head" >/dev/null; then echo "FAIL: fingerprint"; return 1; fi
1078
+ fp="$_ATT_LAST_FP"
1079
+ ofp="$(_att_owner_fingerprint "$owner_rel" "$full_head")" || { echo "FAIL: owner fp"; return 1; }
1080
+ local lname; lname="$(_att_subject_lock_name semantic_gate "$id")"
1081
+ _att_lock_acquire "$lname" || return 1
1082
+ chmod +x "$ROOT/$cmd_rel" 2>/dev/null || true
1083
+ _att_run_supervised "$ROOT/$cmd_rel" --subject-id "$id" --subject-revision "$full_head" --subject-fingerprint "$fp"
1084
+ local verdict=fail eref=none
1085
+ if [ "$_ATT_RUN_RC" = "exit" ] && [ "$_ATT_RUN_CODE" -eq 0 ]; then
1086
+ local sch sfp crit high
1087
+ sch="$(printf '%s\n' "$_ATT_RUN_OUT" | awk -F': ' '/^schema: /{print $2; exit}' | tr -d '\r')"
1088
+ sfp="$(printf '%s\n' "$_ATT_RUN_OUT" | awk -F': ' '/^subject_fingerprint: /{print $2; exit}' | tr -d '\r')"
1089
+ verdict="$(printf '%s\n' "$_ATT_RUN_OUT" | awk -F': ' '/^verdict: /{print $2; exit}' | tr -d '\r')"
1090
+ crit="$(printf '%s\n' "$_ATT_RUN_OUT" | awk -F': ' '/^critical_count: /{print $2; exit}' | tr -d '\r')"
1091
+ high="$(printf '%s\n' "$_ATT_RUN_OUT" | awk -F': ' '/^high_count: /{print $2; exit}' | tr -d '\r')"
1092
+ eref="$(printf '%s\n' "$_ATT_RUN_OUT" | awk -F': ' '/^evidence_ref: /{print $2; exit}' | tr -d '\r')"
1093
+ [ "$sch" = "$_ATT_SEM_RESULT_SCHEMA" ] && [ "$sfp" = "$fp" ] || verdict=fail
1094
+ case "$verdict" in pass|fail) : ;; *) verdict=fail ;; esac
1095
+ [ "$verdict" = "pass" ] && { [ "$crit" = "0" ] && [ "$high" = "0" ] || verdict=fail; }
1096
+ [ -n "$eref" ] || eref=none
1097
+ fi
1098
+ _ATT_W_kind=semantic_gate; _ATT_W_subject_type=card; _ATT_W_subject_id="$id"
1099
+ _ATT_W_subject_fingerprint="$fp"; _ATT_W_verdict="$verdict"
1100
+ _ATT_W_actor="$(_att_default_actor)"; _ATT_W_engine="$(_att_default_engine)"
1101
+ _ATT_W_evidence_ref="$eref"; _ATT_W_timestamp="$(_att_now_rfc3339)"; _ATT_W_override_ref=none
1102
+ _ATT_W_owner_ref="repo:${owner_rel}"; _ATT_W_owner_fingerprint="$ofp"
1103
+ _ATT_W_subject_base="$_ATT_SUBJ_BASE"; _ATT_W_subject_revision="$_ATT_SUBJ_REV"; _ATT_W_subject_tree="$_ATT_SUBJ_TREE"
1104
+ _att_write_receipt || { _att_lock_release "$lname"; return 1; }
1105
+ _att_lock_release "$lname"
1106
+ echo "PASS: semantic_gate card $id verdict=$verdict"
1107
+ [ "$verdict" = "pass" ]
1108
+ }
1109
+
1110
+ _att_mint_live() {
1111
+ local id="$1" rev="$2" owner_rel="$3"
1112
+ local file full owner_abs cmd_rel oracle_rel target ofp cfp lname ap
1113
+ id="$(_att_norm_card_id "$id")" || { echo "FAIL: bad card"; return 2; }
1114
+ file="$(resolve_card_file "$id")"; [ -f "$file" ] || { echo "FAIL: card missing"; return 2; }
1115
+ _att_git_ok || { echo "FAIL: git required"; return 2; }
1116
+ if ! _att_supervisor_capable; then echo "FAIL: process supervisor unsupported on this platform"; return 1; fi
1117
+ full="$(_att_full_commit "$rev")" || { echo "FAIL: bad revision"; return 2; }
1118
+ owner_abs="$ROOT/$owner_rel"
1119
+ [ -f "$owner_abs" ] || { echo "FAIL: owner missing"; return 2; }
1120
+ git -C "$ROOT" cat-file -e "${full}:${owner_rel}" 2>/dev/null || { echo "FAIL: owner not committed"; return 1; }
1121
+ _att_parse_kv_file "$owner_abs" || { echo "FAIL: invalid owner"; return 2; }
1122
+ [ "$_ATT_KV_kind" = "live_verify" ] || { echo "FAIL: owner kind must be live_verify"; return 2; }
1123
+ [ "$_ATT_KV_subject_id" = "$id" ] || { echo "FAIL: subject mismatch"; return 2; }
1124
+ target="$_ATT_KV_target_id"
1125
+ _att_valid_target_id "$target" && [ "$target" != "none" ] || { echo "FAIL: live needs target_id"; return 2; }
1126
+ case "$_ATT_KV_revision_oracle" in repo:*) : ;; *) echo "FAIL: live needs revision_oracle"; return 2 ;; esac
1127
+ cmd_rel="${_ATT_KV_command#repo:}"; oracle_rel="${_ATT_KV_revision_oracle#repo:}"
1128
+ ofp="$(_att_owner_fingerprint "$owner_rel" "$full")" || { echo "FAIL: owner fp"; return 1; }
1129
+ local o_fp; o_fp="$(_att_hash_text "oracle:${oracle_rel}")"
1130
+ cfp="$(_att_hash_text "argv:${cmd_rel}|${id}|${full}|${target}")"
1131
+ # live subject fingerprint from verify+done-evidence projection
1132
+ local proj fp
1133
+ proj="$(awk '/^## Verify/{p=1} /^## Done-evidence/{p=1} /^## Evidence/{p=0} /^## /{if($0!~/^## (Verify|Done-evidence)/)p=0} p{print}' "$file" | tr -d '\r')"
1134
+ fp="$(_att_hash_text "live|${full}|${target}|${ofp}|${proj}")"
1135
+ lname="$(_att_subject_lock_name live_verify "$id")"
1136
+ _att_lock_acquire "$lname" || return 1
1137
+ ap="$(_att_attempt_path "$id")"
1138
+ mkdir -p "$(dirname "$ap")" 2>/dev/null || true
1139
+ printf 'pid=%s\nstart=%s\nhost=%s\n' "$$" "$(_att_now_s)" "$(uname -n 2>/dev/null || echo host)" > "$ap"
1140
+
1141
+ # oracle first
1142
+ chmod +x "$ROOT/$oracle_rel" 2>/dev/null || true
1143
+ _att_run_supervised "$ROOT/$oracle_rel" --subject-id "$id" --subject-revision "$full" --target-id "$target"
1144
+ local oracle_oid verdict=fail rcode
1145
+ rcode="$(_att_result_code_str)"
1146
+ if [ "$_ATT_RUN_RC" = "exit" ] && [ "$_ATT_RUN_CODE" -eq 0 ]; then
1147
+ oracle_oid="$(printf '%s' "$_ATT_RUN_OUT" | tr -d '\r\n[:space:]')"
1148
+ if [ "$oracle_oid" = "$full" ]; then
1149
+ chmod +x "$ROOT/$cmd_rel" 2>/dev/null || true
1150
+ _att_run_supervised "$ROOT/$cmd_rel" --subject-id "$id" --subject-revision "$full" --subject-fingerprint "$fp"
1151
+ rcode="$(_att_result_code_str)"
1152
+ if [ "$_ATT_RUN_RC" = "exit" ] && [ "$_ATT_RUN_CODE" -eq 0 ]; then
1153
+ verdict=pass
1154
+ fi
1155
+ else
1156
+ rcode="exit:1"
1157
+ fi
1158
+ fi
1159
+ local rfp
1160
+ rfp="$(_att_hash_text "result|${rcode}|${_ATT_RUN_MS}|${cfp}")"
1161
+ _ATT_W_kind=live_verify; _ATT_W_subject_type=card; _ATT_W_subject_id="$id"
1162
+ _ATT_W_subject_fingerprint="$fp"; _ATT_W_verdict="$verdict"
1163
+ _ATT_W_actor="$(_att_default_actor)"; _ATT_W_engine="$(_att_default_engine)"
1164
+ _ATT_W_evidence_ref=none; _ATT_W_timestamp="$(_att_now_rfc3339)"; _ATT_W_override_ref=none
1165
+ _ATT_W_owner_ref="repo:${owner_rel}"; _ATT_W_owner_fingerprint="$ofp"
1166
+ _ATT_W_subject_revision="$full"
1167
+ _ATT_W_command_fingerprint="$cfp"; _ATT_W_result_code="$rcode"
1168
+ _ATT_W_result_fingerprint="$rfp"; _ATT_W_duration_ms="$_ATT_RUN_MS"
1169
+ _ATT_W_target_id="$target"
1170
+ _ATT_W_revision_oracle_ref="repo:${oracle_rel}"
1171
+ _ATT_W_revision_oracle_fingerprint="$o_fp"
1172
+ _att_write_receipt || true
1173
+ rm -f "$ap" 2>/dev/null || true
1174
+ _att_lock_release "$lname"
1175
+ echo "PASS: live_verify $id verdict=$verdict result=$rcode"
1176
+ [ "$verdict" = "pass" ]
1177
+ }
1178
+
1179
+ _att_recover_live() {
1180
+ local id="$1"
1181
+ id="$(_att_norm_card_id "$id")" || return 2
1182
+ local ap lname
1183
+ ap="$(_att_attempt_path "$id")"
1184
+ lname="$(_att_subject_lock_name live_verify "$id")"
1185
+ _att_lock_acquire "$lname" || return 1
1186
+ # write fail receipt if none/attempt
1187
+ _ATT_W_kind=live_verify; _ATT_W_subject_type=card; _ATT_W_subject_id="$id"
1188
+ _ATT_W_subject_fingerprint="git-sha1:0"; _ATT_W_verdict=fail
1189
+ _ATT_W_actor="$(_att_default_actor)"; _ATT_W_engine="$(_att_default_engine)"
1190
+ _ATT_W_evidence_ref=none; _ATT_W_timestamp="$(_att_now_rfc3339)"; _ATT_W_override_ref=none
1191
+ _ATT_W_owner_ref=none; _ATT_W_owner_fingerprint="git-sha1:0"
1192
+ _ATT_W_subject_revision="$(_att_full_commit HEAD 2>/dev/null || echo 0)"
1193
+ _ATT_W_command_fingerprint="git-sha1:0"; _ATT_W_result_code=spawn-error
1194
+ _ATT_W_result_fingerprint="git-sha1:0"; _ATT_W_duration_ms=0
1195
+ _ATT_W_target_id=none
1196
+ _ATT_W_revision_oracle_ref=none; _ATT_W_revision_oracle_fingerprint="git-sha1:0"
1197
+ _att_write_receipt || true
1198
+ rm -f "$ap" 2>/dev/null || true
1199
+ _att_lock_release "$lname"
1200
+ echo "PASS: recovered live attempt for $id as fail"
1201
+ return 0
1202
+ }
1203
+
1204
+ # ---------- enforcement helpers ----------
1205
+ _att_warn_or_block() { # $1 message $2 force_block(0/1)
1206
+ if [ "$2" = "1" ]; then
1207
+ echo "BLOCK: $1"
1208
+ return 1
1209
+ fi
1210
+ echo "WARNING: $1 (auto inactive — manual path still usable)"
1211
+ return 0
1212
+ }
1213
+
1214
+ # Check card receipts for auto. $1 card id $2 require_live(0/1) $3 enforce(0/1)
1215
+ _att_enforce_card() {
1216
+ local id="$1" need_live="$2" enforce="$3" st
1217
+ if [ "$enforce" != "1" ]; then
1218
+ if ! _att_accepted_semantic card "$id"; then
1219
+ _att_warn_or_block "card $id semantic receipt missing/stale/red" 0
1220
+ fi
1221
+ if [ "$need_live" = "1" ] && ! _att_accepted_live "$id"; then
1222
+ _att_warn_or_block "card $id live_verify receipt missing/stale/red" 0
1223
+ fi
1224
+ return 0
1225
+ fi
1226
+ # enforcing
1227
+ st="$(_att_auto_state_read)"
1228
+ if [ "$st" = "STALE" ] || [ "$st" = "INVALID" ]; then
1229
+ echo "BLOCK: auto state is $st — run '/flow auto' to refresh or '/flow auto stop'"
1230
+ return 1
1231
+ fi
1232
+ if [ -f "$(_att_attempt_path "$id")" ]; then
1233
+ echo "BLOCK: live attempt in progress for $id — wait or '/flow attest recover $id --mark-failed'"
1234
+ return 1
1235
+ fi
1236
+ if ! _att_accepted_semantic card "$id"; then
1237
+ echo "BLOCK: card $id needs current semantic_gate receipt (pass/override)"
1238
+ return 1
1239
+ fi
1240
+ if [ "$need_live" = "1" ] && ! _att_accepted_live "$id"; then
1241
+ echo "BLOCK: card $id needs current live_verify pass receipt"
1242
+ return 1
1243
+ fi
1244
+ return 0
1245
+ }
1246
+
1247
+ # ---------- cmd_attest ----------
1248
+ cmd_attest() {
1249
+ local sub="${1:-}"; shift 2>/dev/null || true
1250
+ case "$sub" in
1251
+ semantic)
1252
+ local stage="" card="" base="" rev="" owner=""
1253
+ while [ $# -gt 0 ]; do
1254
+ case "$1" in
1255
+ --stage) shift; stage="${1:-}" ;;
1256
+ --card) shift; card="${1:-}" ;;
1257
+ --base) shift; base="${1:-}" ;;
1258
+ --revision) shift; rev="${1:-}" ;;
1259
+ --owner) shift; owner="${1:-}" ;;
1260
+ *) echo "usage: flow attest semantic --stage S --revision R --owner PATH | --card C --base B --revision R --owner PATH"; return 2 ;;
1261
+ esac
1262
+ shift 2>/dev/null || true
1263
+ done
1264
+ [ -n "$owner" ] && [ -n "$rev" ] || { echo "usage: flow attest semantic ... --revision R --owner PATH"; return 2; }
1265
+ # strip repo: prefix if given
1266
+ case "$owner" in repo:*) owner="${owner#repo:}" ;; esac
1267
+ if [ -n "$stage" ] && [ -z "$card" ]; then
1268
+ _att_mint_semantic_stage "$stage" "$rev" "$owner"; return $?
1269
+ elif [ -n "$card" ] && [ -z "$stage" ]; then
1270
+ [ -n "$base" ] || { echo "FAIL: --base required for card semantic"; return 2; }
1271
+ _att_mint_semantic_card "$card" "$base" "$rev" "$owner"; return $?
1272
+ else
1273
+ echo "FAIL: specify exactly one of --stage or --card"; return 2
1274
+ fi
1275
+ ;;
1276
+ live-verify)
1277
+ local id="${1:-}"; shift 2>/dev/null || true
1278
+ local rev="" owner=""
1279
+ while [ $# -gt 0 ]; do
1280
+ case "$1" in
1281
+ --revision) shift; rev="${1:-}" ;;
1282
+ --owner) shift; owner="${1:-}" ;;
1283
+ *) echo "usage: flow attest live-verify C-NNN --revision R --owner PATH"; return 2 ;;
1284
+ esac
1285
+ shift 2>/dev/null || true
1286
+ done
1287
+ [ -n "$id" ] && [ -n "$rev" ] && [ -n "$owner" ] || { echo "usage: flow attest live-verify C-NNN --revision R --owner PATH"; return 2; }
1288
+ case "$owner" in repo:*) owner="${owner#repo:}" ;; esac
1289
+ _att_mint_live "$id" "$rev" "$owner"; return $?
1290
+ ;;
1291
+ status)
1292
+ local target="${1:-}"
1293
+ local st
1294
+ st="$(_att_auto_state_read)"
1295
+ echo "auto: $st"
1296
+ if [ -n "${_ATT_A_activated_at:-}" ]; then
1297
+ echo " activated_at: ${_ATT_A_activated_at}"
1298
+ echo " branch: ${_ATT_A_integration_branch}"
1299
+ fi
1300
+ if [ -n "$target" ]; then
1301
+ local sid path kind
1302
+ if _att_is_stage "$target"; then
1303
+ path="$(_att_receipt_path semantic_gate stage "$target")"
1304
+ echo "semantic_gate stage $target: $(_att_receipt_status "$path" 2>/dev/null || echo missing)"
1305
+ else
1306
+ sid="$(_att_norm_card_id "$target" 2>/dev/null || echo "$target")"
1307
+ path="$(_att_receipt_path semantic_gate card "$sid")"
1308
+ echo "semantic_gate card $sid: $(_att_receipt_status "$path" 2>/dev/null || echo missing)"
1309
+ path="$(_att_receipt_path live_verify card "$sid")"
1310
+ echo "live_verify card $sid: $(_att_receipt_status "$path" 2>/dev/null || echo missing)"
1311
+ fi
1312
+ else
1313
+ echo "receipts dir: $(_att_dir)"
1314
+ ls -la "$(_att_dir)" 2>/dev/null | sed 's/^/ /' || echo " (none)"
1315
+ fi
1316
+ return 0
1317
+ ;;
1318
+ recover)
1319
+ local id="${1:-}"; shift 2>/dev/null || true
1320
+ [ "${1:-}" = "--mark-failed" ] || { echo "usage: flow attest recover C-NNN --mark-failed"; return 2; }
1321
+ _att_recover_live "$id"; return $?
1322
+ ;;
1323
+ *)
1324
+ echo "usage: flow attest semantic|live-verify|status|recover ..."
1325
+ return 2
1326
+ ;;
1327
+ esac
1328
+ }
1329
+
1330
+ # Risk preflight for auto — print all blockers; return 0 if clear
1331
+ _att_risk_preflight() {
1332
+ local f id rv any=0
1333
+ local n=0
1334
+ for f in $(_att_all_card_files); do
1335
+ n=$((n + 1))
1336
+ id="$(basename "$f" .md)"
1337
+ rv="$(_card_risk_validate "$f" auto 2>/dev/null || echo "invalid:parse")"
1338
+ case "$rv" in
1339
+ standard)
1340
+ echo "READY $id risk=standard: $(_att_field_once "$f" risk-reason | cut -c1-80)"
1341
+ ;;
1342
+ security-class)
1343
+ echo "READY $id risk=security-class: ack ok"
1344
+ ;;
1345
+ unknown)
1346
+ echo "BLOCK $id risk=unknown: classify risk + reason"
1347
+ any=1
1348
+ ;;
1349
+ halt:*)
1350
+ echo "HALT $id risk=security-class: ${rv#halt:}"
1351
+ any=1
1352
+ ;;
1353
+ invalid:*)
1354
+ echo "BLOCK $id ${rv}"
1355
+ any=1
1356
+ ;;
1357
+ *)
1358
+ echo "BLOCK $id risk=$rv"
1359
+ any=1
1360
+ ;;
1361
+ esac
1362
+ done
1363
+ [ "$n" -gt 0 ] || { echo "FAIL: no cards"; return 1; }
1364
+ [ "$any" -eq 0 ]
1365
+ }