@consiliency/agent-board-cli 1.24.0 → 1.26.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.
Files changed (37) hide show
  1. package/claude-marketplace/.claude-plugin/marketplace.json +14 -0
  2. package/claude-marketplace/plugins/agent-board/.claude-plugin/plugin.json +19 -0
  3. package/claude-marketplace/plugins/agent-board/hooks/hooks.json +25 -0
  4. package/claude-marketplace/plugins/agent-board/hooks/wake-hook.sh +288 -0
  5. package/dist/assets.d.ts +26 -0
  6. package/dist/assets.js +70 -0
  7. package/dist/assets.json +6 -0
  8. package/dist/claude_marketplace.d.ts +117 -0
  9. package/dist/claude_marketplace.js +306 -0
  10. package/dist/cli.d.ts +37 -3
  11. package/dist/cli.js +68 -26
  12. package/dist/config_edit.d.ts +37 -0
  13. package/dist/config_edit.js +549 -0
  14. package/dist/doctor.d.ts +24 -2
  15. package/dist/doctor.js +164 -4
  16. package/dist/doctor_scopes.d.ts +34 -2
  17. package/dist/doctor_scopes.js +128 -20
  18. package/dist/identity_writer.d.ts +9 -7
  19. package/dist/identity_writer.js +18 -30
  20. package/dist/setup.d.ts +116 -0
  21. package/dist/setup.js +603 -0
  22. package/dist/setup_storage.d.ts +117 -0
  23. package/dist/setup_storage.js +447 -0
  24. package/dist/setup_writers.d.ts +147 -0
  25. package/dist/setup_writers.js +916 -0
  26. package/keyslot/agent-board-keyslot-load@.service +20 -0
  27. package/keyslot/agent-board-keyslot@.service +20 -0
  28. package/keyslot/constants.sh +73 -0
  29. package/keyslot/keygen.mjs +67 -0
  30. package/keyslot/keyslot-load.sh +78 -0
  31. package/keyslot/lib.sh +599 -0
  32. package/keyslot/optin.sh +185 -0
  33. package/keyslot/probe.sh +135 -0
  34. package/keyslot/provision.sh +530 -0
  35. package/keyslot/seam.sh +48 -0
  36. package/keyslot/status.sh +254 -0
  37. package/package.json +9 -4
@@ -0,0 +1,20 @@
1
+ # Installed to ~/.config/systemd/user/ by scripts/keyslot/provision.sh (IF-0-KEYSEAL-2).
2
+ # Loads the slot key into agent-board-keyslot@%i whenever that agent starts or restarts: a
3
+ # sealed key by decrypting it into a pipe, or an opted-in unsealed key from its 0600 file (A1).
4
+ # slot-%i is a per-slot symlink to the immutable loader version that slot runs (A1).
5
+ [Unit]
6
+ Description=Message Board key slot loader for %i (sealed key via a pipe, or opted-in unsealed key)
7
+ Documentation=https://github.com/Consiliency/message-board
8
+ BindsTo=agent-board-keyslot@%i.service
9
+ After=agent-board-keyslot@%i.service
10
+
11
+ [Service]
12
+ Type=oneshot
13
+ RemainAfterExit=yes
14
+ # Informational only: keyslot-load.sh sets its own socket from XDG_RUNTIME_DIR and %i,
15
+ # because the manager exports an ambient SSH_AUTH_SOCK (M7).
16
+ Environment=SSH_AUTH_SOCK=%t/agent-board/%i.sock
17
+ ExecStart=%h/.local/libexec/agent-board/slot-%i/keyslot-load.sh %i
18
+
19
+ [Install]
20
+ WantedBy=agent-board-keyslot@%i.service
@@ -0,0 +1,20 @@
1
+ # Installed to ~/.config/systemd/user/ by scripts/keyslot/provision.sh (IF-0-KEYSEAL-2).
2
+ # Its directive set is dev0's hand-installed unit (M7), so an existing hand install is
3
+ # adopted by directive-set equality rather than rewritten.
4
+ [Unit]
5
+ Description=Message Board key slot for %i (dedicated, non-forwarded ssh-agent; starts empty)
6
+ Documentation=https://github.com/Consiliency/message-board
7
+
8
+ [Service]
9
+ Type=simple
10
+ UMask=0077
11
+ # Shared parent dir is created, never removed: RuntimeDirectory= would be deleted
12
+ # when ANY instance stops, pulling the sockets out from under the other instances.
13
+ ExecStartPre=/bin/mkdir -p -m 0700 %t/agent-board
14
+ ExecStartPre=/bin/rm -f %t/agent-board/%i.sock
15
+ ExecStart=/usr/bin/ssh-agent -D -a %t/agent-board/%i.sock
16
+ Restart=on-failure
17
+ RestartSec=5
18
+
19
+ [Install]
20
+ WantedBy=default.target
@@ -0,0 +1,73 @@
1
+ # shellcheck shell=bash
2
+ # KEYSEAL constants (IF-0-KEYSEAL-1/-2). Sourced by keyslot-load.sh and by every
3
+ # scripts/keyslot tool; a value here is never restated anywhere else.
4
+
5
+ # The slot name: IF-0-KEYSEAL-1.
6
+ # shellcheck disable=SC2034
7
+ KEYSEAL_SLOT_RE='^[a-z][a-z0-9-]{0,31}$'
8
+
9
+ # Unit template names, without "@.service".
10
+ # shellcheck disable=SC2034
11
+ KEYSEAL_BASE_UNIT=agent-board-keyslot
12
+ # shellcheck disable=SC2034
13
+ KEYSEAL_LOAD_UNIT=agent-board-keyslot-load
14
+
15
+ # The installed copies of the loader and its siblings, relative to $HOME, at 0755 (A1). Each
16
+ # version lives in an immutable directory v-<first 16 hex of the set sha256>; slot <s> runs
17
+ # the version its symlink slot-<s> points at, recorded in <s>.loader-sha256 beside the slot.
18
+ # shellcheck disable=SC2034
19
+ KEYSEAL_LIBEXEC_REL=.local/libexec/agent-board
20
+ # Sorted by name: a version's sha is over its files in name order (keyseal_files_sha).
21
+ # shellcheck disable=SC2034
22
+ KEYSEAL_INSTALL_SET=(constants.sh keyslot-load.sh optin.sh seam.sh)
23
+ # shellcheck disable=SC2034
24
+ KEYSEAL_VERSION_PREFIX=v-
25
+ # shellcheck disable=SC2034
26
+ KEYSEAL_SLOT_LINK_PREFIX=slot-
27
+ # shellcheck disable=SC2034
28
+ KEYSEAL_VERSION_HEX=16
29
+
30
+ # The loader's binaries, by absolute path: no PATH assumption. Each has a seam override
31
+ # named KEYSEAL_TEST_<NAME> (seam.sh), honoured only under KEYSEAL_TEST_ROOT.
32
+ # shellcheck disable=SC2034
33
+ KEYSEAL_BIN_SYSTEMD_CREDS=/usr/bin/systemd-creds
34
+ # shellcheck disable=SC2034
35
+ KEYSEAL_BIN_SSH_ADD=/usr/bin/ssh-add
36
+ # shellcheck disable=SC2034
37
+ KEYSEAL_BIN_SLEEP=/usr/bin/sleep
38
+ # shellcheck disable=SC2034
39
+ KEYSEAL_LOADER_BINS=("$KEYSEAL_BIN_SYSTEMD_CREDS" "$KEYSEAL_BIN_SSH_ADD" "$KEYSEAL_BIN_SLEEP")
40
+ # The unsealed branch's checks (optin.sh), by absolute path too: coreutils and ssh-keygen.
41
+ # shellcheck disable=SC2034
42
+ KEYSEAL_BIN_STAT=/usr/bin/stat
43
+ # shellcheck disable=SC2034
44
+ KEYSEAL_BIN_SHA256SUM=/usr/bin/sha256sum
45
+ # shellcheck disable=SC2034
46
+ KEYSEAL_BIN_ID=/usr/bin/id
47
+ # shellcheck disable=SC2034
48
+ KEYSEAL_BIN_BASE64=/usr/bin/base64
49
+ # shellcheck disable=SC2034
50
+ KEYSEAL_BIN_TAIL=/usr/bin/tail
51
+ # shellcheck disable=SC2034
52
+ KEYSEAL_BIN_SSH_KEYGEN=/usr/bin/ssh-keygen
53
+ # The host identity an opt-in binds to (sha256 of it, first 16 hex). Seam override
54
+ # KEYSEAL_TEST_MACHINE_ID_FILE, under KEYSEAL_TEST_ROOT only.
55
+ # shellcheck disable=SC2034
56
+ KEYSEAL_MACHINE_ID_FILE=/etc/machine-id
57
+
58
+ # How long the loader waits for the slot socket: 50 x 0.1 s = 5 s.
59
+ # shellcheck disable=SC2034
60
+ KEYSEAL_SOCK_WAIT_STEPS=50
61
+
62
+ # The sealed credential's name for slot $1.
63
+ keyseal_cred_name() { printf 'agent-board-key-%s' "$1"; }
64
+
65
+ # The slot agent's socket for slot $1.
66
+ keyseal_sock_path() { printf '%s/agent-board/%s.sock' "$XDG_RUNTIME_DIR" "$1"; }
67
+
68
+ # K: the slot directory. XDG_CONFIG_HOME if set, else $HOME/.config, which is what the user
69
+ # manager resolves when (as on dev0) it exports HOME but no XDG_CONFIG_HOME.
70
+ keyseal_key_dir() { printf '%s/agent-board/keyslot' "${XDG_CONFIG_HOME:-$HOME/.config}"; }
71
+
72
+ # The unsealed key of slot $1 (A1): a 0600 file, written only on the opt-in path.
73
+ keyseal_unsealed_key_path() { printf '%s/%s.key' "$(keyseal_key_dir)" "$1"; }
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+ // keygen.mjs --pub-out <path>: generate one Ed25519 key in memory (IF-0-KEYSEAL-1 step 4).
3
+ //
4
+ // The private key goes to STDOUT ONLY, which provision.sh pipes straight into
5
+ // `systemd-creds --user encrypt`. It is an OpenSSH `openssh-key-v1` container with cipher
6
+ // none, not PKCS#8: OpenSSH 9.6p1 (Ubuntu 24.04) rejects an Ed25519 PKCS#8 PEM with
7
+ // `invalid format`, in `ssh-add -` and `ssh-keygen -y` alike, while every OpenSSH since 6.5
8
+ // reads this container. Node exports no OpenSSH private format, so it is written here, per
9
+ // PROTOCOL.key. The one filesystem write is the raw public key, as 43-char
10
+ // base64url, to --pub-out (created 0600, never overwritten). A static test pins that this is
11
+ // the only write and that nothing but node:crypto and node:fs is imported (KS-S1).
12
+ //
13
+ // KEYSEAL_TEST_FAIL_AT=keygen, honoured only under KEYSEAL_TEST_ROOT and only in the `fail`
14
+ // mode, exits 1 AFTER the full PEM and the pub are written (KS-4): the pipefail pin.
15
+
16
+ import { generateKeyPairSync, randomBytes } from "node:crypto";
17
+ import { writeFileSync } from "node:fs";
18
+
19
+ const at = process.argv.indexOf("--pub-out");
20
+ const pubOut = at > 0 ? process.argv[at + 1] : undefined;
21
+ if (!pubOut || process.argv.length !== 4) {
22
+ process.stderr.write("usage: keygen.mjs --pub-out <path>\n");
23
+ process.exit(1);
24
+ }
25
+
26
+ const { privateKey } = generateKeyPairSync("ed25519");
27
+ const jwk = privateKey.export({ format: "jwk" });
28
+ writeFileSync(pubOut, `${jwk.x}\n`, { mode: 0o600, flag: "wx" });
29
+
30
+ function sshString(b) {
31
+ const buf = Buffer.isBuffer(b) ? b : Buffer.from(b);
32
+ const len = Buffer.alloc(4);
33
+ len.writeUInt32BE(buf.length);
34
+ return Buffer.concat([len, buf]);
35
+ }
36
+
37
+ function opensshPem() {
38
+ const pub = Buffer.from(jwk.x, "base64url");
39
+ const seed = Buffer.from(jwk.d, "base64url");
40
+ const nkeys = Buffer.alloc(4);
41
+ nkeys.writeUInt32BE(1);
42
+ const check = randomBytes(4);
43
+ const wire = Buffer.concat([sshString("ssh-ed25519"), sshString(pub)]);
44
+ let priv = Buffer.concat([check, check, wire, sshString(Buffer.concat([seed, pub])), sshString("")]);
45
+ const pad = [];
46
+ for (let i = 1; (priv.length + pad.length) % 8 !== 0; i++) pad.push(i);
47
+ priv = Buffer.concat([priv, Buffer.from(pad)]);
48
+ const body = Buffer.concat([
49
+ Buffer.from("openssh-key-v1\0"),
50
+ sshString("none"),
51
+ sshString("none"),
52
+ sshString(""),
53
+ nkeys,
54
+ sshString(wire),
55
+ sshString(priv),
56
+ ]);
57
+ const b64 = body.toString("base64").match(/.{1,70}/g).join("\n");
58
+ return `-----BEGIN OPENSSH PRIVATE KEY-----\n${b64}\n-----END OPENSSH PRIVATE KEY-----\n`;
59
+ }
60
+
61
+ const failHere =
62
+ Boolean(process.env.KEYSEAL_TEST_ROOT) &&
63
+ process.env.KEYSEAL_TEST_FAIL_AT === "keygen" &&
64
+ (process.env.KEYSEAL_TEST_FAIL_MODE ?? "fail") === "fail";
65
+ process.stdout.write(opensshPem(), () => {
66
+ process.exit(failHere ? 1 : 0);
67
+ });
@@ -0,0 +1,78 @@
1
+ #!/bin/bash
2
+ # keyslot-load.sh <slot>: load the key of <slot> into its slot agent.
3
+ #
4
+ # Run by agent-board-keyslot-load@<slot>.service from ~/.local/libexec/agent-board/slot-<slot>/,
5
+ # a symlink to the immutable version directory that slot runs (A1), so it sources its siblings
6
+ # by its own directory and works with the checkout gone. One loader, two modes:
7
+ # sealed <slot>.cred: decrypted into a pipe to ssh-add; it never exists as a file
8
+ # (IF-0-KEYSEAL-2).
9
+ # unsealed <slot>.key (A1, opt-in only): streamed into ssh-add ONLY when optin.sh's
10
+ # predicate holds, the same one status.sh uses: a valid opt-in for this machine,
11
+ # user, slot and pin; the key 0600 and this user's; its public key the pin. The
12
+ # loader runs at every boot and status does not, so the refusal lives here. This
13
+ # branch never resolves systemd-creds, so it runs on a host that has none.
14
+ # Both present is a contradiction, refused.
15
+ set -euo pipefail
16
+
17
+ if [[ ${BASH_SOURCE[0]} == */* ]]; then here=${BASH_SOURCE[0]%/*}; else here=.; fi
18
+ . "$here/constants.sh"
19
+ . "$here/seam.sh"
20
+ . "$here/optin.sh"
21
+
22
+ slot=${1-}
23
+ if [[ ! $slot =~ $KEYSEAL_SLOT_RE ]]; then
24
+ echo "keyslot-load: bad_slot" >&2
25
+ exit 1
26
+ fi
27
+ : "${XDG_RUNTIME_DIR:?keyslot-load: XDG_RUNTIME_DIR unset}"
28
+ : "${HOME:?keyslot-load: HOME unset}"
29
+
30
+ ssh_add=$(seam_resolve KEYSEAL_TEST_SSH_ADD "$KEYSEAL_BIN_SSH_ADD")
31
+ sleep=$(seam_resolve KEYSEAL_TEST_SLEEP "$KEYSEAL_BIN_SLEEP")
32
+
33
+ # Set, never inherited: the manager exports an ambient SSH_AUTH_SOCK (M7), and the unit's
34
+ # Environment= is only as good as the unit file.
35
+ SSH_AUTH_SOCK=$(keyseal_sock_path "$slot")
36
+ export SSH_AUTH_SOCK
37
+
38
+ for ((i = 0; i < KEYSEAL_SOCK_WAIT_STEPS; i++)); do
39
+ [[ -S $SSH_AUTH_SOCK ]] && break
40
+ "$sleep" 0.1
41
+ done
42
+ if [[ ! -S $SSH_AUTH_SOCK ]]; then
43
+ echo "keyslot-load: socket_absent" >&2
44
+ exit 1
45
+ fi
46
+
47
+ cred=$(keyseal_key_dir)/$slot.cred
48
+ key=$(keyseal_unsealed_key_path "$slot")
49
+ if [[ -e $key || -L $key ]]; then
50
+ if [[ -e $cred || -L $cred ]]; then
51
+ echo "keyslot-load: slot_contradiction (both $slot.cred and $slot.key)" >&2
52
+ exit 1
53
+ fi
54
+ mid_file=$(seam_resolve KEYSEAL_TEST_MACHINE_ID_FILE "$KEYSEAL_MACHINE_ID_FILE")
55
+ mid=$(keyseal_machine_id_of "$mid_file") || mid=unreadable
56
+ # On success the predicate prints the pin it verified: that is the value compared below.
57
+ if ! pin=$(keyseal_unsealed_verify "$slot" "$("$KEYSEAL_BIN_ID" -u)" "$mid"); then
58
+ printf 'keyslot-load: unsealed_key_refused: %s\n' "${pin//$'\n'/; }" >&2
59
+ exit 1
60
+ fi
61
+ seam_pause verified "$sleep" || exit 1
62
+ # ssh-add is fed the descriptor that is checked here, never a re-open of the path: open it
63
+ # under principle 4, derive its public key through it, compare with the pin, load from it.
64
+ keyseal_trusted_open "$key" 65536 >&2 || { echo "keyslot-load: unsealed_key_refused" >&2; exit 1; }
65
+ kp=$(keyseal_fd_pub "$KEYSEAL_FD" "$slot.key") || kp=
66
+ if [[ -z $pin || $kp != "$pin" ]]; then
67
+ echo "keyslot-load: unsealed_key_refused: the opened key is not the pin" >&2
68
+ exit 1
69
+ fi
70
+ seam_pause opened "$sleep" || exit 1
71
+ SSH_AUTH_SOCK=$SSH_AUTH_SOCK "$ssh_add" - <&"$KEYSEAL_FD"
72
+ exit
73
+ fi
74
+
75
+ creds=$(seam_resolve KEYSEAL_TEST_SYSTEMD_CREDS "$KEYSEAL_BIN_SYSTEMD_CREDS")
76
+ # pipefail: a decrypt that fails after printing still fails the load.
77
+ "$creds" --user decrypt --name="$(keyseal_cred_name "$slot")" "$cred" - \
78
+ | "$ssh_add" -