@consiliency/agent-board-cli 1.27.0 → 1.28.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/keyslot/status.sh CHANGED
@@ -1,6 +1,15 @@
1
1
  #!/bin/bash
2
- # status.sh --harness <h> [--slot <s>] [--json]: what does slot <s> hold, measured, and is it
3
- # chained and loaded? (IF-0-KEYSEAL-3, IF-0-KEYSEAL-6). <s> defaults to <h>.
2
+ # status.sh --harness <h> [--slot <s>] [--json] [--scan-cache fresh|use|only] [--scan-timeout <s>]:
3
+ # what does slot <s> hold, measured, and is it chained and loaded? (IF-0-KEYSEAL-3,
4
+ # IF-0-KEYSEAL-6). <s> defaults to <h>.
5
+ #
6
+ # SCAN CACHE (IF-552-2, #552): check 6's result is cached per slot under
7
+ # <state root>/agent-board/keyslot-scan/<slot>.json, valid for 24 h and only for the same seam,
8
+ # uid, machine, slot, mode, exemption, pin, blob, scope and scanner. `fresh` (the default, a hand
9
+ # run) never reads it and scans; `use` reads it and scans on a miss; `only` never scans, and a
10
+ # miss is exit 2 `scan_not_cached`. A match is never cached: it writes a tombstone. The whole
11
+ # scan stage runs under one `timeout` (--scan-timeout, default 1800 s); a scan that gives up is
12
+ # exit 2 `scan_timed_out` and is never cached. Progress goes to stderr; stdout stays JSON.
4
13
  #
5
14
  # MODE, from provenance only, never from what provision was asked (A1):
6
15
  # sealed <s>.cred + <s>.pub, the pin derives from a PIPED decrypt of the blob, no
@@ -34,6 +43,8 @@
34
43
  # legacy_loader_present flat-layout loader files (pre-A1) are present, or the installed
35
44
  # template still runs them
36
45
  # nothing_committed, undeterminable
46
+ # scan_not_cached --scan-cache only, and no valid cache (the scan is not judged)
47
+ # scan_timed_out the scan stage hit --scan-timeout (the scan is not judged)
37
48
  # The remedy for the three loader causes is provision.sh --refresh-loader, which never re-keys.
38
49
  # The JSON carries hashes only: never the pin, the reference or any key byte.
39
50
  set -euo pipefail
@@ -43,21 +54,30 @@ here=${BASH_SOURCE[0]%/*}
43
54
  # shellcheck source=lib.sh
44
55
  . "$here/lib.sh"
45
56
 
46
- harness= slot= json=no
57
+ harness= slot= json=no scan_cache=fresh scan_timeout=$KEYSEAL_SCAN_TIMEOUT_S
58
+ usage="usage: status.sh --harness <h> [--slot <s>] [--json] [--scan-cache fresh|use|only] [--scan-timeout <s>]"
47
59
  while (($#)); do
48
60
  case $1 in
49
61
  --harness) harness=${2-}; shift 2 || keyseal_die "usage: --harness <h>" ;;
50
62
  --slot) slot=${2-}; shift 2 || keyseal_die "usage: --slot <s>" ;;
51
63
  --json) json=yes; shift ;;
52
- *) keyseal_die "usage: status.sh --harness <h> [--slot <s>] [--json]" ;;
64
+ --scan-cache) scan_cache=${2-}; shift 2 || keyseal_die "$usage" ;;
65
+ --scan-timeout) scan_timeout=${2-}; shift 2 || keyseal_die "$usage" ;;
66
+ *) keyseal_die "$usage" ;;
53
67
  esac
54
68
  done
55
69
  [[ $harness =~ $KEYSEAL_SLOT_RE ]] || keyseal_die "bad_harness"
56
70
  slot=${slot:-$harness}
57
71
  [[ $slot =~ $KEYSEAL_SLOT_RE ]] || keyseal_die "bad_slot"
58
- : "${XDG_RUNTIME_DIR:?status: XDG_RUNTIME_DIR unset}"
72
+ [[ $scan_cache == fresh || $scan_cache == use || $scan_cache == only ]] || keyseal_die "bad_scan_cache: $usage"
73
+ [[ $scan_timeout =~ ^[1-9][0-9]{0,6}$ ]] || keyseal_die "bad_scan_timeout: $usage"
74
+ [[ -n ${XDG_RUNTIME_DIR-} ]] || keyseal_die "runtime_dir_unset: XDG_RUNTIME_DIR is unset, so the slot's agent socket cannot be located; run from a login session or export XDG_RUNTIME_DIR=/run/user/$(id -u)"
59
75
  : "${HOME:?status: HOME unset}"
60
76
  keyseal_resolve_bins || exit 1
77
+ # The scan's cleanup (IF-552-2): the ticker, the stage group and every temp file, on any exit.
78
+ trap keyseal_scan_cleanup EXIT
79
+ trap 'exit 130' INT
80
+ trap 'exit 143' TERM
61
81
 
62
82
  K=$(keyseal_key_dir)
63
83
  cred=$K/$slot.cred pub=$K/$slot.pub key=$K/$slot.key optin=$K/$slot.unsealed-optin
@@ -66,6 +86,7 @@ load_unit=$KEYSEAL_LOAD_UNIT@$slot.service
66
86
  declare -A ok detail
67
87
  exit2=()
68
88
  has() { [[ -e $1 || -L $1 ]]; }
89
+ str_or_null() { if [[ -n $1 ]]; then keyseal_json_str "$1"; else printf null; fi; }
69
90
 
70
91
  probe_out=$("$here/probe.sh" --json 2>/dev/null) && probe_rc=0 || probe_rc=$?
71
92
  outcome=error
@@ -170,20 +191,105 @@ case $mode in
170
191
  ;;
171
192
  esac
172
193
 
173
- # 6 scan: the unsealed slot's own key file is the one exemption
194
+ pin_sha=null blob_sha=null
195
+ [[ -n $pin ]] && pin_sha=$(keyseal_json_str "$(keyseal_sha256_str "$pin")")
196
+ [[ -f $cred ]] && blob_sha=$(keyseal_json_str "$(keyseal_sha256_file "$cred")")
197
+ machine_id=$(keyseal_machine_id) || machine_id=
198
+
199
+ # 6 scan: the unsealed slot's own key file is the one exemption. IF-552-2: the inputs a cached
200
+ # result is valid for, the cache read (under its lock), then a scan unless a valid cache serves.
174
201
  exempt=
175
202
  [[ $mode == unsealed ]] && exempt=$key
176
- if scan=$(keyseal_scan "$pin" "$exempt"); then
177
- if [[ $scan == *'"plaintext_matches":[]'* ]]; then ok[scan]=true detail[scan]=""
203
+ stale_temp=$(keyseal_scan_stale_temp)
204
+ keyseal_scan_roots
205
+ KEYSEAL_INPUT=(
206
+ [seam]=$(keyseal_seam_json)
207
+ [uid]=$(keyseal_uid)
208
+ [machine_id]=$(str_or_null "$machine_id")
209
+ [slot]=$(keyseal_json_str "$slot")
210
+ [mode]=$(keyseal_json_str "$mode")
211
+ [exempt]=$(if [[ -n $exempt ]]; then keyseal_json_str "$(keyseal_sha256_str "$exempt")"; else printf null; fi)
212
+ [pin_sha256]=$pin_sha
213
+ [blob_sha256]=$blob_sha
214
+ [scope_sha256]=$(keyseal_json_str "$(keyseal_sha256_str "$(keyseal_scan_scope_json)")")
215
+ [scanner_sha256]=$(str_or_null "$(keyseal_files_sha "$here" lib.sh status.sh || true)")
216
+ )
217
+ keyseal_cache_read "$slot" "$scan_cache"
218
+ cache_read="$KEYSEAL_CACHE_STATE:$KEYSEAL_CACHE_REASON"
219
+ cache_last=null
220
+ [[ -n $KEYSEAL_CACHE_LAST ]] && cache_last=$(keyseal_json_str "$(keyseal_iso "$KEYSEAL_CACHE_LAST")")
221
+ scan_source=none scan_scanned_at=null scan_age=null scan_duration=null scan_core=
222
+ scan_judged=yes scan_timed_out=no
223
+ if [[ $KEYSEAL_CACHE_STATE == hit ]]; then
224
+ scan_source=cached scan_core=$KEYSEAL_CACHE_SCAN
225
+ scan_now=$(keyseal_now) || scan_now=$KEYSEAL_CACHE_SCANNED_AT
226
+ scan_scanned_at=$(keyseal_json_str "$(keyseal_iso "$KEYSEAL_CACHE_SCANNED_AT")")
227
+ scan_age=$((scan_now - KEYSEAL_CACHE_SCANNED_AT)) scan_duration=$KEYSEAL_CACHE_DURATION
228
+ elif [[ $scan_cache == only ]]; then
229
+ scan_judged=no
230
+ detail[scan]="not judged: no valid scan cache ($KEYSEAL_CACHE_STATE:$KEYSEAL_CACHE_REASON)"
231
+ exit2+=(scan_not_cached)
232
+ else
233
+ # scanned_at is the scan's START: a file created after it may be missed.
234
+ scan_start=$(keyseal_now) || scan_start=0
235
+ printf 'status: scan: starting (%s:%s)\n' "$KEYSEAL_CACHE_STATE" "$KEYSEAL_CACHE_REASON" >&2
236
+ keyseal_scan_run "$pin" "$exempt" "$scan_timeout"
237
+ scan_end=$(keyseal_now) || scan_end=$scan_start
238
+ scan_duration=$((scan_end - scan_start))
239
+ ((scan_duration >= 0)) || scan_duration=0
240
+ case $KEYSEAL_SCAN_OUTCOME in
241
+ ok)
242
+ scan_source=fresh scan_core=$(keyseal_scan_core_json)
243
+ scan_scanned_at=$(keyseal_json_str "$(keyseal_iso "$scan_start")") scan_age=$scan_duration
244
+ ;;
245
+ timed_out)
246
+ scan_judged=no scan_timed_out=yes
247
+ detail[scan]="not judged: the scan gave up after $scan_timeout s (scan_timed_out)"
248
+ exit2+=(scan_timed_out)
249
+ KEYSEAL_CACHE_STATE=not_written KEYSEAL_CACHE_REASON=timed_out
250
+ ;;
251
+ *)
252
+ detail[scan]="the scan did not run to completion"
253
+ KEYSEAL_CACHE_STATE=not_written KEYSEAL_CACHE_REASON=incomplete
254
+ ;;
255
+ esac
256
+ fi
257
+ if [[ -n $scan_core ]]; then
258
+ if [[ $scan_core == *'"plaintext_matches":[]'* ]]; then ok[scan]=true detail[scan]=""
178
259
  else ok[scan]=false detail[scan]="plaintext copy of the slot key found"
179
260
  fi
180
261
  else
181
- scan='{}' ok[scan]=false detail[scan]="the scan did not run to completion"
262
+ ok[scan]=false
263
+ fi
264
+ # A match is never cached: it writes a tombstone. Only a clean, completed, fresh scan commits.
265
+ if [[ $scan_source == fresh ]]; then
266
+ if [[ ${ok[scan]} == true ]]; then keyseal_cache_commit_clean "$slot" "$scan_start" "$scan_duration" "$scan_core"
267
+ else keyseal_cache_commit_tombstone "$slot" "$harness"
268
+ fi
269
+ fi
270
+ if [[ $scan_source == fresh ]]; then
271
+ n_matches=0 n_candidates=0
272
+ [[ $scan_core =~ \"candidates\":([0-9]+) ]] && n_candidates=${BASH_REMATCH[1]}
273
+ [[ ${ok[scan]} == true ]] || n_matches=$(printf '%s' "$scan_core" | keyseal_node count-matches 2>/dev/null || printf '?')
274
+ printf 'status: scan: done in %s s (%s folders, %s candidates, %s matches)\n' "$scan_duration" "$KEYSEAL_SCAN_FOLDERS" "$n_candidates" "$n_matches" >&2
275
+ elif [[ $scan_source == none && $scan_judged == yes || $scan_timed_out == yes ]]; then
276
+ printf 'status: scan: done in %s s (%s folders): %s\n' "$scan_duration" "$KEYSEAL_SCAN_FOLDERS" "${detail[scan]}" >&2
277
+ fi
278
+ scan_meta="\"source\":\"$scan_source\",\"scanned_at\":$scan_scanned_at,\"age_s\":$scan_age,\"duration_s\":$scan_duration,\"inputs\":$(keyseal_cache_inputs_json),\"stale_temp\":$stale_temp,\"cache\":{\"mode\":\"$scan_cache\",\"state\":\"$KEYSEAL_CACHE_STATE\",\"reason\":$(str_or_null "$KEYSEAL_CACHE_REASON"),\"read\":$(keyseal_json_str "$cache_read"),\"last_scanned_at\":$cache_last}"
279
+ # A scan that was not judged still names the scope it would have scanned.
280
+ if [[ -n $scan_core ]]; then scan=$(keyseal_scan_with_headers "${scan_core%\}},$scan_meta}")
281
+ else scan="{\"roots\":$(keyseal_json_list ${KEYSEAL_ROOTS[@]+"${KEYSEAL_ROOTS[@]}"}),\"roots_skipped\":$KEYSEAL_ROOTS_SKIPPED_JSON,$scan_meta}"
182
282
  fi
183
283
 
184
284
  # Nothing committed is 2 by the provenance table: there is no unit, agent or chain to judge.
185
285
  hard=(committed chain units agent scan)
186
286
  [[ ${exit2[*]} == *nothing_committed* ]] && hard=(committed scan)
287
+ # A scan that was not judged (no valid cache under `only`, or the deadline) is 2, never 1.
288
+ if [[ $scan_judged == no ]]; then
289
+ judged=()
290
+ for c in "${hard[@]}"; do [[ $c == scan ]] || judged+=("$c"); done
291
+ hard=("${judged[@]}")
292
+ fi
187
293
  # committed, chain and scan are always hard. A loader that could not run, or an agent it left
188
294
  # EMPTY, is the consequence of a sealed slot's capability loss or an undeterminable blob, so it
189
295
  # is 2 while one is present (review round 1, Opus B2). A wrong or extra key stays 1.
@@ -219,17 +325,12 @@ rc=0
219
325
  for c in "${hard[@]}"; do [[ ${ok[$c]} == true ]] || rc=1; done
220
326
  if ((rc == 0 && ${#exit2[@]})); then rc=2; fi
221
327
 
222
- pin_sha=null blob_sha=null
223
- [[ -n $pin ]] && pin_sha=$(keyseal_json_str "$(keyseal_sha256_str "$pin")")
224
- [[ -f $cred ]] && blob_sha=$(keyseal_json_str "$(keyseal_sha256_file "$cred")")
225
328
  boot_id=$(</proc/sys/kernel/random/boot_id)
226
- machine_id=$(keyseal_machine_id) || machine_id=
227
329
  bool() { if [[ $1 == yes ]]; then printf true; else printf false; fi; }
228
330
  reasons=
229
331
  for r in "${exit2[@]}"; do reasons+="${reasons:+,}$(keyseal_json_str "$r")"; done
230
332
  outdated=no
231
333
  [[ ${exit2[*]} == *loader_outdated* ]] && outdated=yes
232
- str_or_null() { if [[ -n $1 ]]; then keyseal_json_str "$1"; else printf null; fi; }
233
334
 
234
335
  if [[ $json == yes ]]; then
235
336
  checks=
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@consiliency/agent-board-cli",
3
- "version": "1.27.0",
3
+ "version": "1.28.0",
4
4
  "repository": "https://github.com/Consiliency/message-board.git",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",