@manhquy/flow-skill 0.4.1 → 0.5.1

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,1433 @@
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
+ # Allowed dirt: .flow/**, flow/.lock{,.d/**}, cards/C-*.md with projection==HEAD.
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
+ local line path
227
+ while IFS= read -r line; do
228
+ [ -z "$line" ] && continue
229
+ path="${line#?? }"
230
+ case "$path" in
231
+ *" -> "*) path="${path##* -> }" ;;
232
+ esac
233
+ path="${path#\"}"; path="${path%\"}"
234
+ case "$path" in
235
+ .flow|.flow/*) continue ;;
236
+ flow/.lock|flow/.lock.d|flow/.lock.d/*) continue ;;
237
+ cards/C-*.md)
238
+ _att_card_projection_matches_head "$path" || return 1
239
+ continue
240
+ ;;
241
+ *) return 1 ;;
242
+ esac
243
+ done <<EOF
244
+ $porcelain
245
+ EOF
246
+ return 0
247
+ }
248
+ _att_card_projection_matches_head() {
249
+ local rel="$1" tmp now old
250
+ [ -f "$ROOT/$rel" ] || return 1
251
+ git -C "$ROOT" cat-file -e "HEAD:${rel}" 2>/dev/null || return 1
252
+ tmp="$(mktemp 2>/dev/null || echo "${TMPDIR:-/tmp}/.att-proj.$$")"
253
+ if ! git -C "$ROOT" show "HEAD:${rel}" >"$tmp" 2>/dev/null; then
254
+ rm -f "$tmp"; return 1
255
+ fi
256
+ now="$(_att_card_contract_projection "$ROOT/$rel")"
257
+ old="$(_att_card_contract_projection "$tmp")"
258
+ rm -f "$tmp"
259
+ [ "$now" = "$old" ]
260
+ }
261
+ _att_require_clean_for_consume() {
262
+ _att_git_ok || return 0
263
+ _att_git_clean_for_attest
264
+ }
265
+
266
+ # ---------- card field parsing ----------
267
+ _att_field_count() { # $1 file $2 field
268
+ grep -cE "^${2}:" "$1" 2>/dev/null | tr -d '\r' || echo 0
269
+ }
270
+
271
+ _att_field_once() { # $1 file $2 field -> value or empty; sets _ATT_FIELD_ERR on dupe
272
+ _ATT_FIELD_ERR=""
273
+ local c v
274
+ c="$(_att_field_count "$1" "$2")"
275
+ case "$c" in
276
+ 0) printf ''; return 0 ;;
277
+ 1)
278
+ v="$(grep -m1 -E "^${2}:" "$1" 2>/dev/null | sed "s/^${2}:[[:space:]]*//" | tr -d '\r')"
279
+ printf '%s' "$v"; return 0
280
+ ;;
281
+ *) _ATT_FIELD_ERR="duplicate"; printf ''; return 1 ;;
282
+ esac
283
+ }
284
+
285
+ card_risk() {
286
+ local v
287
+ v="$(_att_field_once "$1" risk)" || { printf 'invalid'; return 1; }
288
+ [ -z "$v" ] && { printf 'unknown'; return 0; }
289
+ printf '%s' "$v"
290
+ }
291
+
292
+ card_risk_reason() {
293
+ _att_field_once "$1" risk-reason || true
294
+ }
295
+
296
+ card_risk_ack() {
297
+ local v
298
+ v="$(_att_field_once "$1" risk-ack)" || { printf 'invalid'; return 1; }
299
+ [ -z "$v" ] && { printf 'none'; return 0; }
300
+ printf '%s' "$v"
301
+ }
302
+
303
+ _att_reason_placeholder() {
304
+ case "$(_att_lc "$1")" in
305
+ ""|"[fill]"*|*"[fill]"*|"tbd"|"todo"|"n/a"|"none"|"-"|"—"|"x") return 0 ;;
306
+ esac
307
+ [ "${#1}" -lt 1 ] && return 0
308
+ [ "${#1}" -gt 256 ] && return 0
309
+ case "$1" in *$'\n'*|*$'\r'*) return 0 ;; esac
310
+ return 1
311
+ }
312
+
313
+ # mode: manual|auto -> prints normalized risk or "invalid:..." ; exit 0 ok, 1 structural
314
+ _card_risk_validate() {
315
+ local file="$1" mode="${2:-manual}" risk reason ack
316
+ if [ ! -f "$file" ]; then echo "invalid:missing-file"; return 1; fi
317
+ risk="$(card_risk "$file")" || { echo "invalid:duplicate-risk"; return 1; }
318
+ reason="$(card_risk_reason "$file" 2>/dev/null || true)"
319
+ if [ "${_ATT_FIELD_ERR:-}" = "duplicate" ]; then echo "invalid:duplicate-risk-reason"; return 1; fi
320
+ # re-count reason for duplicates
321
+ if [ "$(_att_field_count "$file" risk-reason)" -gt 1 ]; then echo "invalid:duplicate-risk-reason"; return 1; fi
322
+ ack="$(card_risk_ack "$file")" || { echo "invalid:duplicate-risk-ack"; return 1; }
323
+ if [ "$(_att_field_count "$file" risk-ack)" -gt 1 ]; then echo "invalid:duplicate-risk-ack"; return 1; fi
324
+
325
+ case "$risk" in
326
+ standard|security-class|unknown) : ;;
327
+ invalid) echo "invalid:duplicate-risk"; return 1 ;;
328
+ *) echo "invalid:risk-value"; return 1 ;;
329
+ esac
330
+ case "$ack" in
331
+ none|git:[0-9a-f]*) : ;;
332
+ invalid) echo "invalid:duplicate-risk-ack"; return 1 ;;
333
+ *)
334
+ if [ -n "$(_att_field_once "$file" risk-ack 2>/dev/null)" ]; then
335
+ case "$ack" in git:*) echo "invalid:risk-ack-shape"; return 1 ;; *) echo "invalid:risk-ack-shape"; return 1 ;; esac
336
+ fi
337
+ ;;
338
+ esac
339
+
340
+ if [ "$risk" = "unknown" ]; then
341
+ [ "$ack" = "none" ] || { echo "invalid:unknown-with-ack"; return 1; }
342
+ echo "unknown"; return 0
343
+ fi
344
+ if _att_reason_placeholder "$reason"; then echo "invalid:risk-reason"; return 1; fi
345
+ if [ "$risk" = "standard" ]; then
346
+ [ "$ack" = "none" ] || { echo "invalid:standard-with-ack"; return 1; }
347
+ echo "standard"; return 0
348
+ fi
349
+ # security-class
350
+ if [ "$mode" = "manual" ]; then
351
+ echo "security-class"; return 0
352
+ fi
353
+ # auto: full ack
354
+ if [ "$ack" = "none" ] || [ -z "$ack" ]; then echo "halt:ack-missing"; return 1; fi
355
+ case "$ack" in git:*) : ;; *) echo "halt:ack-shape"; return 1 ;; esac
356
+ local oid; oid="${ack#git:}"
357
+ if ! printf '%s' "$oid" | grep -Eq "$(_att_oid_re)"; then echo "halt:ack-oid"; return 1; fi
358
+ if ! _att_git_ok; then echo "halt:no-git"; return 1; fi
359
+ if ! git -C "$ROOT" cat-file -e "${oid}^{commit}" 2>/dev/null; then echo "halt:ack-missing-commit"; return 1; fi
360
+ local head; head="$(_att_full_commit HEAD)"
361
+ if ! _att_is_ancestor "$oid" "$head"; then echo "halt:ack-not-ancestor"; return 1; fi
362
+ local id; id="$(basename "$file" .md)"
363
+ local prefix debt_blob line_no line author_mail exec_mail
364
+ prefix="$(git -C "$ROOT" rev-parse --show-prefix 2>/dev/null || true)"
365
+ debt_blob="$(git -C "$ROOT" show "${oid}:${prefix}DEBT.md" 2>/dev/null)" || { echo "halt:ack-no-debt"; return 1; }
366
+ # exact open grammar
367
+ line="$(printf '%s\n' "$debt_blob" | grep -nE "^- \[ \] DEBT: security-class ${id} --" || true)"
368
+ if [ -z "$line" ]; then echo "halt:ack-debt-line"; return 1; fi
369
+ if [ "$(printf '%s\n' "$line" | wc -l | tr -d ' ')" -ne 1 ]; then echo "halt:ack-debt-multi"; return 1; fi
370
+ line_no="${line%%:*}"
371
+ author_mail="$(git -C "$ROOT" blame --line-porcelain "$oid" -L "${line_no},${line_no}" -- "${prefix}DEBT.md" 2>/dev/null \
372
+ | awk '/^author-mail /{print $2; exit}' | tr -d '<>' | tr 'A-Z' 'a-z' | tr -d '\r')"
373
+ 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')"
374
+ if [ -z "$author_mail" ] || [ -z "$exec_mail" ]; then echo "halt:ack-identity-ambiguous"; return 1; fi
375
+ if [ "$author_mail" = "$exec_mail" ]; then echo "halt:ack-same-identity"; return 1; fi
376
+ echo "security-class"; return 0
377
+ }
378
+
379
+ _att_all_card_files() {
380
+ ls -1 "$CARDS_DIR"/C-*.md 2>/dev/null | sort
381
+ }
382
+
383
+ _att_risk_fingerprint() {
384
+ local f id risk reason ack body=""
385
+ body=""
386
+ for f in $(_att_all_card_files); do
387
+ [ -f "$f" ] || continue
388
+ id="$(basename "$f" .md)"
389
+ risk="$(card_risk "$f" 2>/dev/null || echo invalid)"
390
+ reason="$(_att_field_once "$f" risk-reason 2>/dev/null || true)"
391
+ ack="$(card_risk_ack "$f" 2>/dev/null || echo invalid)"
392
+ body="${body}${id}|${risk}|${reason}|${ack}"$'\n'
393
+ done
394
+ local fmt oid
395
+ fmt="$(_att_object_format)"
396
+ oid="$(printf '%s' "$body" | git -C "$ROOT" hash-object --stdin 2>/dev/null | tr -d '\r\n')"
397
+ [ -n "$oid" ] || oid="0"
398
+ printf 'git-%s:%s' "$fmt" "$oid"
399
+ }
400
+
401
+ # ---------- owner manifest ----------
402
+ _att_parse_kv_file() { # $1 path — sets _ATT_KV_<key> via eval-safe names only for known keys
403
+ # shellcheck disable=SC2034
404
+ _ATT_KV_schema=""; _ATT_KV_kind=""; _ATT_KV_subject_id=""; _ATT_KV_target_id=""
405
+ _ATT_KV_command=""; _ATT_KV_revision_oracle=""
406
+ local line k v seen=""
407
+ [ -f "$1" ] || return 1
408
+ while IFS= read -r line || [ -n "$line" ]; do
409
+ line="$(printf '%s' "$line" | tr -d '\r')"
410
+ [ -z "$line" ] && continue
411
+ case "$line" in \#*) continue ;; esac
412
+ case "$line" in
413
+ *": "*) k="${line%%: *}"; v="${line#*: }" ;;
414
+ *:*) k="${line%%:*}"; v="${line#*:}"; v="${v# }" ;;
415
+ *) return 1 ;;
416
+ esac
417
+ case "$k" in
418
+ schema|kind|subject_id|target_id|command|revision_oracle) : ;;
419
+ *) return 1 ;;
420
+ esac
421
+ case " $seen " in *" $k "*) return 1 ;; esac
422
+ seen="$seen $k"
423
+ case "$k" in
424
+ schema) _ATT_KV_schema="$v" ;;
425
+ kind) _ATT_KV_kind="$v" ;;
426
+ subject_id) _ATT_KV_subject_id="$v" ;;
427
+ target_id) _ATT_KV_target_id="$v" ;;
428
+ command) _ATT_KV_command="$v" ;;
429
+ revision_oracle) _ATT_KV_revision_oracle="$v" ;;
430
+ esac
431
+ done < "$1"
432
+ [ "$_ATT_KV_schema" = "$_ATT_OWNER_SCHEMA" ] || return 1
433
+ return 0
434
+ }
435
+
436
+ _att_owner_fingerprint() { # $1 owner path relative, $2 revision
437
+ local rel="$1" rev="$2" cmd_path oracle_path mode blob o_mode o_blob body fmt oid owner_blob owner_mode
438
+ cmd_path="${_ATT_KV_command#repo:}"
439
+ owner_blob="$(git -C "$ROOT" rev-parse "${rev}:${rel}" 2>/dev/null | tr -d '\r\n')" || return 1
440
+ owner_mode="$(git -C "$ROOT" ls-tree "$rev" -- "$rel" 2>/dev/null | awk '{print $1}' | head -1)"
441
+ blob="$(git -C "$ROOT" rev-parse "${rev}:${cmd_path}" 2>/dev/null | tr -d '\r\n')" || return 1
442
+ mode="$(git -C "$ROOT" ls-tree "$rev" -- "$cmd_path" 2>/dev/null | awk '{print $1}' | head -1)"
443
+ body="owner=${rel}:${owner_mode}:${owner_blob}"$'\n'"cmd=${cmd_path}:${mode}:${blob}"$'\n'
444
+ if [ "${_ATT_KV_revision_oracle}" != "none" ] && [ -n "${_ATT_KV_revision_oracle}" ]; then
445
+ oracle_path="${_ATT_KV_revision_oracle#repo:}"
446
+ o_blob="$(git -C "$ROOT" rev-parse "${rev}:${oracle_path}" 2>/dev/null | tr -d '\r\n')" || return 1
447
+ o_mode="$(git -C "$ROOT" ls-tree "$rev" -- "$oracle_path" 2>/dev/null | awk '{print $1}' | head -1)"
448
+ body="${body}oracle=${oracle_path}:${o_mode}:${o_blob}"$'\n'
449
+ fi
450
+ body="${body}target=${_ATT_KV_target_id}"$'\n'"kind=${_ATT_KV_kind}"$'\n'
451
+ fmt="$(_att_object_format)"
452
+ oid="$(printf '%s' "$body" | git -C "$ROOT" hash-object --stdin 2>/dev/null | tr -d '\r\n')"
453
+ printf 'git-%s:%s' "$fmt" "$oid"
454
+ }
455
+
456
+ # ---------- committed-blob execution ----------
457
+ _att_path_mode_at_rev() { git -C "$ROOT" ls-tree "$1" -- "$2" 2>/dev/null | awk '{print $1}' | head -1; }
458
+ _att_blob_at_rev() { git -C "$ROOT" rev-parse "${1}:${2}" 2>/dev/null | tr -d '\r\n'; }
459
+ _att_blob_on_disk() {
460
+ local f="$ROOT/$1"
461
+ [ -f "$f" ] || return 1
462
+ [ ! -L "$f" ] || return 1
463
+ git -C "$ROOT" hash-object -- "$f" 2>/dev/null | tr -d '\r\n'
464
+ }
465
+ _att_assert_committed_regular() {
466
+ local rev="$1" rel="$2" mode blob disk
467
+ mode="$(_att_path_mode_at_rev "$rev" "$rel")"
468
+ case "$mode" in 100644|100755) : ;; *) return 1 ;; esac
469
+ blob="$(_att_blob_at_rev "$rev" "$rel")" || return 1
470
+ [ -n "$blob" ] || return 1
471
+ disk="$(_att_blob_on_disk "$rel")" || return 1
472
+ [ "$disk" = "$blob" ] || return 1
473
+ _ATT_BLOB_OID="$blob"
474
+ return 0
475
+ }
476
+ _att_materialize_blob_exec() {
477
+ local rev="$1" rel="$2" mode blob tmp
478
+ mode="$(_att_path_mode_at_rev "$rev" "$rel")"
479
+ case "$mode" in 100644|100755) : ;; *) return 1 ;; esac
480
+ blob="$(_att_blob_at_rev "$rev" "$rel")" || return 1
481
+ tmp="$(mktemp 2>/dev/null || echo "${TMPDIR:-/tmp}/.att-exec.$$.$RANDOM")"
482
+ if ! git -C "$ROOT" cat-file blob "$blob" >"$tmp" 2>/dev/null; then
483
+ rm -f "$tmp"; return 1
484
+ fi
485
+ chmod 700 "$tmp" 2>/dev/null || chmod +x "$tmp" 2>/dev/null || true
486
+ printf '%s' "$tmp"
487
+ return 0
488
+ }
489
+ _att_load_owner_at_rev() {
490
+ local rev="$1" rel="$2" cmd_rel
491
+ _att_assert_committed_regular "$rev" "$rel" || return 1
492
+ _att_parse_kv_file "$ROOT/$rel" || return 1
493
+ case "${_ATT_KV_command:-}" in repo:*) : ;; *) return 1 ;; esac
494
+ cmd_rel="${_ATT_KV_command#repo:}"
495
+ _att_assert_committed_regular "$rev" "$cmd_rel" || return 1
496
+ if [ "${_ATT_KV_revision_oracle:-none}" != "none" ] && [ -n "${_ATT_KV_revision_oracle:-}" ]; then
497
+ case "$_ATT_KV_revision_oracle" in repo:*) : ;; *) return 1 ;; esac
498
+ _att_assert_committed_regular "$rev" "${_ATT_KV_revision_oracle#repo:}" || return 1
499
+ fi
500
+ return 0
501
+ }
502
+
503
+ # ---------- projections / fingerprints ----------
504
+ _att_card_contract_projection() { # $1 card file
505
+ local f="$1"
506
+ # Extract contract fields; exclude status/Evidence
507
+ {
508
+ grep -E '^(deps|implements|risk|risk-reason|risk-ack):' "$f" 2>/dev/null || true
509
+ awk '
510
+ /^## Scope/{p=1; print; next}
511
+ /^## Allowed files/{p=1; print; next}
512
+ /^## Verify/{p=1; print; next}
513
+ /^## Done-evidence/{p=1; print; next}
514
+ /^## Evidence/{p=0; next}
515
+ /^## /{ if($0 !~ /^## (Scope|Allowed files|Verify|Done-evidence)/) p=0 }
516
+ p{print}
517
+ ' "$f" 2>/dev/null || true
518
+ } | tr -d '\r'
519
+ }
520
+
521
+ _att_stage_fingerprint() { # $1 stage $2 revision -> fp (also sets _ATT_LAST_FP)
522
+ local stage="$1" rev="$2" path blob fmt
523
+ path="flow/${stage}.md"
524
+ blob="$(git -C "$ROOT" rev-parse "${rev}:${path}" 2>/dev/null | tr -d '\r\n')" || return 1
525
+ fmt="$(_att_object_format)"
526
+ _ATT_LAST_FP="$(printf 'git-%s:%s' "$fmt" "$blob")"
527
+ printf '%s' "$_ATT_LAST_FP"
528
+ }
529
+
530
+ _att_card_semantic_fingerprint() { # $1 card file $2 base $3 head -> prints fp; sets _ATT_SUBJ_*
531
+ local file="$1" base="$2" head="$3" id proj tree body fmt oid path_manifest
532
+ id="$(basename "$file" .md)"
533
+ base="$(_att_full_commit "$base")" || return 1
534
+ head="$(_att_full_commit "$head")" || return 1
535
+ tree="$(git -C "$ROOT" rev-parse "${head}^{tree}" 2>/dev/null | tr -d '\r\n')" || return 1
536
+ proj="$(_att_card_contract_projection "$file")"
537
+ # changed paths base..head excluding card path
538
+ path_manifest="$(git -C "$ROOT" diff-tree -r --no-commit-id --name-status "$base" "$head" 2>/dev/null \
539
+ | awk -v card="cards/${id}.md" '$2 != card && $3 != card {print}' \
540
+ | while read -r st p rest; do
541
+ case "$st" in
542
+ R*|C*) p="$rest" ;;
543
+ esac
544
+ [ -z "$p" ] && continue
545
+ mode_oid="$(git -C "$ROOT" ls-tree "$head" -- "$p" 2>/dev/null | awk '{print $1" "$3}')"
546
+ printf '%s %s\n' "$p" "$mode_oid"
547
+ done | sort)"
548
+ body="base=${base}"$'\n'"head=${head}"$'\n'"tree=${tree}"$'\n'"proj<<"$'\n'"${proj}"$'\n'">>"$'\n'"paths<<"$'\n'"${path_manifest}"$'\n'">>"$'\n'
549
+ fmt="$(_att_object_format)"
550
+ oid="$(printf '%s' "$body" | git -C "$ROOT" hash-object --stdin 2>/dev/null | tr -d '\r\n')"
551
+ _ATT_SUBJ_BASE="$base"
552
+ _ATT_SUBJ_REV="$head"
553
+ _ATT_SUBJ_TREE="$tree"
554
+ _ATT_LAST_FP="$(printf 'git-%s:%s' "$fmt" "$oid")"
555
+ printf '%s' "$_ATT_LAST_FP"
556
+ }
557
+
558
+ # ---------- receipt parse / write ----------
559
+ _att_receipt_parse() { # $1 path — sets _ATT_R_* ; return 0 valid schema structure
560
+ local f="$1" line k v seen="" n=0 size
561
+ _ATT_R_schema=""; _ATT_R_kind=""; _ATT_R_subject_type=""; _ATT_R_subject_id=""
562
+ _ATT_R_subject_fingerprint=""; _ATT_R_verdict=""; _ATT_R_actor=""; _ATT_R_engine=""
563
+ _ATT_R_evidence_ref=""; _ATT_R_timestamp=""; _ATT_R_override_ref=""
564
+ _ATT_R_owner_ref=""; _ATT_R_owner_fingerprint=""
565
+ _ATT_R_subject_revision=""; _ATT_R_subject_base=""; _ATT_R_subject_tree=""
566
+ _ATT_R_command_fingerprint=""; _ATT_R_result_code=""; _ATT_R_result_fingerprint=""
567
+ _ATT_R_duration_ms=""; _ATT_R_target_id=""
568
+ _ATT_R_revision_oracle_ref=""; _ATT_R_revision_oracle_fingerprint=""
569
+ [ -f "$f" ] || return 1
570
+ size="$(wc -c < "$f" 2>/dev/null | tr -d ' ')"
571
+ [ "${size:-0}" -le 16384 ] || return 1
572
+ while IFS= read -r line || [ -n "$line" ]; do
573
+ line="$(printf '%s' "$line" | tr -d '\r')"
574
+ [ -z "$line" ] && continue
575
+ n=$((n + 1))
576
+ [ "$n" -gt 64 ] && return 1
577
+ case "$line" in
578
+ *": "*) k="${line%%: *}"; v="${line#*: }" ;;
579
+ *) return 1 ;;
580
+ esac
581
+ case "$k" in
582
+ 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) : ;;
583
+ *) return 1 ;;
584
+ esac
585
+ case " $seen " in *" $k "*) return 1 ;; esac
586
+ seen="$seen $k"
587
+ case "$v" in *$'\n'*|*$'\t'*) return 1 ;; esac
588
+ [ "${#v}" -le 512 ] || return 1
589
+ case "$k" in
590
+ schema) _ATT_R_schema="$v" ;;
591
+ kind) _ATT_R_kind="$v" ;;
592
+ subject_type) _ATT_R_subject_type="$v" ;;
593
+ subject_id) _ATT_R_subject_id="$v" ;;
594
+ subject_fingerprint) _ATT_R_subject_fingerprint="$v" ;;
595
+ verdict) _ATT_R_verdict="$v" ;;
596
+ actor) _ATT_R_actor="$v" ;;
597
+ engine) _ATT_R_engine="$v" ;;
598
+ evidence_ref) _ATT_R_evidence_ref="$v" ;;
599
+ timestamp) _ATT_R_timestamp="$v" ;;
600
+ override_ref) _ATT_R_override_ref="$v" ;;
601
+ owner_ref) _ATT_R_owner_ref="$v" ;;
602
+ owner_fingerprint) _ATT_R_owner_fingerprint="$v" ;;
603
+ subject_revision) _ATT_R_subject_revision="$v" ;;
604
+ subject_base) _ATT_R_subject_base="$v" ;;
605
+ subject_tree) _ATT_R_subject_tree="$v" ;;
606
+ command_fingerprint) _ATT_R_command_fingerprint="$v" ;;
607
+ result_code) _ATT_R_result_code="$v" ;;
608
+ result_fingerprint) _ATT_R_result_fingerprint="$v" ;;
609
+ duration_ms) _ATT_R_duration_ms="$v" ;;
610
+ target_id) _ATT_R_target_id="$v" ;;
611
+ revision_oracle_ref) _ATT_R_revision_oracle_ref="$v" ;;
612
+ revision_oracle_fingerprint) _ATT_R_revision_oracle_fingerprint="$v" ;;
613
+ esac
614
+ done < "$f"
615
+ [ "$_ATT_R_schema" = "$_ATT_SCHEMA" ] || return 1
616
+ case "$_ATT_R_kind" in semantic_gate|live_verify) : ;; *) return 1 ;; esac
617
+ case "$_ATT_R_subject_type" in stage|card) : ;; *) return 1 ;; esac
618
+ _att_valid_subject_id "$_ATT_R_subject_id" || return 1
619
+ case "$_ATT_R_verdict" in pass|fail|override) : ;; *) return 1 ;; esac
620
+ [ "$_ATT_R_kind" = "live_verify" ] && [ "$_ATT_R_verdict" = "override" ] && return 1
621
+ _att_valid_actor "$_ATT_R_actor" || return 1
622
+ _att_valid_actor "$_ATT_R_engine" || return 1
623
+ _att_valid_repo_ref "$_ATT_R_evidence_ref" || return 1
624
+ _att_valid_repo_ref "$_ATT_R_owner_ref" || return 1
625
+ [ -n "$_ATT_R_subject_fingerprint" ] && [ -n "$_ATT_R_owner_fingerprint" ] && [ -n "$_ATT_R_timestamp" ] || return 1
626
+ return 0
627
+ }
628
+
629
+ _att_atomic_write() { # $1 dest $2 content
630
+ local dest="$1" dir tmp
631
+ dir="$(dirname "$dest")"
632
+ mkdir -p "$dir" 2>/dev/null || return 1
633
+ tmp="$dir/.tmp.$$.$(_att_now_s)"
634
+ printf '%s' "$2" > "$tmp" || { rm -f "$tmp"; return 1; }
635
+ chmod 600 "$tmp" 2>/dev/null || true
636
+ mv -f "$tmp" "$dest" || { rm -f "$tmp"; return 1; }
637
+ return 0
638
+ }
639
+
640
+ _att_write_receipt() { # builds from _ATT_W_* vars
641
+ local body dest kind st sid
642
+ kind="$_ATT_W_kind"; st="$_ATT_W_subject_type"; sid="$_ATT_W_subject_id"
643
+ dest="$(_att_receipt_path "$kind" "$st" "$sid")" || return 1
644
+ body="schema: ${_ATT_SCHEMA}
645
+ kind: ${kind}
646
+ subject_type: ${st}
647
+ subject_id: ${sid}
648
+ subject_fingerprint: ${_ATT_W_subject_fingerprint}
649
+ verdict: ${_ATT_W_verdict}
650
+ actor: ${_ATT_W_actor}
651
+ engine: ${_ATT_W_engine}
652
+ evidence_ref: ${_ATT_W_evidence_ref}
653
+ timestamp: ${_ATT_W_timestamp}
654
+ override_ref: ${_ATT_W_override_ref}
655
+ owner_ref: ${_ATT_W_owner_ref}
656
+ owner_fingerprint: ${_ATT_W_owner_fingerprint}
657
+ "
658
+ if [ "$kind" = "semantic_gate" ] && [ "$st" = "stage" ]; then
659
+ body="${body}subject_revision: ${_ATT_W_subject_revision}
660
+ "
661
+ fi
662
+ if [ "$kind" = "semantic_gate" ] && [ "$st" = "card" ]; then
663
+ body="${body}subject_base: ${_ATT_W_subject_base}
664
+ subject_revision: ${_ATT_W_subject_revision}
665
+ subject_tree: ${_ATT_W_subject_tree}
666
+ "
667
+ fi
668
+ if [ "$kind" = "live_verify" ]; then
669
+ body="${body}subject_revision: ${_ATT_W_subject_revision}
670
+ command_fingerprint: ${_ATT_W_command_fingerprint}
671
+ result_code: ${_ATT_W_result_code}
672
+ result_fingerprint: ${_ATT_W_result_fingerprint}
673
+ duration_ms: ${_ATT_W_duration_ms}
674
+ target_id: ${_ATT_W_target_id}
675
+ revision_oracle_ref: ${_ATT_W_revision_oracle_ref}
676
+ revision_oracle_fingerprint: ${_ATT_W_revision_oracle_fingerprint}
677
+ "
678
+ fi
679
+ _att_atomic_write "$dest" "$body"
680
+ }
681
+
682
+ # ---------- process supervisor ----------
683
+ _att_supervisor_capable() {
684
+ if [ "${FLOW_ATTEST_SUPERVISOR:-}" = "force-unsupported" ]; then return 1; fi
685
+ if [ "${FLOW_ATTEST_SUPERVISOR:-}" = "force-ok" ]; then return 0; fi
686
+ command -v setsid >/dev/null 2>&1 || return 1
687
+ if [ "${_ATT_SUP_CAP_CACHED:-}" = "1" ]; then
688
+ [ "${_ATT_SUP_CAP_OK:-0}" = "1" ]; return $?
689
+ fi
690
+ _ATT_SUP_CAP_CACHED=1; _ATT_SUP_CAP_OK=0
691
+ local outf pid pgid
692
+ outf="$(mktemp 2>/dev/null || echo "${TMPDIR:-/tmp}/.att-cap.$$")"
693
+ : > "$outf"
694
+ ( cd "${ROOT:-/tmp}" || exit 127; exec setsid sleep 30 ) >"$outf" 2>&1 &
695
+ pid=$!
696
+ sleep 0.15 2>/dev/null || true
697
+ pgid="$(ps -o pgid= -p "$pid" 2>/dev/null | tr -d ' ')"
698
+ [ -n "$pgid" ] || pgid="$pid"
699
+ kill -TERM -"$pgid" 2>/dev/null || kill -TERM "$pid" 2>/dev/null || true
700
+ sleep 0.25 2>/dev/null || true
701
+ if kill -0 "$pid" 2>/dev/null; then
702
+ kill -KILL -"$pgid" 2>/dev/null || kill -KILL "$pid" 2>/dev/null || true
703
+ sleep 0.1 2>/dev/null || true
704
+ fi
705
+ wait "$pid" 2>/dev/null || true
706
+ rm -f "$outf" 2>/dev/null || true
707
+ kill -0 "$pid" 2>/dev/null && return 1
708
+ _ATT_SUP_CAP_OK=1
709
+ return 0
710
+ }
711
+ _att_run_supervised() {
712
+ local timeout_s="${FLOW_ATTEST_TIMEOUT_S:-30}" cap="${FLOW_ATTEST_OUT_CAP:-65536}"
713
+ local comb="${FLOW_ATTEST_COMBINED_CAP:-98304}" grace_s="${FLOW_ATTEST_GRACE_S:-2}"
714
+ local outf errf start end rc=0 pid="" pgid="" i
715
+ _ATT_RUN_RC="spawn-error"; _ATT_RUN_CODE=127; _ATT_RUN_OUT=""; _ATT_RUN_MS=0
716
+ if ! _att_supervisor_capable; then _ATT_RUN_RC="spawn-error"; return 1; fi
717
+ case "$timeout_s" in ''|*[!0-9]*|0) timeout_s=30 ;; esac
718
+ case "$cap" in ''|*[!0-9]*|0) cap=65536 ;; esac
719
+ case "$comb" in ''|*[!0-9]*|0) comb=98304 ;; esac
720
+ case "$grace_s" in ''|*[!0-9]*) grace_s=2 ;; esac
721
+ outf="$(mktemp 2>/dev/null || echo "${TMPDIR:-/tmp}/.att-out.$$")"
722
+ errf="$(mktemp 2>/dev/null || echo "${TMPDIR:-/tmp}/.att-err.$$")"
723
+ : > "$outf"; : > "$errf"
724
+ start="$(_att_now_s)"
725
+ ( cd "$ROOT" || exit 127; exec setsid "$@" ) >"$outf" 2>"$errf" &
726
+ pid=$!
727
+ i=0
728
+ while [ "$i" -lt 20 ]; do
729
+ if kill -0 "$pid" 2>/dev/null; then
730
+ pgid="$(ps -o pgid= -p "$pid" 2>/dev/null | tr -d ' ')"
731
+ [ -n "$pgid" ] && break
732
+ fi
733
+ i=$((i+1)); sleep 0.05 2>/dev/null || true
734
+ done
735
+ [ -n "$pgid" ] || pgid="$pid"
736
+ ( sleep "$timeout_s" 2>/dev/null
737
+ if kill -0 "$pid" 2>/dev/null; then
738
+ kill -TERM -"$pgid" 2>/dev/null || kill -TERM "$pid" 2>/dev/null || true
739
+ sleep "$grace_s" 2>/dev/null
740
+ kill -KILL -"$pgid" 2>/dev/null || kill -KILL "$pid" 2>/dev/null || true
741
+ echo timeout > "${outf}.to"
742
+ fi ) &
743
+ local wd=$!
744
+ ( while kill -0 "$pid" 2>/dev/null; do
745
+ local os es
746
+ os="$(wc -c < "$outf" 2>/dev/null | tr -d ' ')"
747
+ es="$(wc -c < "$errf" 2>/dev/null | tr -d ' ')"
748
+ if [ "${os:-0}" -gt "$cap" ] || [ "${es:-0}" -gt "$cap" ] || [ $(( ${os:-0} + ${es:-0} )) -gt "$comb" ]; then
749
+ kill -TERM -"$pgid" 2>/dev/null || kill -TERM "$pid" 2>/dev/null || true
750
+ sleep 1
751
+ kill -KILL -"$pgid" 2>/dev/null || kill -KILL "$pid" 2>/dev/null || true
752
+ echo cap > "${outf}.cap"; break
753
+ fi
754
+ sleep 0.1 2>/dev/null || sleep 1
755
+ done ) &
756
+ local capw=$!
757
+ wait "$pid" 2>/dev/null; rc=$?
758
+ kill "$wd" 2>/dev/null; wait "$wd" 2>/dev/null
759
+ kill "$capw" 2>/dev/null; wait "$capw" 2>/dev/null
760
+ kill -KILL -"$pgid" 2>/dev/null || true
761
+ end="$(_att_now_s)"
762
+ _ATT_RUN_MS=$(( (end - start) * 1000 )); [ "$_ATT_RUN_MS" -lt 0 ] && _ATT_RUN_MS=0
763
+ if [ -f "${outf}.cap" ]; then _ATT_RUN_RC="output-cap"; _ATT_RUN_CODE=1; rm -f "${outf}.cap"
764
+ elif [ -f "${outf}.to" ]; then _ATT_RUN_RC="timeout"; _ATT_RUN_CODE=124; rm -f "${outf}.to"
765
+ elif [ "$rc" -eq 137 ] || [ "$rc" -eq 143 ]; then
766
+ if [ $((end - start)) -ge "$timeout_s" ]; then _ATT_RUN_RC="timeout"; _ATT_RUN_CODE=124
767
+ else _ATT_RUN_RC="signal"; _ATT_RUN_CODE="$rc"; fi
768
+ elif [ "$rc" -eq 127 ]; then _ATT_RUN_RC="spawn-error"; _ATT_RUN_CODE=127
769
+ else _ATT_RUN_RC="exit"; _ATT_RUN_CODE="$rc"; fi
770
+ _ATT_RUN_OUT="$(head -c "$cap" "$outf" 2>/dev/null | tr -d '\000')"
771
+ rm -f "$outf" "$errf" 2>/dev/null || true
772
+ return 0
773
+ }
774
+
775
+ _att_result_code_str() {
776
+ case "$_ATT_RUN_RC" in
777
+ exit) printf 'exit:%s' "$_ATT_RUN_CODE" ;;
778
+ timeout) printf 'timeout' ;;
779
+ output-cap) printf 'output-cap' ;;
780
+ signal) printf 'signal:%s' "$_ATT_RUN_CODE" ;;
781
+ *) printf 'spawn-error' ;;
782
+ esac
783
+ }
784
+
785
+ _att_hash_text() {
786
+ local fmt oid
787
+ fmt="$(_att_object_format)"
788
+ oid="$(printf '%s' "$1" | git -C "$ROOT" hash-object --stdin 2>/dev/null | tr -d '\r\n')"
789
+ printf 'git-%s:%s' "$fmt" "${oid:-0}"
790
+ }
791
+
792
+ # ---------- receipt currentness ----------
793
+ _att_receipt_status() { # $1 path -> current|missing|stale|invalid|red|blocked-attempt
794
+ local path="$1"
795
+ if [ ! -f "$path" ]; then echo "missing"; return 1; fi
796
+ if ! _att_receipt_parse "$path"; then echo "invalid"; return 1; fi
797
+ if [ "$_ATT_R_verdict" = "fail" ]; then echo "red"; return 1; fi
798
+ # attempt marker for live
799
+ if [ "$_ATT_R_kind" = "live_verify" ]; then
800
+ local ap; ap="$(_att_attempt_path "$_ATT_R_subject_id")"
801
+ if [ -f "$ap" ]; then echo "blocked-attempt"; return 1; fi
802
+ fi
803
+ # recompute subject fingerprint loosely
804
+ case "$_ATT_R_kind:$_ATT_R_subject_type" in
805
+ semantic_gate:stage)
806
+ local cur
807
+ if ! cur="$(_att_stage_fingerprint "$_ATT_R_subject_id" "${_ATT_R_subject_revision:-HEAD}")"; then
808
+ echo "stale"; return 1
809
+ fi
810
+ # compare to current HEAD blob too
811
+ local headfp
812
+ headfp="$(_att_stage_fingerprint "$_ATT_R_subject_id" HEAD 2>/dev/null || true)"
813
+ if [ "$_ATT_R_subject_fingerprint" != "$cur" ] && [ "$_ATT_R_subject_fingerprint" != "$headfp" ]; then
814
+ echo "stale"; return 1
815
+ fi
816
+ # if HEAD blob differs from recorded revision blob, stale unless same
817
+ if [ -n "$headfp" ] && [ "$headfp" != "$_ATT_R_subject_fingerprint" ]; then
818
+ echo "stale"; return 1
819
+ fi
820
+ if ! _att_require_clean_for_consume; then echo "stale"; return 1; fi
821
+ ;;
822
+ semantic_gate:card)
823
+ local file; file="$(resolve_card_file "$_ATT_R_subject_id")"
824
+ [ -f "$file" ] || { echo "stale"; return 1; }
825
+ local head; head="$(_att_full_commit HEAD)"
826
+ [ -n "$head" ] && [ -n "$_ATT_R_subject_revision" ] && [ -n "$_ATT_R_subject_base" ] || { echo "stale"; return 1; }
827
+ # Same revision: recompute fingerprint against base..rev (must match receipt).
828
+ if [ "$head" = "$_ATT_R_subject_revision" ]; then
829
+ if ! _att_card_semantic_fingerprint "$file" "$_ATT_R_subject_base" "$_ATT_R_subject_revision" >/dev/null; then
830
+ echo "stale"; return 1
831
+ fi
832
+ [ "$_ATT_R_subject_fingerprint" = "$_ATT_LAST_FP" ] || { echo "stale"; return 1; }
833
+ else
834
+ # Ancestry-preserving integration path: always revalidate projection + path survival.
835
+ if ! _att_is_ancestor "$_ATT_R_subject_revision" "$head"; then
836
+ echo "stale"; return 1
837
+ fi
838
+ local proj_now proj_old tmpc
839
+ proj_now="$(_att_card_contract_projection "$file")"
840
+ tmpc="$(mktemp 2>/dev/null || echo "${TMPDIR:-/tmp}/.att-card.$$")"
841
+ if ! git -C "$ROOT" show "${_ATT_R_subject_revision}:cards/${_ATT_R_subject_id}.md" >"$tmpc" 2>/dev/null; then
842
+ rm -f "$tmpc"; echo "stale"; return 1
843
+ fi
844
+ proj_old="$(_att_card_contract_projection "$tmpc")"
845
+ rm -f "$tmpc"
846
+ [ "$proj_now" = "$proj_old" ] || { echo "stale"; return 1; }
847
+ if ! _att_card_paths_survive "$_ATT_R_subject_base" "$_ATT_R_subject_revision" "$head" "$_ATT_R_subject_id"; then
848
+ echo "stale"; return 1
849
+ fi
850
+ fi
851
+ if ! _att_require_clean_for_consume; then echo "stale"; return 1; fi
852
+ ;;
853
+ live_verify:card)
854
+ [ "$_ATT_R_verdict" = "pass" ] || { echo "red"; return 1; }
855
+ local head file proj cur_fp ofp_now owner_rel
856
+ head="$(_att_full_commit HEAD)"
857
+ file="$(resolve_card_file "$_ATT_R_subject_id")"
858
+ [ -f "$file" ] || { echo "stale"; return 1; }
859
+ [ -n "$_ATT_R_subject_revision" ] || { echo "stale"; return 1; }
860
+ [ "$_ATT_R_subject_revision" = "$head" ] || { echo "stale"; return 1; }
861
+ case "${_ATT_R_owner_ref:-}" in repo:*) owner_rel="${_ATT_R_owner_ref#repo:}" ;; *) echo "stale"; return 1 ;; esac
862
+ if ! _att_load_owner_at_rev "$head" "$owner_rel"; then echo "stale"; return 1; fi
863
+ ofp_now="$(_att_owner_fingerprint "$owner_rel" "$head")" || { echo "stale"; return 1; }
864
+ [ "$ofp_now" = "${_ATT_R_owner_fingerprint}" ] || { echo "stale"; return 1; }
865
+ 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')"
866
+ cur_fp="$(_att_hash_text "live|${head}|${_ATT_R_target_id}|${ofp_now}|${proj}")"
867
+ [ "$cur_fp" = "$_ATT_R_subject_fingerprint" ] || { echo "stale"; return 1; }
868
+ if ! _att_require_clean_for_consume; then echo "stale"; return 1; fi
869
+ ;;
870
+ esac
871
+ case "$_ATT_R_verdict" in
872
+ pass|override) echo "current"; return 0 ;;
873
+ fail) echo "red"; return 1 ;;
874
+ esac
875
+ echo "invalid"; return 1
876
+ }
877
+
878
+ _att_card_paths_survive() { # base rev head cardid
879
+ local base="$1" rev="$2" head="$3" id="$4" p mode_rev mode_head oid_rev oid_head
880
+ # name-only: each line is the path (not name-status $2)
881
+ while IFS= read -r p; do
882
+ p="$(printf '%s' "$p" | tr -d '\r')"
883
+ [ -z "$p" ] && continue
884
+ case "$p" in "cards/${id}.md") continue ;; esac
885
+ mode_rev="$(git -C "$ROOT" ls-tree "$rev" -- "$p" 2>/dev/null | awk '{print $1}')"
886
+ oid_rev="$(git -C "$ROOT" ls-tree "$rev" -- "$p" 2>/dev/null | awk '{print $3}')"
887
+ mode_head="$(git -C "$ROOT" ls-tree "$head" -- "$p" 2>/dev/null | awk '{print $1}')"
888
+ oid_head="$(git -C "$ROOT" ls-tree "$head" -- "$p" 2>/dev/null | awk '{print $3}')"
889
+ [ -n "$mode_rev" ] && [ -n "$oid_rev" ] || return 1
890
+ [ "$mode_rev" = "$mode_head" ] && [ "$oid_rev" = "$oid_head" ] || return 1
891
+ done <<EOF
892
+ $(git -C "$ROOT" diff-tree -r --name-only "$base" "$rev" 2>/dev/null)
893
+ EOF
894
+ return 0
895
+ }
896
+
897
+ _att_accepted_semantic() { # subject_type subject_id — exit 0 if current pass|override
898
+ local st="$1" sid="$2" path
899
+ path="$(_att_receipt_path semantic_gate "$st" "$sid")"
900
+ # Do not use command substitution: _att_receipt_status sets _ATT_R_* in this shell.
901
+ if ! _att_receipt_status "$path" >/dev/null 2>&1; then return 1; fi
902
+ case "${_ATT_R_verdict:-}" in pass|override) return 0 ;; *) return 1 ;; esac
903
+ }
904
+
905
+ _att_accepted_live() { # card id
906
+ local path
907
+ path="$(_att_receipt_path live_verify card "$1")"
908
+ if [ -f "$(_att_attempt_path "$1")" ]; then return 1; fi
909
+ if ! _att_receipt_status "$path" >/dev/null 2>&1; then return 1; fi
910
+ [ "${_ATT_R_verdict:-}" = "pass" ]
911
+ }
912
+
913
+ # ---------- auto state ----------
914
+ _att_auto_state_read() { # sets _ATT_A_*; echoes INACTIVE|ACTIVE|STALE|INVALID
915
+ local p; p="$(_att_auto_state_path)"
916
+ _ATT_A_schema=""; _ATT_A_status=""; _ATT_A_activated_at=""; _ATT_A_activated_by=""
917
+ _ATT_A_integration_branch=""; _ATT_A_risk_fingerprint=""; _ATT_A_contract_fingerprint=""
918
+ if [ ! -f "$p" ]; then echo "INACTIVE"; return 0; fi
919
+ if ! _att_parse_auto_file "$p"; then echo "INVALID"; return 0; fi
920
+ # revalidate
921
+ if ! _att_auto_current_ok; then echo "STALE"; return 0; fi
922
+ echo "ACTIVE"; return 0
923
+ }
924
+
925
+ _att_parse_auto_file() {
926
+ local f="$1" line k v seen=""
927
+ while IFS= read -r line || [ -n "$line" ]; do
928
+ line="$(printf '%s' "$line" | tr -d '\r')"
929
+ [ -z "$line" ] && continue
930
+ case "$line" in
931
+ *": "*) k="${line%%: *}"; v="${line#*: }" ;;
932
+ *) return 1 ;;
933
+ esac
934
+ case "$k" in
935
+ schema|status|activated_at|activated_by|integration_branch|risk_fingerprint|contract_fingerprint) : ;;
936
+ *) return 1 ;;
937
+ esac
938
+ case " $seen " in *" $k "*) return 1 ;; esac
939
+ seen="$seen $k"
940
+ case "$k" in
941
+ schema) _ATT_A_schema="$v" ;;
942
+ status) _ATT_A_status="$v" ;;
943
+ activated_at) _ATT_A_activated_at="$v" ;;
944
+ activated_by) _ATT_A_activated_by="$v" ;;
945
+ integration_branch) _ATT_A_integration_branch="$v" ;;
946
+ risk_fingerprint) _ATT_A_risk_fingerprint="$v" ;;
947
+ contract_fingerprint) _ATT_A_contract_fingerprint="$v" ;;
948
+ esac
949
+ done < "$f"
950
+ [ "$_ATT_A_schema" = "$_ATT_AUTO_SCHEMA" ] || return 1
951
+ [ "$_ATT_A_status" = "active" ] || return 1
952
+ return 0
953
+ }
954
+
955
+ _att_auto_current_ok() {
956
+ _att_git_ok || return 1
957
+ local br head rfp cfp f rv
958
+ br="$(git -C "$ROOT" rev-parse --abbrev-ref HEAD 2>/dev/null | tr -d '\r')"
959
+ [ "$br" = "$_ATT_A_integration_branch" ] || return 1
960
+ [ "$br" != "HEAD" ] || return 1
961
+ # risk set revalidation
962
+ for f in $(_att_all_card_files); do
963
+ rv="$(_card_risk_validate "$f" auto 2>/dev/null || echo bad)"
964
+ case "$rv" in standard|security-class) : ;; *) return 1 ;; esac
965
+ done
966
+ rfp="$(_att_risk_fingerprint)"
967
+ [ "$rfp" = "$_ATT_A_risk_fingerprint" ] || return 1
968
+ # stage 05 receipt
969
+ _att_accepted_semantic stage 05-contract || return 1
970
+ cfp="$_ATT_R_subject_fingerprint"
971
+ [ "$cfp" = "$_ATT_A_contract_fingerprint" ] || return 1
972
+ return 0
973
+ }
974
+
975
+ _att_auto_write() {
976
+ local path body
977
+ path="$(_att_auto_state_path)"
978
+ mkdir -p "$(dirname "$path")" 2>/dev/null || true
979
+ body="schema: ${_ATT_AUTO_SCHEMA}
980
+ status: active
981
+ activated_at: ${_ATT_W_activated_at}
982
+ activated_by: ${_ATT_W_activated_by}
983
+ integration_branch: ${_ATT_W_integration_branch}
984
+ risk_fingerprint: ${_ATT_W_risk_fingerprint}
985
+ contract_fingerprint: ${_ATT_W_contract_fingerprint}
986
+ "
987
+ _att_atomic_write "$path" "$body"
988
+ }
989
+
990
+ _att_auto_clear() {
991
+ rm -f "$(_att_auto_state_path)" 2>/dev/null || true
992
+ }
993
+
994
+ _att_auto_is_enforcing() {
995
+ # File existence is the policy latch; STALE/INVALID still hard-enforce (plan matrix).
996
+ local p; p="$(_att_auto_state_path)"
997
+ [ -f "$p" ]
998
+ }
999
+
1000
+ _att_is_main_worktree() {
1001
+ _att_git_ok || return 1
1002
+ local git_dir common_dir
1003
+ git_dir="$(git -C "$ROOT" rev-parse --absolute-git-dir 2>/dev/null || true)"
1004
+ common_dir="$(git -C "$ROOT" rev-parse --git-common-dir 2>/dev/null || true)"
1005
+ case "$common_dir" in /*) : ;; "") return 1 ;; *)
1006
+ common_dir="$(cd "$ROOT" && cd "$common_dir" 2>/dev/null && pwd)"
1007
+ ;; esac
1008
+ [ -n "$git_dir" ] && [ -n "$common_dir" ] || return 1
1009
+ # linked when git_dir != common_dir
1010
+ [ "$(cd "$(dirname "$git_dir")" 2>/dev/null && pwd -P)/$(basename "$git_dir")" = \
1011
+ "$(cd "$(dirname "$common_dir")" 2>/dev/null && pwd -P)/$(basename "$common_dir")" ] 2>/dev/null \
1012
+ || {
1013
+ # compare realpaths
1014
+ local a b
1015
+ a="$(cd "$git_dir" 2>/dev/null && pwd -P || echo "$git_dir")"
1016
+ b="$(cd "$common_dir" 2>/dev/null && pwd -P || echo "$common_dir")"
1017
+ [ "$a" = "$b" ]
1018
+ }
1019
+ }
1020
+
1021
+ _att_main_worktree_path() {
1022
+ git -C "$ROOT" worktree list --porcelain 2>/dev/null | awk '/^worktree /{print substr($0,10); exit}'
1023
+ }
1024
+
1025
+ # ---------- mint commands ----------
1026
+ _att_default_actor() {
1027
+ local a
1028
+ a="$(_att_sanitize_label "${FLOW_SESSION_ID:-flow}")"
1029
+ [ -n "$a" ] || a="flow"
1030
+ printf '%s' "$a"
1031
+ }
1032
+
1033
+ _att_default_engine() {
1034
+ local e
1035
+ e="$(_att_sanitize_label "${FLOW_ENGINE_TIER:-builtin}")"
1036
+ [ -n "$e" ] || e="builtin"
1037
+ printf '%s' "$e"
1038
+ }
1039
+
1040
+ _att_mint_semantic_stage() {
1041
+ local stage="$1" rev="$2" owner_rel="$3"
1042
+ local full owner_abs fp ofp out_doc verdict crit high eref
1043
+ _att_is_stage "$stage" || { echo "FAIL: invalid stage '$stage'"; return 2; }
1044
+ _att_git_ok || { echo "FAIL: attest requires a Git repository"; return 2; }
1045
+ full="$(_att_full_commit "$rev")" || { echo "FAIL: bad revision"; return 2; }
1046
+ _att_git_clean_for_attest || { echo "FAIL: dirty worktree (only .flow/, locks, status/Evidence allowed)"; return 1; }
1047
+ if ! _att_load_owner_at_rev "$full" "$owner_rel"; then
1048
+ echo "FAIL: owner/command must be committed regular files matching disk at revision (blob equality)"
1049
+ return 1
1050
+ fi
1051
+ [ "$_ATT_KV_kind" = "semantic_gate" ] || { echo "FAIL: owner kind must be semantic_gate"; return 2; }
1052
+ [ "$_ATT_KV_subject_id" = "$stage" ] || { echo "FAIL: owner subject_id mismatch"; return 2; }
1053
+ [ "$_ATT_KV_target_id" = "none" ] && [ "$_ATT_KV_revision_oracle" = "none" ] || { echo "FAIL: semantic owner must have target/oracle none"; return 2; }
1054
+ local cmd_rel="${_ATT_KV_command#repo:}"
1055
+ if ! _att_stage_fingerprint "$stage" "$full" >/dev/null; then echo "FAIL: stage fingerprint"; return 1; fi
1056
+ fp="$_ATT_LAST_FP"
1057
+ ofp="$(_att_owner_fingerprint "$owner_rel" "$full")" || { echo "FAIL: owner fingerprint"; return 1; }
1058
+
1059
+ local lname execf; lname="$(_att_subject_lock_name semantic_gate "$stage")"
1060
+ _att_lock_acquire "$lname" || return 1
1061
+ execf="$(_att_materialize_blob_exec "$full" "$cmd_rel")" || {
1062
+ _att_lock_release "$lname"; echo "FAIL: could not materialize committed producer"; return 1
1063
+ }
1064
+ _att_run_supervised "$execf" --subject-id "$stage" --subject-revision "$full" --subject-fingerprint "$fp"
1065
+ rm -f "$execf" 2>/dev/null || true
1066
+ if [ "$_ATT_RUN_RC" != "exit" ] || [ "$_ATT_RUN_CODE" -ne 0 ]; then
1067
+ _ATT_W_kind=semantic_gate; _ATT_W_subject_type=stage; _ATT_W_subject_id="$stage"
1068
+ _ATT_W_subject_fingerprint="$fp"; _ATT_W_verdict=fail
1069
+ _ATT_W_actor="$(_att_default_actor)"; _ATT_W_engine="$(_att_default_engine)"
1070
+ _ATT_W_evidence_ref=none; _ATT_W_timestamp="$(_att_now_rfc3339)"; _ATT_W_override_ref=none
1071
+ _ATT_W_owner_ref="repo:${owner_rel}"; _ATT_W_owner_fingerprint="$ofp"
1072
+ _ATT_W_subject_revision="$full"
1073
+ _att_write_receipt || true
1074
+ _att_lock_release "$lname"
1075
+ echo "FAIL: semantic producer non-zero (${_ATT_RUN_RC}:${_ATT_RUN_CODE})"
1076
+ return 1
1077
+ fi
1078
+ # parse semantic result
1079
+ out_doc="$_ATT_RUN_OUT"
1080
+ verdict="$(printf '%s\n' "$out_doc" | awk -F': ' '/^verdict: /{print $2; exit}' | tr -d '\r')"
1081
+ local sfp
1082
+ sfp="$(printf '%s\n' "$out_doc" | awk -F': ' '/^subject_fingerprint: /{print $2; exit}' | tr -d '\r')"
1083
+ crit="$(printf '%s\n' "$out_doc" | awk -F': ' '/^critical_count: /{print $2; exit}' | tr -d '\r')"
1084
+ high="$(printf '%s\n' "$out_doc" | awk -F': ' '/^high_count: /{print $2; exit}' | tr -d '\r')"
1085
+ eref="$(printf '%s\n' "$out_doc" | awk -F': ' '/^evidence_ref: /{print $2; exit}' | tr -d '\r')"
1086
+ local sch
1087
+ sch="$(printf '%s\n' "$out_doc" | awk -F': ' '/^schema: /{print $2; exit}' | tr -d '\r')"
1088
+ if [ "$sch" != "$_ATT_SEM_RESULT_SCHEMA" ] || [ "$sfp" != "$fp" ]; then
1089
+ _att_lock_release "$lname"
1090
+ echo "FAIL: producer result schema/fingerprint mismatch"; return 1
1091
+ fi
1092
+ case "$verdict" in pass|fail) : ;; *) _att_lock_release "$lname"; echo "FAIL: bad producer verdict"; return 1 ;; esac
1093
+ if [ "$verdict" = "pass" ]; then
1094
+ [ "$crit" = "0" ] && [ "$high" = "0" ] || verdict=fail
1095
+ fi
1096
+ [ -n "$eref" ] || eref=none
1097
+ _att_valid_repo_ref "$eref" || eref=none
1098
+ _ATT_W_kind=semantic_gate; _ATT_W_subject_type=stage; _ATT_W_subject_id="$stage"
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_revision="$full"
1104
+ if ! _att_write_receipt; then _att_lock_release "$lname"; echo "FAIL: write receipt"; return 1; fi
1105
+ _att_lock_release "$lname"
1106
+ echo "PASS: semantic_gate stage $stage verdict=$verdict fp=$fp"
1107
+ [ "$verdict" = "pass" ] || [ "$verdict" = "override" ]
1108
+ }
1109
+
1110
+ _att_mint_semantic_card() {
1111
+ local id="$1" base="$2" rev="$3" owner_rel="$4"
1112
+ local file full_base full_head fp ofp
1113
+ id="$(_att_norm_card_id "$id")" || { echo "FAIL: bad card id"; return 2; }
1114
+ file="$(resolve_card_file "$id")"
1115
+ [ -f "$file" ] || { echo "FAIL: card not found"; return 2; }
1116
+ _att_git_ok || { echo "FAIL: attest requires Git"; return 2; }
1117
+ _att_git_clean_for_attest || { echo "FAIL: dirty worktree"; return 1; }
1118
+ full_base="$(_att_full_commit "$base")" || { echo "FAIL: bad base"; return 2; }
1119
+ full_head="$(_att_full_commit "$rev")" || { echo "FAIL: bad revision"; return 2; }
1120
+ if [ "$full_base" = "$full_head" ]; then
1121
+ echo "FAIL: card semantic requires non-empty base..revision (base must differ from --revision)"
1122
+ return 2
1123
+ fi
1124
+ if ! _att_load_owner_at_rev "$full_head" "$owner_rel"; then
1125
+ echo "FAIL: owner/command must be committed regular files matching disk at revision"
1126
+ return 1
1127
+ fi
1128
+ [ "$_ATT_KV_kind" = "semantic_gate" ] || { echo "FAIL: owner kind"; return 2; }
1129
+ [ "$_ATT_KV_subject_id" = "$id" ] || { echo "FAIL: subject_id mismatch"; return 2; }
1130
+ local cmd_rel="${_ATT_KV_command#repo:}"
1131
+ if ! _att_card_semantic_fingerprint "$file" "$full_base" "$full_head" >/dev/null; then echo "FAIL: fingerprint"; return 1; fi
1132
+ fp="$_ATT_LAST_FP"
1133
+ ofp="$(_att_owner_fingerprint "$owner_rel" "$full_head")" || { echo "FAIL: owner fp"; return 1; }
1134
+ local lname execf; lname="$(_att_subject_lock_name semantic_gate "$id")"
1135
+ _att_lock_acquire "$lname" || return 1
1136
+ execf="$(_att_materialize_blob_exec "$full_head" "$cmd_rel")" || {
1137
+ _att_lock_release "$lname"; echo "FAIL: could not materialize committed producer"; return 1
1138
+ }
1139
+ _att_run_supervised "$execf" --subject-id "$id" --subject-revision "$full_head" --subject-fingerprint "$fp"
1140
+ rm -f "$execf" 2>/dev/null || true
1141
+ local verdict=fail eref=none
1142
+ if [ "$_ATT_RUN_RC" = "exit" ] && [ "$_ATT_RUN_CODE" -eq 0 ]; then
1143
+ local sch sfp crit high
1144
+ sch="$(printf '%s\n' "$_ATT_RUN_OUT" | awk -F': ' '/^schema: /{print $2; exit}' | tr -d '\r')"
1145
+ sfp="$(printf '%s\n' "$_ATT_RUN_OUT" | awk -F': ' '/^subject_fingerprint: /{print $2; exit}' | tr -d '\r')"
1146
+ verdict="$(printf '%s\n' "$_ATT_RUN_OUT" | awk -F': ' '/^verdict: /{print $2; exit}' | tr -d '\r')"
1147
+ crit="$(printf '%s\n' "$_ATT_RUN_OUT" | awk -F': ' '/^critical_count: /{print $2; exit}' | tr -d '\r')"
1148
+ high="$(printf '%s\n' "$_ATT_RUN_OUT" | awk -F': ' '/^high_count: /{print $2; exit}' | tr -d '\r')"
1149
+ eref="$(printf '%s\n' "$_ATT_RUN_OUT" | awk -F': ' '/^evidence_ref: /{print $2; exit}' | tr -d '\r')"
1150
+ [ "$sch" = "$_ATT_SEM_RESULT_SCHEMA" ] && [ "$sfp" = "$fp" ] || verdict=fail
1151
+ case "$verdict" in pass|fail) : ;; *) verdict=fail ;; esac
1152
+ [ "$verdict" = "pass" ] && { [ "$crit" = "0" ] && [ "$high" = "0" ] || verdict=fail; }
1153
+ [ -n "$eref" ] || eref=none
1154
+ fi
1155
+ _ATT_W_kind=semantic_gate; _ATT_W_subject_type=card; _ATT_W_subject_id="$id"
1156
+ _ATT_W_subject_fingerprint="$fp"; _ATT_W_verdict="$verdict"
1157
+ _ATT_W_actor="$(_att_default_actor)"; _ATT_W_engine="$(_att_default_engine)"
1158
+ _ATT_W_evidence_ref="$eref"; _ATT_W_timestamp="$(_att_now_rfc3339)"; _ATT_W_override_ref=none
1159
+ _ATT_W_owner_ref="repo:${owner_rel}"; _ATT_W_owner_fingerprint="$ofp"
1160
+ _ATT_W_subject_base="$_ATT_SUBJ_BASE"; _ATT_W_subject_revision="$_ATT_SUBJ_REV"; _ATT_W_subject_tree="$_ATT_SUBJ_TREE"
1161
+ _att_write_receipt || { _att_lock_release "$lname"; return 1; }
1162
+ _att_lock_release "$lname"
1163
+ echo "PASS: semantic_gate card $id verdict=$verdict"
1164
+ [ "$verdict" = "pass" ]
1165
+ }
1166
+
1167
+ _att_mint_live() {
1168
+ local id="$1" rev="$2" owner_rel="$3"
1169
+ local file full cmd_rel oracle_rel target ofp cfp lname ap
1170
+ id="$(_att_norm_card_id "$id")" || { echo "FAIL: bad card"; return 2; }
1171
+ file="$(resolve_card_file "$id")"; [ -f "$file" ] || { echo "FAIL: card missing"; return 2; }
1172
+ _att_git_ok || { echo "FAIL: git required"; return 2; }
1173
+ if ! _att_supervisor_capable; then echo "FAIL: process supervisor unsupported on this platform"; return 1; fi
1174
+ _att_git_clean_for_attest || { echo "FAIL: dirty worktree (live mint requires clean subject)"; return 1; }
1175
+ full="$(_att_full_commit "$rev")" || { echo "FAIL: bad revision"; return 2; }
1176
+ if ! _att_load_owner_at_rev "$full" "$owner_rel"; then
1177
+ echo "FAIL: owner/command/oracle must be committed regular files matching disk at revision"
1178
+ return 1
1179
+ fi
1180
+ [ "$_ATT_KV_kind" = "live_verify" ] || { echo "FAIL: owner kind must be live_verify"; return 2; }
1181
+ [ "$_ATT_KV_subject_id" = "$id" ] || { echo "FAIL: subject mismatch"; return 2; }
1182
+ target="$_ATT_KV_target_id"
1183
+ _att_valid_target_id "$target" && [ "$target" != "none" ] || { echo "FAIL: live needs target_id"; return 2; }
1184
+ case "$_ATT_KV_revision_oracle" in repo:*) : ;; *) echo "FAIL: live needs revision_oracle"; return 2 ;; esac
1185
+ cmd_rel="${_ATT_KV_command#repo:}"; oracle_rel="${_ATT_KV_revision_oracle#repo:}"
1186
+ ofp="$(_att_owner_fingerprint "$owner_rel" "$full")" || { echo "FAIL: owner fp"; return 1; }
1187
+ local o_blob o_mode o_fp cmd_blob cmd_mode
1188
+ o_blob="$(_att_blob_at_rev "$full" "$oracle_rel")"
1189
+ o_mode="$(_att_path_mode_at_rev "$full" "$oracle_rel")"
1190
+ o_fp="$(_att_hash_text "oracle:${oracle_rel}:${o_mode}:${o_blob}")"
1191
+ cmd_blob="$(_att_blob_at_rev "$full" "$cmd_rel")"
1192
+ cmd_mode="$(_att_path_mode_at_rev "$full" "$cmd_rel")"
1193
+ cfp="$(_att_hash_text "argv:${cmd_rel}:${cmd_mode}:${cmd_blob}|${id}|${full}|${target}")"
1194
+ local proj fp
1195
+ 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')"
1196
+ fp="$(_att_hash_text "live|${full}|${target}|${ofp}|${proj}")"
1197
+ lname="$(_att_subject_lock_name live_verify "$id")"
1198
+ _att_lock_acquire "$lname" || return 1
1199
+ ap="$(_att_attempt_path "$id")"
1200
+ mkdir -p "$(dirname "$ap")" 2>/dev/null || true
1201
+ printf 'pid=%s\nstart=%s\nhost=%s\n' "$$" "$(_att_now_s)" "$(uname -n 2>/dev/null || echo host)" > "$ap"
1202
+ local oracle_exec cmd_exec oracle_oid verdict=fail rcode
1203
+ oracle_exec="$(_att_materialize_blob_exec "$full" "$oracle_rel")" || {
1204
+ rm -f "$ap"; _att_lock_release "$lname"; echo "FAIL: materialize oracle"; return 1
1205
+ }
1206
+ _att_run_supervised "$oracle_exec" --subject-id "$id" --subject-revision "$full" --target-id "$target"
1207
+ rm -f "$oracle_exec" 2>/dev/null || true
1208
+ rcode="$(_att_result_code_str)"
1209
+ if [ "$_ATT_RUN_RC" = "exit" ] && [ "$_ATT_RUN_CODE" -eq 0 ]; then
1210
+ oracle_oid="$(printf '%s' "$_ATT_RUN_OUT" | tr -d '\r\n[:space:]')"
1211
+ if [ "$oracle_oid" = "$full" ]; then
1212
+ cmd_exec="$(_att_materialize_blob_exec "$full" "$cmd_rel")" || true
1213
+ if [ -n "${cmd_exec:-}" ]; then
1214
+ _att_run_supervised "$cmd_exec" --subject-id "$id" --subject-revision "$full" --subject-fingerprint "$fp"
1215
+ rm -f "$cmd_exec" 2>/dev/null || true
1216
+ rcode="$(_att_result_code_str)"
1217
+ if [ "$_ATT_RUN_RC" = "exit" ] && [ "$_ATT_RUN_CODE" -eq 0 ]; then
1218
+ verdict=pass
1219
+ fi
1220
+ else
1221
+ rcode="spawn-error"
1222
+ fi
1223
+ else
1224
+ rcode="exit:1"
1225
+ fi
1226
+ fi
1227
+ local rfp
1228
+ rfp="$(_att_hash_text "result|${rcode}|${_ATT_RUN_MS}|${cfp}")"
1229
+ _ATT_W_kind=live_verify; _ATT_W_subject_type=card; _ATT_W_subject_id="$id"
1230
+ _ATT_W_subject_fingerprint="$fp"; _ATT_W_verdict="$verdict"
1231
+ _ATT_W_actor="$(_att_default_actor)"; _ATT_W_engine="$(_att_default_engine)"
1232
+ _ATT_W_evidence_ref=none; _ATT_W_timestamp="$(_att_now_rfc3339)"; _ATT_W_override_ref=none
1233
+ _ATT_W_owner_ref="repo:${owner_rel}"; _ATT_W_owner_fingerprint="$ofp"
1234
+ _ATT_W_subject_revision="$full"
1235
+ _ATT_W_command_fingerprint="$cfp"; _ATT_W_result_code="$rcode"
1236
+ _ATT_W_result_fingerprint="$rfp"; _ATT_W_duration_ms="$_ATT_RUN_MS"
1237
+ _ATT_W_target_id="$target"
1238
+ _ATT_W_revision_oracle_ref="repo:${oracle_rel}"
1239
+ _ATT_W_revision_oracle_fingerprint="$o_fp"
1240
+ _att_write_receipt || true
1241
+ rm -f "$ap" 2>/dev/null || true
1242
+ _att_lock_release "$lname"
1243
+ echo "PASS: live_verify $id verdict=$verdict result=$rcode"
1244
+ [ "$verdict" = "pass" ]
1245
+ }
1246
+
1247
+ _att_recover_live() {
1248
+ local id="$1"
1249
+ id="$(_att_norm_card_id "$id")" || return 2
1250
+ local ap lname
1251
+ ap="$(_att_attempt_path "$id")"
1252
+ lname="$(_att_subject_lock_name live_verify "$id")"
1253
+ _att_lock_acquire "$lname" || return 1
1254
+ # write fail receipt if none/attempt
1255
+ _ATT_W_kind=live_verify; _ATT_W_subject_type=card; _ATT_W_subject_id="$id"
1256
+ _ATT_W_subject_fingerprint="git-sha1:0"; _ATT_W_verdict=fail
1257
+ _ATT_W_actor="$(_att_default_actor)"; _ATT_W_engine="$(_att_default_engine)"
1258
+ _ATT_W_evidence_ref=none; _ATT_W_timestamp="$(_att_now_rfc3339)"; _ATT_W_override_ref=none
1259
+ _ATT_W_owner_ref=none; _ATT_W_owner_fingerprint="git-sha1:0"
1260
+ _ATT_W_subject_revision="$(_att_full_commit HEAD 2>/dev/null || echo 0)"
1261
+ _ATT_W_command_fingerprint="git-sha1:0"; _ATT_W_result_code=spawn-error
1262
+ _ATT_W_result_fingerprint="git-sha1:0"; _ATT_W_duration_ms=0
1263
+ _ATT_W_target_id=none
1264
+ _ATT_W_revision_oracle_ref=none; _ATT_W_revision_oracle_fingerprint="git-sha1:0"
1265
+ _att_write_receipt || true
1266
+ rm -f "$ap" 2>/dev/null || true
1267
+ _att_lock_release "$lname"
1268
+ echo "PASS: recovered live attempt for $id as fail"
1269
+ return 0
1270
+ }
1271
+
1272
+ # ---------- enforcement helpers ----------
1273
+ _att_warn_or_block() { # $1 message $2 force_block(0/1)
1274
+ if [ "$2" = "1" ]; then
1275
+ echo "BLOCK: $1"
1276
+ return 1
1277
+ fi
1278
+ echo "WARNING: $1 (auto inactive — manual path still usable)"
1279
+ return 0
1280
+ }
1281
+
1282
+ # Check card receipts for auto. $1 card id $2 require_live(0/1) $3 enforce(0/1)
1283
+ _att_enforce_card() {
1284
+ local id="$1" need_live="$2" enforce="$3" st
1285
+ if [ "$enforce" != "1" ]; then
1286
+ if ! _att_accepted_semantic card "$id"; then
1287
+ _att_warn_or_block "card $id semantic receipt missing/stale/red" 0
1288
+ fi
1289
+ if [ "$need_live" = "1" ] && ! _att_accepted_live "$id"; then
1290
+ _att_warn_or_block "card $id live_verify receipt missing/stale/red" 0
1291
+ fi
1292
+ return 0
1293
+ fi
1294
+ # enforcing
1295
+ st="$(_att_auto_state_read)"
1296
+ if [ "$st" = "STALE" ] || [ "$st" = "INVALID" ]; then
1297
+ echo "BLOCK: auto state is $st — run '/flow auto' to refresh or '/flow auto stop'"
1298
+ return 1
1299
+ fi
1300
+ if [ -f "$(_att_attempt_path "$id")" ]; then
1301
+ echo "BLOCK: live attempt in progress for $id — wait or '/flow attest recover $id --mark-failed'"
1302
+ return 1
1303
+ fi
1304
+ if ! _att_accepted_semantic card "$id"; then
1305
+ echo "BLOCK: card $id needs current semantic_gate receipt (pass/override)"
1306
+ return 1
1307
+ fi
1308
+ if [ "$need_live" = "1" ] && ! _att_accepted_live "$id"; then
1309
+ echo "BLOCK: card $id needs current live_verify pass receipt"
1310
+ return 1
1311
+ fi
1312
+ return 0
1313
+ }
1314
+
1315
+ # ---------- cmd_attest ----------
1316
+ cmd_attest() {
1317
+ local sub="${1:-}"; shift 2>/dev/null || true
1318
+ case "$sub" in
1319
+ semantic)
1320
+ local stage="" card="" base="" rev="" owner=""
1321
+ while [ $# -gt 0 ]; do
1322
+ case "$1" in
1323
+ --stage) shift; stage="${1:-}" ;;
1324
+ --card) shift; card="${1:-}" ;;
1325
+ --base) shift; base="${1:-}" ;;
1326
+ --revision) shift; rev="${1:-}" ;;
1327
+ --owner) shift; owner="${1:-}" ;;
1328
+ *) echo "usage: flow attest semantic --stage S --revision R --owner PATH | --card C --base B --revision R --owner PATH"; return 2 ;;
1329
+ esac
1330
+ shift 2>/dev/null || true
1331
+ done
1332
+ [ -n "$owner" ] && [ -n "$rev" ] || { echo "usage: flow attest semantic ... --revision R --owner PATH"; return 2; }
1333
+ # strip repo: prefix if given
1334
+ case "$owner" in repo:*) owner="${owner#repo:}" ;; esac
1335
+ if [ -n "$stage" ] && [ -z "$card" ]; then
1336
+ _att_mint_semantic_stage "$stage" "$rev" "$owner"; return $?
1337
+ elif [ -n "$card" ] && [ -z "$stage" ]; then
1338
+ [ -n "$base" ] || { echo "FAIL: --base required for card semantic"; return 2; }
1339
+ _att_mint_semantic_card "$card" "$base" "$rev" "$owner"; return $?
1340
+ else
1341
+ echo "FAIL: specify exactly one of --stage or --card"; return 2
1342
+ fi
1343
+ ;;
1344
+ live-verify)
1345
+ local id="${1:-}"; shift 2>/dev/null || true
1346
+ local rev="" owner=""
1347
+ while [ $# -gt 0 ]; do
1348
+ case "$1" in
1349
+ --revision) shift; rev="${1:-}" ;;
1350
+ --owner) shift; owner="${1:-}" ;;
1351
+ *) echo "usage: flow attest live-verify C-NNN --revision R --owner PATH"; return 2 ;;
1352
+ esac
1353
+ shift 2>/dev/null || true
1354
+ done
1355
+ [ -n "$id" ] && [ -n "$rev" ] && [ -n "$owner" ] || { echo "usage: flow attest live-verify C-NNN --revision R --owner PATH"; return 2; }
1356
+ case "$owner" in repo:*) owner="${owner#repo:}" ;; esac
1357
+ _att_mint_live "$id" "$rev" "$owner"; return $?
1358
+ ;;
1359
+ status)
1360
+ local target="${1:-}"
1361
+ local st
1362
+ st="$(_att_auto_state_read)"
1363
+ echo "auto: $st"
1364
+ if [ -n "${_ATT_A_activated_at:-}" ]; then
1365
+ echo " activated_at: ${_ATT_A_activated_at}"
1366
+ echo " branch: ${_ATT_A_integration_branch}"
1367
+ fi
1368
+ if [ -n "$target" ]; then
1369
+ local sid path kind
1370
+ if _att_is_stage "$target"; then
1371
+ path="$(_att_receipt_path semantic_gate stage "$target")"
1372
+ echo "semantic_gate stage $target: $(_att_receipt_status "$path" 2>/dev/null || echo missing)"
1373
+ else
1374
+ sid="$(_att_norm_card_id "$target" 2>/dev/null || echo "$target")"
1375
+ path="$(_att_receipt_path semantic_gate card "$sid")"
1376
+ echo "semantic_gate card $sid: $(_att_receipt_status "$path" 2>/dev/null || echo missing)"
1377
+ path="$(_att_receipt_path live_verify card "$sid")"
1378
+ echo "live_verify card $sid: $(_att_receipt_status "$path" 2>/dev/null || echo missing)"
1379
+ fi
1380
+ else
1381
+ echo "receipts dir: $(_att_dir)"
1382
+ ls -la "$(_att_dir)" 2>/dev/null | sed 's/^/ /' || echo " (none)"
1383
+ fi
1384
+ return 0
1385
+ ;;
1386
+ recover)
1387
+ local id="${1:-}"; shift 2>/dev/null || true
1388
+ [ "${1:-}" = "--mark-failed" ] || { echo "usage: flow attest recover C-NNN --mark-failed"; return 2; }
1389
+ _att_recover_live "$id"; return $?
1390
+ ;;
1391
+ *)
1392
+ echo "usage: flow attest semantic|live-verify|status|recover ..."
1393
+ return 2
1394
+ ;;
1395
+ esac
1396
+ }
1397
+
1398
+ # Risk preflight for auto — print all blockers; return 0 if clear
1399
+ _att_risk_preflight() {
1400
+ local f id rv any=0
1401
+ local n=0
1402
+ for f in $(_att_all_card_files); do
1403
+ n=$((n + 1))
1404
+ id="$(basename "$f" .md)"
1405
+ rv="$(_card_risk_validate "$f" auto 2>/dev/null || echo "invalid:parse")"
1406
+ case "$rv" in
1407
+ standard)
1408
+ echo "READY $id risk=standard: $(_att_field_once "$f" risk-reason | cut -c1-80)"
1409
+ ;;
1410
+ security-class)
1411
+ echo "READY $id risk=security-class: ack ok"
1412
+ ;;
1413
+ unknown)
1414
+ echo "BLOCK $id risk=unknown: classify risk + reason"
1415
+ any=1
1416
+ ;;
1417
+ halt:*)
1418
+ echo "HALT $id risk=security-class: ${rv#halt:}"
1419
+ any=1
1420
+ ;;
1421
+ invalid:*)
1422
+ echo "BLOCK $id ${rv}"
1423
+ any=1
1424
+ ;;
1425
+ *)
1426
+ echo "BLOCK $id risk=$rv"
1427
+ any=1
1428
+ ;;
1429
+ esac
1430
+ done
1431
+ [ "$n" -gt 0 ] || { echo "FAIL: no cards"; return 1; }
1432
+ [ "$any" -eq 0 ]
1433
+ }