feat(cli): two-key role routing + init bootstrap

openStore(role) selects the DEK wrap slot; admin commands require
EMCLI_ADMIN_KEY (admin slot only, no agent fallback); init writes both
slots from both keys. Test helpers seed the wrap slots.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-22 22:59:16 +01:00
parent cb0425f18d
commit 9d946b1b03
9 changed files with 158 additions and 27 deletions
+40 -9
View File
@@ -25,17 +25,48 @@ func realMailer(acc store.Account) (Mailer, error) {
return c, nil
}
// openStore loads the key and opens the DB, returning a human-readable error string.
func openStore() (*store.Store, error) {
key, err := crypto.KeyFromEnv()
if err != nil {
return nil, err
// commandRole maps a command to the privilege it requires. Admin commands
// mutate configuration or expose oversight data; everything else is agent.
func commandRole(cmd string) store.Role {
switch cmd {
case "account", "whitelist", "config", "audit":
return store.RoleAdmin
default: // list, get, search, ack, send, doctor
return store.RoleAgent
}
}
// openStore resolves the keys for the role, opens the DB, and unlocks the DEK.
// Admin commands require EMCLI_ADMIN_KEY and unlock the admin slot only; agent
// commands use EMCLI_KEY (falling back to the admin key if that is all there is).
func openStore(role store.Role) (*store.Store, error) {
adminKey, adminErr := crypto.AdminKeyFromEnv()
agentKey, agentErr := crypto.AgentKeyFromEnv()
switch role {
case store.RoleAdmin:
if adminErr != nil {
return nil, fmt.Errorf("this command requires EMCLI_ADMIN_KEY (admin privilege)")
}
case store.RoleAgent:
if agentErr != nil && adminErr != nil {
return nil, agentErr // "EMCLI_KEY is not set"
}
}
path, err := store.DefaultDBPath()
if err != nil {
return nil, err
}
return store.Open(path, key)
st, err := store.Open(path)
if err != nil {
return nil, err
}
if err := st.Unlock(role, adminKey, agentKey); err != nil {
st.Close()
return nil, err
}
return st, nil
}
func realSender(acc store.Account, m mail.OutgoingMessage) error {
@@ -79,7 +110,7 @@ func runDoctor(args []string, out, errOut io.Writer) int {
}
return 2
}
st, err := openStore()
st, err := openStore(store.RoleAgent)
if err != nil {
fmt.Fprintf(errOut, "emcli: %v\n", err)
return 1
@@ -159,7 +190,7 @@ func runAgent(cmd string, args []string, out, errOut io.Writer) int {
_ = Failure(CodeUsage, "--account is required").Write(out)
return 2
}
st, err := openStore()
st, err := openStore(store.RoleAgent)
if err != nil {
_ = Failure(CodeConfig, err.Error()).Write(out)
return 1
@@ -249,7 +280,7 @@ func runSend(args []string, out, errOut io.Writer) int {
_ = Failure(CodeUsage, "--account is required").Write(out)
return 2
}
st, err := openStore()
st, err := openStore(store.RoleAgent)
if err != nil {
_ = Failure(CodeConfig, err.Error()).Write(out)
return 1