Skip to content

Latest commit

 

History

History
222 lines (171 loc) · 10.1 KB

File metadata and controls

222 lines (171 loc) · 10.1 KB

Tenant-Scoped Multi-User Operator Guide

This guide covers the bounded operator-managed multi-user model shipped in Day Captain.

Operating model

  • One deployment serves one Microsoft 365 tenant.
  • Users are explicit targets, not auto-discovered recipients.
  • Each digest run executes for exactly one target_user_id.
  • Persistence is partitioned by tenant_id and user_id.

Required hosted settings

  • DAY_CAPTAIN_ENV=production
  • DAY_CAPTAIN_DATABASE_URL=postgresql://...
  • DAY_CAPTAIN_JOB_SECRET=...
  • DAY_CAPTAIN_GRAPH_AUTH_MODE=app_only
  • DAY_CAPTAIN_GRAPH_TENANT_ID=...
  • DAY_CAPTAIN_GRAPH_CLIENT_ID=...
  • DAY_CAPTAIN_GRAPH_CLIENT_SECRET=...
  • DAY_CAPTAIN_TARGET_USERS=["alice@example.com","bob@example.com"] is not used directly by the app. Keep the app env var as CSV:
    • DAY_CAPTAIN_TARGET_USERS=alice@example.com,bob@example.com
  • optional DAY_CAPTAIN_GRAPH_SENDER_USER_ID=daycaptain@example.com when delivery should come from a dedicated shared mailbox
  • optional DAY_CAPTAIN_EMAIL_COMMAND_ALLOWED_SENDERS=assistant@example.com=alice@example.com when inbound mail commands should be accepted from a bounded helper sender set
  • DAY_CAPTAIN_GRAPH_SEND_ENABLED=true

Add a target user

  1. Confirm the user mailbox is inside the configured tenant.
  2. Add the mailbox identifier to DAY_CAPTAIN_TARGET_USERS.
  3. Ensure the Graph application permissions still cover the mailbox read and send operations.
  4. Trigger a single-user validation run for the new target before adding it to automated schedules.

Dedicated sender mailbox

  • Use DAY_CAPTAIN_GRAPH_SENDER_USER_ID=daycaptain@example.com when the digest should be sent from a shared mailbox such as daycaptain@....
  • In that mode, Day Captain still reads Outlook mail and calendar data from the selected target_user_id.
  • Delivery is routed through /users/{sender}/sendMail, so confirm the dedicated mailbox exists and the app-only Graph permissions cover it.
  • Keep the target user explicit in hosted runs; otherwise the sender mailbox is not enough to infer which mailbox should be analyzed.

Inbound email-command recall

  • The shipped command surface is bounded to recall, recall-today, and recall-week.
  • recall and recall-today generate a digest for the current day in DAY_CAPTAIN_DISPLAY_TIMEZONE.
  • recall-week generates a digest from Monday 00:00 through now in DAY_CAPTAIN_DISPLAY_TIMEZONE.
  • Sender resolution is strict:
    • the feature is disabled unless DAY_CAPTAIN_EMAIL_COMMAND_ALLOWED_SENDERS is configured
    • single-user deployments must explicitly list allowed senders through DAY_CAPTAIN_EMAIL_COMMAND_ALLOWED_SENDERS
    • multi-user deployments may authorize helper senders only through explicit sender=target mappings in DAY_CAPTAIN_EMAIL_COMMAND_ALLOWED_SENDERS
  • Duplicate suppression is keyed by inbound command_message_id, so replaying the same inbound message should not regenerate a second digest.
  • The first recommended transport bridge is Power Automate on top of the shared mailbox trigger. See power_automate_shared_mailbox_recall_setup.md.

Manual validation

Validate the hosted env contract before triggering runs:

PYTHONPATH=src python3 -m day_captain validate-config
PYTHONPATH=src python3 -m day_captain validate-config --target-user alice@example.com

Run a single target through the CLI:

PYTHONPATH=src python3 -m day_captain morning-digest --force --target-user alice@example.com

Recall the latest digest for one target:

PYTHONPATH=src python3 -m day_captain recall-digest --target-user alice@example.com

Trigger the hosted job directly:

curl -X POST "$DAY_CAPTAIN_SERVICE_URL/jobs/morning-digest" \
  -H "Content-Type: application/json" \
  -H "X-Day-Captain-Secret: $DAY_CAPTAIN_JOB_SECRET" \
  -d '{"force":false,"target_user_id":"alice@example.com"}'

Run the hosted validation flow:

DAY_CAPTAIN_SERVICE_URL=... \
DAY_CAPTAIN_JOB_SECRET=... \
PYTHONPATH=src python3 -m day_captain validate-hosted-service \
  --target-user alice@example.com \
  --wake-service \
  --wake-timeout-seconds 45 \
  --wake-max-attempts 6 \
  --wake-delay-seconds 10 \
  --timeout-seconds 90 \
  --expect-graph-auth-mode app_only \
  --expect-storage-backend postgres

Run the hosted validation flow including inbound email-command recall:

DAY_CAPTAIN_SERVICE_URL=... \
DAY_CAPTAIN_JOB_SECRET=... \
PYTHONPATH=src python3 -m day_captain validate-hosted-service \
  --target-user alice@example.com \
  --wake-service \
  --wake-timeout-seconds 45 \
  --wake-max-attempts 6 \
  --wake-delay-seconds 10 \
  --timeout-seconds 90 \
  --expect-graph-auth-mode app_only \
  --expect-storage-backend postgres \
  --check-email-command \
  --email-command-sender alice@example.com \
  --email-command-text recall-week

If the hosted service can sleep between runs, use --wake-service instead of assuming the first scheduler call will execute immediately.

For several target users, prefer a separate readiness pass before fan-out:

DAY_CAPTAIN_SERVICE_URL=... \
DAY_CAPTAIN_JOB_SECRET=... \
PYTHONPATH=src python3 -m day_captain check-hosted-health \
  --wake-service \
  --wake-timeout-seconds 45 \
  --wake-max-attempts 6 \
  --wake-delay-seconds 10 \
  --expect-graph-auth-mode app_only \
  --expect-storage-backend postgres

Then use trigger-only calls for the routine schedule:

DAY_CAPTAIN_SERVICE_URL=... \
DAY_CAPTAIN_JOB_SECRET=... \
PYTHONPATH=src python3 -m day_captain trigger-hosted-job \
  --job morning-digest \
  --target-user alice@example.com \
  --timeout-seconds 90

Trigger a bounded email-command recall manually:

DAY_CAPTAIN_SERVICE_URL=... \
DAY_CAPTAIN_JOB_SECRET=... \
PYTHONPATH=src python3 -m day_captain trigger-hosted-job \
  --job email-command-recall \
  --message-id inbound-001 \
  --sender-address alice@example.com \
  --command-text recall-week \
  --timeout-seconds 90

Scheduling model

Production scheduling should use Power Automate recurrence. It calls the hosted Day Captain job endpoints directly, using Europe/Paris schedule semantics without GitHub cron jitter, checkout, Python setup, or package installation.

Use power_automate_scheduler_setup.md as the scheduler runbook.

The production Power Automate scheduler has two flows:

  • Day Captain - Morning Digest: Monday-Friday at 08:45 Europe/Paris, calling /jobs/morning-digest.
  • Day Captain - Weekly Digest: Sunday at 20:30 Europe/Paris, calling /jobs/weekly-digest.

Each flow:

  • warms the hosted service with authenticated GET /healthz
  • fans out over explicit target users from a tenant-managed JSON array
  • sends one content-free POST per target user
  • keeps concurrency at one run
  • leaves digest generation, Graph access, storage, and delivery inside Day Captain

Keep the private GitHub Actions workflows as manual fallback only:

  • workflow_dispatch can trigger a one-off target user override.
  • schedule: blocks should be removed only after successful Power Automate morning and weekly run evidence is recorded.
  • The copy-ready GitHub scheduler templates remain useful for rollback and debugging, not as the primary production scheduler.

Sleeping-service fallback

If the hosted web service is on a plan that can sleep:

  • use Power Automate to call authenticated GET /healthz as a warm-up step before the real trigger
  • for multi-user schedules, do that warm-up once before the per-user fan-out rather than once per user
  • use trigger-only job calls for the weekday and Sunday schedules, and keep full validate-hosted-service runs for manual validation or rollout checks
  • allow bounded retries before POST /jobs/morning-digest
  • allow bounded retries before POST /jobs/weekly-digest
  • use longer timeout settings than for an always-on deployment
  • keep this as a fallback mode only; prefer a paid always-on service for routine production delivery

Use private_ops_repo_bootstrap.md as the starting point for private ops fallback docs.

Isolation checks

After adding or changing users, validate:

  • each scheduled or manual run returns 200
  • each user receives only their own digest
  • recall for alice@example.com never returns bob@example.com's latest run
  • if a dedicated sender mailbox is configured, delivery still arrives for the target user while the visible sender is daycaptain@...
  • if inbound email-command recall is enabled, only authorized senders can trigger it and replaying the same inbound command_message_id is deduplicated
  • if inbound email-command recall uses helper senders in multi-user mode, each helper sender must map to exactly one target user and ambiguous mappings must fail explicitly
  • a run left in delivery_pending still means delivery may already have happened and requires reconciliation before another send
  • a run marked delivery_failed means Graph prerequisites or delivery failed before acceptance was likely, so a later retry is expected to be safe
  • feedback recorded against one user changes only that user's preferences
  • persisted rows for messages, meetings, runs, feedback, and preferences stay partitioned by tenant_id and user_id

Mailbox evidence and retention

  • Treat configured target users and the dedicated sender as the complete authorized mailbox set; requests for any other target must fail before Graph collection or delivery.
  • Authentication codes, password-reset messages, magic links, and equivalent authentication content are dropped before storage or model processing.
  • Do not copy mailbox subjects, previews, bodies, identities, URLs, tokens, or secrets into logs, tickets, fixtures, screenshots, or telemetry.
  • Keep mailbox-derived audit exports only for the duration of the audit, restrict them to the operator performing it, and delete them immediately after aggregate content-free metrics are recorded.
  • Microsoft 365 retention for the sender mailbox remains an administrator policy; Day Captain does not create a second content archive.

Rollout note

Use this model for bounded operator-managed deployments. It is not a self-service tenant administration system.