feat(membershipd): one-command bot provisioning (bot add)
Add `membershipd bot add --handle <name> --out <path> [--role] [--store]` to provision a bus identity for an automated process in a single step: mint a fresh Ed25519+X25519 identity (cs.GenerateIdentity, the same derivation worker/chat use), register its signing key in the allowlist, and write the credentials to a 0600 file. The file is the canonical identity format read by client.LoadIdentity, so a worker/clientcheck binary pointed at --out connects as the new user with no extra conversion. Shares the sqlite/kv store plumbing with `user add`. New exported pkg/client.WriteNewIdentity writes an identity in that format but refuses to overwrite an existing file (never silently clobber private keys). provisionBot ordering guarantees no half-provisioned bot: refuse an existing --out before touching the store, register (an already-registered key is a clear error, not a panic), then write credentials. Tests cover the golden path (register + 0600 file + LoadIdentity round-trip), default role, the already-registered error path (no file written), and the out-exists error path (no orphan user). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -75,6 +75,22 @@ func LoadOrCreateIdentity(path string) (cs.Identity, error) {
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// WriteNewIdentity writes id to path in the canonical identity-file format read
|
||||
// by LoadIdentity, but REFUSES to overwrite an existing file: provisioning a new
|
||||
// identity must never silently clobber another process's private keys. The file
|
||||
// is created 0600 (it holds private keys). It is the write half of one-command
|
||||
// bot provisioning (`membershipd bot add --out <path>`): the freshly minted
|
||||
// identity it writes is exactly what LoadIdentity reconstructs, so a bot binary
|
||||
// (worker/clientcheck) consumes the credentials with no extra conversion step.
|
||||
func WriteNewIdentity(path string, id cs.Identity) error {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return fmt.Errorf("client: identity file %q already exists; refusing to overwrite", path)
|
||||
} else if !os.IsNotExist(err) {
|
||||
return fmt.Errorf("client: stat identity %q: %w", path, err)
|
||||
}
|
||||
return saveIdentity(path, id)
|
||||
}
|
||||
|
||||
func saveIdentity(path string, id cs.Identity) error {
|
||||
if dir := filepath.Dir(path); dir != "" {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user