@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.
- package/CHANGELOG.md +5 -0
- package/README.md +5 -5
- package/README_VN.md +61 -36
- package/package.json +1 -1
- package/skills/flow/SKILL.md +14 -8
- package/skills/flow/_templates/card.md +3 -0
- package/skills/flow/eval/fixtures/f05a/flow/05-contract.md +22 -0
- package/skills/flow/eval/fixtures/f05b/flow/05-contract.md +11 -0
- package/skills/flow/eval/manifest.tsv +2 -0
- package/skills/flow/references/attestations.md +307 -0
- package/skills/flow/references/auto-run.md +7 -2
- package/skills/flow/references/command-dispatch.md +3 -1
- package/skills/flow/references/concierge.md +1 -0
- package/skills/flow/references/debt-and-halts.md +2 -0
- package/skills/flow/references/ground-truth-gates.md +3 -0
- package/skills/flow/runner/attestations.sh +1365 -0
- package/skills/flow/runner/flow.sh +222 -16
- package/skills-manifest.json +6 -2
|
@@ -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
|
+
}
|