Add send-email action with per-user permission model #2

Closed
opened 2026-03-17 21:01:36 +00:00 by mikael-lovqvists-claude-agent · 0 comments

Overview

Add email sending support with a runtime-toggleable permission model. The human user controls which (agent, recipient, topic) combinations are allowed. Once granted, emails on that combination flow without queue interaction.


New actions

send-email

  • Policy: auto-accept (permission check is inside the handler)
  • Params: to, subject, body, topic
  • Handler checks mail_perm_store.check(caller, to, topic) before sending. Throws if not permitted.

set-mail-permission

  • Policy: auto-accept
  • Params: target_user, to, topic, allow (bool)
  • Only callable by a user whose canApprove includes target_user — enforced in the handler via check_can_approve. Agent calling this gets an immediate error, not a queue entry (prevents social-engineering).

get-mail-permissions

  • Policy: auto-accept
  • Params: target_user (optional)
  • Returns current permission entries. Readable by any authenticated user.

Permission data shape (mail-permissions.json)

{
  "allowed": [
    { "user": "agent", "to": "mikael@example.com", "topic": "deployments" },
    { "user": "agent", "to": "ops@example.com",    "topic": "alerts" }
  ]
}

All three fields must match exactly. No wildcards in v1. Persisted to disk on every mutation, loaded at startup. Path configured via --mail-perms <path> or CONDUIT_MAIL_PERMS env var. If not set, runs in-memory-only with a startup warning.


SMTP config (in secrets.json)

{
  "smtp": {
    "host": "smtp.example.com",
    "port": 587,
    "secure": false,
    "auth": { "user": "relay@example.com", "pass": "..." },
    "from": "agent@example.com"
  },
  "users": { ... }
}

Optional key — if absent, send-email fails with 'SMTP not configured'.


Handler context threading

Current handler signature: handler(params)
New signature: handler(params, ctx) where ctx = { caller, users, mail_perm_store, mailer_send }

Existing handlers ignore ctx, so this is backwards-compatible. Both handler call sites in index.mjs (auto-accept and queue-approval) change to def.handler(params, ctx).


New files

  • server/mailer.mjs — nodemailer transport wrapper, create_mailer(smtp_config) returns a send(to, subject, body) async fn
  • server/mail_perms.mjs — permission store, load_mail_perms(path) returns store with check, add, remove, list

Modified files

  • server/index.mjs — add --mail-perms arg, build ctx, thread into handlers
  • server/actions.mjs — add 3 new actions, import check_can_approve from ./auth.mjs
  • server/secrets.mjs — return smtp alongside users
  • secrets.example.json — add smtp block
  • package.json — add nodemailer dependency

Implementation order

  1. Add nodemailer to package.json
  2. Implement server/mailer.mjs
  3. Implement server/mail_perms.mjs
  4. Update server/secrets.mjs to return smtp
  5. Update server/index.mjs--mail-perms, build ctx, change call sites
  6. Add three actions to server/actions.mjs
  7. Update secrets.example.json
## Overview Add email sending support with a runtime-toggleable permission model. The human user controls which `(agent, recipient, topic)` combinations are allowed. Once granted, emails on that combination flow without queue interaction. --- ## New actions ### `send-email` - **Policy**: `auto-accept` (permission check is inside the handler) - **Params**: `to`, `subject`, `body`, `topic` - Handler checks `mail_perm_store.check(caller, to, topic)` before sending. Throws if not permitted. ### `set-mail-permission` - **Policy**: `auto-accept` - **Params**: `target_user`, `to`, `topic`, `allow` (bool) - Only callable by a user whose `canApprove` includes `target_user` — enforced in the handler via `check_can_approve`. Agent calling this gets an immediate error, not a queue entry (prevents social-engineering). ### `get-mail-permissions` - **Policy**: `auto-accept` - **Params**: `target_user` (optional) - Returns current permission entries. Readable by any authenticated user. --- ## Permission data shape (`mail-permissions.json`) ```json { "allowed": [ { "user": "agent", "to": "mikael@example.com", "topic": "deployments" }, { "user": "agent", "to": "ops@example.com", "topic": "alerts" } ] } ``` All three fields must match exactly. No wildcards in v1. Persisted to disk on every mutation, loaded at startup. Path configured via `--mail-perms <path>` or `CONDUIT_MAIL_PERMS` env var. If not set, runs in-memory-only with a startup warning. --- ## SMTP config (in `secrets.json`) ```json { "smtp": { "host": "smtp.example.com", "port": 587, "secure": false, "auth": { "user": "relay@example.com", "pass": "..." }, "from": "agent@example.com" }, "users": { ... } } ``` Optional key — if absent, `send-email` fails with `'SMTP not configured'`. --- ## Handler context threading Current handler signature: `handler(params)` New signature: `handler(params, ctx)` where `ctx = { caller, users, mail_perm_store, mailer_send }` Existing handlers ignore `ctx`, so this is backwards-compatible. Both handler call sites in `index.mjs` (auto-accept and queue-approval) change to `def.handler(params, ctx)`. --- ## New files - `server/mailer.mjs` — nodemailer transport wrapper, `create_mailer(smtp_config)` returns a `send(to, subject, body)` async fn - `server/mail_perms.mjs` — permission store, `load_mail_perms(path)` returns store with `check`, `add`, `remove`, `list` ## Modified files - `server/index.mjs` — add `--mail-perms` arg, build `ctx`, thread into handlers - `server/actions.mjs` — add 3 new actions, import `check_can_approve` from `./auth.mjs` - `server/secrets.mjs` — return `smtp` alongside `users` - `secrets.example.json` — add `smtp` block - `package.json` — add `nodemailer` dependency --- ## Implementation order 1. Add `nodemailer` to `package.json` 2. Implement `server/mailer.mjs` 3. Implement `server/mail_perms.mjs` 4. Update `server/secrets.mjs` to return `smtp` 5. Update `server/index.mjs` — `--mail-perms`, build `ctx`, change call sites 6. Add three actions to `server/actions.mjs` 7. Update `secrets.example.json`
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mikael-lovqvists-claude-agent/claude-code-conduit#2