@consiliency/agent-board-cli 1.23.0 → 1.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,254 @@
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>.
4
+ #
5
+ # MODE, from provenance only, never from what provision was asked (A1):
6
+ # sealed <s>.cred + <s>.pub, the pin derives from a PIPED decrypt of the blob, no
7
+ # <s>.key, no opt-in
8
+ # unsealed a valid opt-in + <s>.key whose public key equals the pin, no blob
9
+ # contradiction (exit 1) the blob and <s>.key both present; an opt-in beside a blob; <s>.key
10
+ # without a valid opt-in; an opt-in without <s>.key; a pin the key or blob does
11
+ # not derive to; a pin or blob missing where the rest is committed; a staging file
12
+ # undetermined (exit 2) nothing committed (no blob, pin, key or opt-in at all), or the blob
13
+ # could not be read because sealing is broken on this host (probe not capable)
14
+ # The opt-in is opened under principle 4 (O_NOFOLLOW, the opened descriptor this uid's and
15
+ # 0600, every ancestor a real directory nobody else can write) and every field must match:
16
+ # machine_id (sha256(/etc/machine-id)[:16]), uid, slot, and pub_sha256 = sha256(<s>.pub). A
17
+ # record copied from another machine, user or slot fails.
18
+ #
19
+ # Checks: 1 committed (the provenance above), 2 chain (both units' Exec argv, Environment,
20
+ # FragmentPath and drop-ins against the shipped units; the slot's loader symlink, version
21
+ # directory and <s>.loader-sha256), 3 units (both enabled, base active, loader active
22
+ # (exited)), 4 agent (`ssh-add -L` lists exactly the pin), 5 linger, 6 scan (no plaintext copy
23
+ # of the slot key in IF-0-KEYSEAL-5's scope; an unsealed slot's own <s>.key, that path, 0600,
24
+ # this uid's, is the one exemption).
25
+ #
26
+ # EXIT, frozen precedence (A1): 1 on any contradiction or failed check, and 1 BEATS every
27
+ # exit-2 cause; else 2 on any exit-2 cause; else 0. Exit-2 causes:
28
+ # capability sealed and probe.sh not `capable`; unsealed and probe.sh neither
29
+ # `incapable` nor `capable` (unsealed EXPECTS incapable)
30
+ # linger_off
31
+ # upgrade_available unsealed on a host that is now capable: provision.sh --rotate
32
+ # loader_outdated the slot runs its recorded loader version, not the running tooling's
33
+ # loader_unrecorded no <s>.loader-sha256 (a slot provisioned before A1)
34
+ # legacy_loader_present flat-layout loader files (pre-A1) are present, or the installed
35
+ # template still runs them
36
+ # nothing_committed, undeterminable
37
+ # The remedy for the three loader causes is provision.sh --refresh-loader, which never re-keys.
38
+ # The JSON carries hashes only: never the pin, the reference or any key byte.
39
+ set -euo pipefail
40
+
41
+ here=${BASH_SOURCE[0]%/*}
42
+ [[ $here == "${BASH_SOURCE[0]}" ]] && here=.
43
+ # shellcheck source=lib.sh
44
+ . "$here/lib.sh"
45
+
46
+ harness= slot= json=no
47
+ while (($#)); do
48
+ case $1 in
49
+ --harness) harness=${2-}; shift 2 || keyseal_die "usage: --harness <h>" ;;
50
+ --slot) slot=${2-}; shift 2 || keyseal_die "usage: --slot <s>" ;;
51
+ --json) json=yes; shift ;;
52
+ *) keyseal_die "usage: status.sh --harness <h> [--slot <s>] [--json]" ;;
53
+ esac
54
+ done
55
+ [[ $harness =~ $KEYSEAL_SLOT_RE ]] || keyseal_die "bad_harness"
56
+ slot=${slot:-$harness}
57
+ [[ $slot =~ $KEYSEAL_SLOT_RE ]] || keyseal_die "bad_slot"
58
+ : "${XDG_RUNTIME_DIR:?status: XDG_RUNTIME_DIR unset}"
59
+ : "${HOME:?status: HOME unset}"
60
+ keyseal_resolve_bins || exit 1
61
+
62
+ K=$(keyseal_key_dir)
63
+ cred=$K/$slot.cred pub=$K/$slot.pub key=$K/$slot.key optin=$K/$slot.unsealed-optin
64
+ base_unit=$KEYSEAL_BASE_UNIT@$slot.service
65
+ load_unit=$KEYSEAL_LOAD_UNIT@$slot.service
66
+ declare -A ok detail
67
+ exit2=()
68
+ has() { [[ -e $1 || -L $1 ]]; }
69
+
70
+ probe_out=$("$here/probe.sh" --json 2>/dev/null) && probe_rc=0 || probe_rc=$?
71
+ outcome=error
72
+ [[ $probe_out =~ \"outcome\":\"([a-z-]+)\" ]] && outcome=${BASH_REMATCH[1]}
73
+ round_trip=unknown
74
+ [[ $probe_out =~ \"round_trip\":\"([a-z_]+)\" ]] && round_trip=${BASH_REMATCH[1]}
75
+
76
+ # 1 committed: the provenance table. The pin is read whatever the provenance, so a
77
+ # contradiction still scans for copies of the pinned key (review round 1, Opus S3).
78
+ contra=()
79
+ mode=undetermined optin_json=null
80
+ pin=$(keyseal_read_pin "$pub") || pin=
81
+ has "$cred" && has "$key" && contra+=("a sealed blob and an unsealed key are both present")
82
+ has "$cred" && has "$optin" && contra+=("an opt-in record beside a sealed blob")
83
+ has "$optin" && ! has "$key" && contra+=("an opt-in record without $slot.key")
84
+ if has "$optin"; then
85
+ optin_ok=true
86
+ optin_problems=$(keyseal_optin_check "$slot") || optin_ok=false
87
+ plist=
88
+ while IFS= read -r line; do [[ -n $line ]] && plist+="${plist:+,}$(keyseal_json_str "$line")"; done <<<"$optin_problems"
89
+ optin_json="{\"ok\":$optin_ok,\"problems\":[$plist]}"
90
+ fi
91
+ if has "$key" && ! has "$optin"; then contra+=("$slot.key without an opt-in record"); fi
92
+ if has "$K/$slot.cred.staging" || has "$K/$slot.pub.staging" || has "$K/$slot.unsealed-optin.staging"; then
93
+ contra+=("staging file present")
94
+ fi
95
+ if ((${#contra[@]} == 0)); then
96
+ if ! has "$cred" && ! has "$pub" && ! has "$key" && ! has "$optin"; then
97
+ exit2+=(nothing_committed)
98
+ elif has "$key"; then
99
+ # The loader's own predicate (optin.sh): what it would refuse to load is a contradiction.
100
+ if problems=$(keyseal_unsealed_check "$slot"); then mode=unsealed
101
+ else contra+=("the unsealed slot does not verify: ${problems//$'\n'/; }")
102
+ fi
103
+ elif ! has "$cred"; then contra+=("blob missing")
104
+ elif [[ -z $pin ]]; then contra+=("pin missing or malformed")
105
+ else
106
+ drc=0
107
+ derived=$(keyseal_derive_pub "$slot") || drc=$?
108
+ if ((drc == 0)); then
109
+ if [[ $derived == "$pin" ]]; then mode=sealed; else contra+=("pin does not derive from the blob"); fi
110
+ elif [[ $outcome == capable ]]; then
111
+ contra+=("the blob does not decrypt to a key, though sealing works here")
112
+ else
113
+ exit2+=(undeterminable)
114
+ fi
115
+ fi
116
+ fi
117
+ if ((${#contra[@]})); then
118
+ mode=contradiction ok[committed]=false
119
+ detail[committed]=$(printf '%s; ' "${contra[@]}")
120
+ detail[committed]=${detail[committed]%; }
121
+ else
122
+ ok[committed]=true detail[committed]=""
123
+ fi
124
+
125
+ # 2 chain, including the slot's loader version
126
+ if keyseal_chain_check "$slot" "$here"; then ok[chain]=true detail[chain]=""
127
+ else ok[chain]=false detail[chain]=${KEYSEAL_CHAIN_PROBLEMS%$'\n'}
128
+ fi
129
+ legacy=no
130
+ if [[ $KEYSEAL_CHAIN_LEGACY == yes ]] || keyseal_legacy_present; then legacy=yes exit2+=(legacy_loader_present); fi
131
+ if [[ $KEYSEAL_CHAIN_LEGACY == no ]]; then
132
+ case $KEYSEAL_LOADER_STATE in
133
+ outdated) exit2+=(loader_outdated) ;;
134
+ unrecorded) exit2+=(loader_unrecorded) ;;
135
+ esac
136
+ fi
137
+
138
+ # 3 units
139
+ state() { "$SYSTEMCTL" --user "$1" "$2" 2>/dev/null || true; }
140
+ sub=$("$SYSTEMCTL" --user show -p SubState "$load_unit" 2>/dev/null || true)
141
+ if [[ $(state is-enabled "$base_unit") == enabled && $(state is-enabled "$load_unit") == enabled &&
142
+ $(state is-active "$base_unit") == active && $(state is-active "$load_unit") == active && $sub == SubState=exited ]]; then
143
+ ok[units]=true detail[units]=""
144
+ else
145
+ ok[units]=false detail[units]="base $(state is-enabled "$base_unit")/$(state is-active "$base_unit"), loader $(state is-enabled "$load_unit")/$(state is-active "$load_unit") ${sub#SubState=}"
146
+ fi
147
+
148
+ # 4 agent: exactly one identity, and it is the pin. `empty` is told apart from a wrong or
149
+ # extra key: an empty agent can be the CONSEQUENCE of an exit-2 cause (below).
150
+ agent_empty=no
151
+ if [[ -n $pin ]] && keyseal_agent_holds_exactly "$slot" "$pin"; then ok[agent]=true detail[agent]=""
152
+ else
153
+ held=$(keyseal_agent_keys "$slot" 2>/dev/null) || held="?"
154
+ if [[ -z $held ]]; then agent_empty=yes; fi
155
+ n=$(printf '%s' "$held" | "$KEYSEAL_BIN_GREP" -c . || true)
156
+ [[ $held == "?" ]] && n=
157
+ ok[agent]=false detail[agent]="the slot agent holds ${n:-an unreadable set of} identities, not exactly the pin"
158
+ fi
159
+
160
+ # 5 linger, and capability for the measured mode
161
+ linger=$(keyseal_linger)
162
+ if [[ $linger == yes ]]; then ok[linger]=true detail[linger]=""; else ok[linger]=false detail[linger]="linger is $linger"; exit2+=(linger_off); fi
163
+ upgrade=no
164
+ case $mode in
165
+ sealed) [[ $outcome == capable ]] || exit2+=("capability_$outcome") ;;
166
+ unsealed)
167
+ if [[ $outcome == capable ]]; then upgrade=yes exit2+=(upgrade_available)
168
+ elif [[ $outcome != incapable ]]; then exit2+=("capability_$outcome")
169
+ fi
170
+ ;;
171
+ esac
172
+
173
+ # 6 scan: the unsealed slot's own key file is the one exemption
174
+ exempt=
175
+ [[ $mode == unsealed ]] && exempt=$key
176
+ if scan=$(keyseal_scan "$pin" "$exempt"); then
177
+ if [[ $scan == *'"plaintext_matches":[]'* ]]; then ok[scan]=true detail[scan]=""
178
+ else ok[scan]=false detail[scan]="plaintext copy of the slot key found"
179
+ fi
180
+ else
181
+ scan='{}' ok[scan]=false detail[scan]="the scan did not run to completion"
182
+ fi
183
+
184
+ # Nothing committed is 2 by the provenance table: there is no unit, agent or chain to judge.
185
+ hard=(committed chain units agent scan)
186
+ [[ ${exit2[*]} == *nothing_committed* ]] && hard=(committed scan)
187
+ # committed, chain and scan are always hard. A loader that could not run, or an agent it left
188
+ # EMPTY, is the consequence of a sealed slot's capability loss or an undeterminable blob, so it
189
+ # is 2 while one is present (review round 1, Opus B2). A wrong or extra key stays 1.
190
+ soft=no
191
+ # Only causes that EXPLAIN an empty agent (review round 2, Opus N1): a sealed slot whose sealing
192
+ # is gone, or a blob that could not be read. Linger off does not (status needs a running
193
+ # manager), and an unsealed loader never uses sealing.
194
+ for r in "${exit2[@]}"; do
195
+ case $r in
196
+ undeterminable) soft=yes ;;
197
+ # error or incapable: the decrypt itself is gone. incapable-for-keyslot is linger or the ssh
198
+ # tools, which alone do not stop a decrypt; but probe.sh reports a lost systemd-creds on a
199
+ # no-linger host as incapable-for-keyslot too, so it counts when the round trip failed.
200
+ capability_error | capability_incapable) [[ $mode == sealed ]] && soft=yes ;;
201
+ capability_incapable-for-keyslot) [[ $mode == sealed && $round_trip != equal ]] && soft=yes ;;
202
+ esac
203
+ done
204
+ if [[ $soft == yes ]]; then
205
+ still_hard=()
206
+ for c in "${hard[@]}"; do
207
+ case $c in
208
+ agent) [[ ${ok[agent]} == true || $agent_empty == yes ]] || still_hard+=(agent) ;;
209
+ units)
210
+ [[ ${ok[units]} == true ]] ||
211
+ { [[ $(state is-enabled "$base_unit") == enabled && $(state is-enabled "$load_unit") == enabled ]] || still_hard+=(units); }
212
+ ;;
213
+ *) still_hard+=("$c") ;;
214
+ esac
215
+ done
216
+ hard=("${still_hard[@]}")
217
+ fi
218
+ rc=0
219
+ for c in "${hard[@]}"; do [[ ${ok[$c]} == true ]] || rc=1; done
220
+ if ((rc == 0 && ${#exit2[@]})); then rc=2; fi
221
+
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
+ boot_id=$(</proc/sys/kernel/random/boot_id)
226
+ machine_id=$(keyseal_machine_id) || machine_id=
227
+ bool() { if [[ $1 == yes ]]; then printf true; else printf false; fi; }
228
+ reasons=
229
+ for r in "${exit2[@]}"; do reasons+="${reasons:+,}$(keyseal_json_str "$r")"; done
230
+ outdated=no
231
+ [[ ${exit2[*]} == *loader_outdated* ]] && outdated=yes
232
+ str_or_null() { if [[ -n $1 ]]; then keyseal_json_str "$1"; else printf null; fi; }
233
+
234
+ if [[ $json == yes ]]; then
235
+ checks=
236
+ for c in committed chain units agent linger scan; do
237
+ checks+="${checks:+,}\"$c\":{\"ok\":${ok[$c]},\"detail\":$(keyseal_json_str "${detail[$c]}")}"
238
+ done
239
+ printf '{"tool":"status","seam":%s,"host":%s,"uid":%s,"machine_id":%s,"harness":%s,"slot":%s,"mode":%s,"boot_id":%s,"pin_sha256":%s,"blob_sha256":%s,"capability_probe_exit":%s,"probe_outcome":%s,"optin":%s,"loader":{"recorded_sha256":%s,"running_sha256":%s,"state":%s},"upgrade_available":%s,"loader_outdated":%s,"legacy_loader_present":%s,"exit2_reasons":[%s],"checks":{%s},"scan":%s,"exit":%s}\n' \
240
+ "$(keyseal_seam_json)" "$(keyseal_json_str "$(uname -n)")" "$(keyseal_uid)" "$(str_or_null "$machine_id")" \
241
+ "$(keyseal_json_str "$harness")" "$(keyseal_json_str "$slot")" "$(keyseal_json_str "$mode")" \
242
+ "$(keyseal_json_str "$boot_id")" "$pin_sha" "$blob_sha" "$probe_rc" "$(keyseal_json_str "$outcome")" "$optin_json" \
243
+ "$(str_or_null "$KEYSEAL_LOADER_RECORDED")" "$(str_or_null "$KEYSEAL_LOADER_RUNNING")" \
244
+ "$(keyseal_json_str "$([[ $KEYSEAL_CHAIN_LEGACY == yes ]] && echo legacy || echo "$KEYSEAL_LOADER_STATE")")" \
245
+ "$(bool "$upgrade")" "$(bool "$outdated")" "$(bool "$legacy")" "$reasons" "$checks" "$scan" "$rc"
246
+ else
247
+ for c in committed chain units agent linger scan; do
248
+ printf '%-9s %s %s\n' "$c" "$([[ ${ok[$c]} == true ]] && echo ok || echo FAIL)" "${detail[$c]}"
249
+ done
250
+ printf 'mode %s\n' "$mode"
251
+ ((${#exit2[@]})) && printf 'exit 2 %s\n' "${exit2[*]}"
252
+ printf 'status: exit %s (seam=%s)\n' "$rc" "$(keyseal_seam_json)"
253
+ fi
254
+ exit "$rc"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@consiliency/agent-board-cli",
3
- "version": "1.23.0",
3
+ "version": "1.25.0",
4
4
  "repository": "https://github.com/Consiliency/message-board.git",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -17,11 +17,12 @@
17
17
  "files": [
18
18
  "bin",
19
19
  "dist",
20
+ "keyslot",
20
21
  "LICENSE",
21
22
  "NOTICE"
22
23
  ],
23
24
  "scripts": {
24
- "build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.build.json",
25
+ "build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.build.json && node ../../scripts/lib/cli-assets.mjs --write",
25
26
  "prepack": "pnpm run build",
26
27
  "typecheck": "tsc -p tsconfig.json --noEmit",
27
28
  "test": "vitest run"
@@ -31,8 +32,11 @@
31
32
  "registry": "https://registry.npmjs.org"
32
33
  },
33
34
  "dependencies": {
34
- "@consiliency/agent-board-client": "1.39.0",
35
+ "@consiliency/agent-board-client": "1.40.0",
35
36
  "commander": "^14.0.1"
36
37
  },
37
- "license": "Apache-2.0"
38
+ "license": "Apache-2.0",
39
+ "devDependencies": {
40
+ "smol-toml": "1.9.0"
41
+ }
38
42
  }