Skip to content

FLT-75 go-live checklist — appsettings + secrets

Deployment model: single operator, single machine. This runs on Andrew's PC with Andrew's Slack and AgentMail tokens. The team never touches a token, installs nothing, and runs nothing. Slack and AgentMail are just a trusted-user allowlist (Slack IDs / email addresses) — Allyson, Shereeba, Nick, and Pam only send messages; the host on Andrew's machine reads them, authorizes against the allowlist, and replies. Tokens stay on Andrew's box, in Andrew's environment, and never leave it.

Fill-in-the-blanks to take the pilot from passive (connected to nothing) to a live, dry-run Slack/AgentMail intake, then to real replies. Nothing here starts an agent or writes Jira/Confluence/Drive — that stays gated on FLT-80 and Artifacts:Enabled=false.

Two golden rules 1. Secret VALUES never go in appsettings, and never leave this machine. The config carries secret names; the host reads the value from Andrew's environment/secret store at runtime. A token in appsettings, a manifest, Jira, Confluence, Slack, or a transcript is a leak. Requesters are not given tokens and cannot be — they authenticate to Slack/AgentMail as themselves; the host trusts them by ID. 2. Nothing goes live by accident. A transport connects only when its Enabled flag is true and its secrets resolve. Real replies are sent only when DryRun is false. Default is safe.


Step 0 — how the flags gate behavior

Flag Effect when set
Slack.Enabled + SLACK_APP_TOKEN + SLACK_BOT_TOKEN Slack listener connects and ingests events (records intakes)
AgentMail.Enabled + AgentMail.WebSocketUrl + AGENTMAIL_API_KEY + a real verifier AgentMail listener connects and ingests email
DryRun: true (default) Acks are logged, not sent ([DRY-SEND ...]). Ingest still happens. Use this first.
DryRun: false Acks are actually posted to Slack / sent via AgentMail
Artifacts:Enabled Leave false — Drive adapter + completion watcher are not built

Staged rollout: enable a transport with DryRun: true → watch ingest + dry-send logs → flip DryRun: false only after the synthetic-request checks pass (Andrew runs these on his machine; Nick can help verify).


Step 1 — appsettings (non-secret values)

Put deployment values in appsettings.Production.json next to the host (or environment overrides). Only the placeholders in CAPS change. Secrets are not here.

{
  "FounderIntake": {
    "Mode": "Pilot",
    "DryRun": true,                       // keep true until synthetic checks pass
    "Database": "founder-intake.db",      // absolute path recommended in production
    "OutboxPollSeconds": 5,

    "AllowedSlackChannelIds": [ "C_XXXXXXXXX" ],   // stable ID of #work-wingman-intake (NOT the name)

    "Requesters": [
      { "Name": "Allyson",  "SlackUserId": "U_XXXXXXXX", "Email": "allyson@EXAMPLE.com"  },
      { "Name": "Shereeba", "SlackUserId": "U_XXXXXXXX", "Email": "shereeba@EXAMPLE.com" },
      { "Name": "Nick",     "SlackUserId": "U_XXXXXXXX", "Email": "nick@EXAMPLE.com"     },
      { "Name": "Pam",      "SlackUserId": "U_XXXXXXXX", "Email": "pam@EXAMPLE.com"       }
    ],

    "Slack": {
      "Enabled": false,                   // -> true to connect Slack ingest
      "StopKeyword": "STOP",
      "QuietHours": { "TimeZone": "America/Chicago", "Start": "21:00", "End": "08:00" }
    },

    "AgentMail": {
      "Enabled": false,                   // -> true only after a real signature verifier is registered
      "Mailbox": "FOUNDER_INTAKE_AGENTMAIL_ADDRESS",
      "McpServerUrl": "",                 // from AUTHENTICATED AgentMail docs — do not guess
      "WebSocketUrl": "",                 // subscription endpoint for the dedicated mailbox only
      "SendUrl": ""                       // HTTPS send endpoint; blank keeps acks as dry-send logs
    },

    "Artifacts": { "Enabled": false, "FastFindEnabled": false, "CouncilIdeationEnabled": false },

    "Jira": { "Enabled": false, "ProjectKey": "FLT", "IssueType": "Task", "BaseUrl": "" },

    "Secrets": {                          // NAMES of env vars, not values
      "SlackBotToken": "SLACK_BOT_TOKEN",
      "SlackAppToken": "SLACK_APP_TOKEN",
      "AgentMailApiKey": "AGENTMAIL_API_KEY",
      "JiraApiToken": "JIRA_API_TOKEN",
      "ConfluenceApiToken": "CONFLUENCE_API_TOKEN"
    }
  }
}

Notes - Requesters[].Name must be one of Allyson / Shereeba / Nick / Pam (the pilot's approved set). The lowercased name is the stable identity the domain authorizes against — display names are not authorization. - SlackUserId and Email are the actual allowlist. Leave a channel/id out and events from it are rejected. - Duplicate Slack IDs or emails, or an enabled transport with an empty allowlist, fail the host at startup (by design) rather than misrouting.


Step 2 — secrets (Andrew's tokens, on Andrew's machine only)

Andrew sets each as an environment variable named exactly as referenced under Secrets above, in his own user/machine environment. These are Andrew's tokens; no one else has them and they never leave this box. Never commit them.

Env var Value Needed for
SLACK_APP_TOKEN Andrew's Slack app-level token (xapp-…, scope connections:write) Slack Socket Mode connect
SLACK_BOT_TOKEN Andrew's Slack bot token (xoxb-…) posting acks (chat.postMessage)
AGENTMAIL_API_KEY narrowly-scoped AgentMail key for the dedicated mailbox only AgentMail connect + send
JIRA_API_TOKEN Jira token (smallest perms) Jira handoff (still Jira.Enabled=false)
CONFLUENCE_API_TOKEN Confluence token Confluence handoff (later)

Options (pick one; all keep values out of git and on this machine). The host resolves each secret name from the environment first, then ~/.fleet-secrets/, so any of these works:

  • ~/.fleet-secrets/ drop-in (recommended — matches the existing jira/atlassian/vikunja convention). Just drop the files; no env vars, no restart-shell dance:
    // ~/.fleet-secrets/slack.json
    { "app_token": "xapp-…", "bot_token": "xoxb-…" }
    // ~/.fleet-secrets/agentmail.json
    { "api_key": "…" }
    
    Jira/Confluence already resolve from the existing ~/.fleet-secrets/jira.json (api_token) and atlassian.json (token) — nothing to add there.
  • User/machine env vars — setx SLACK_BOT_TOKEN "xoxb-…" (new shell to pick up), or the machine secret store.

(The host resolves secrets only from environment variables and ~/.fleet-secrets/ — it does not read dotnet user-secrets/appsettings for secret VALUES, so use one of the two options above.)

Verify a secret resolves without printing it: the startup log shows botToken=True/False per transport, never the value.


Step 3 — bring-up order

  1. Set the appsettings non-secret values (Step 1). Keep all Enabled false, DryRun: true.
  2. Set the secrets (Step 2).
  3. Slack.Enabled: true. Start the host. Expect the log: Slack Socket Mode connected. and, on a test message from an allowlisted requester in the intake channel, an Accepted slack intake … line plus a [DRY-SEND slack] … acknowledgement (not yet posted).
  4. Run the synthetic checks (Andrew, on his machine): allowlisted vs non-allowlisted user, wrong channel, duplicate event (dedup), STOP, quiet-hours (reply held, ingest recorded). All must reject/park/hold correctly. A requester who is not on the allowlist gets nothing — no token, no reply, no intake.
  5. Flip DryRun: false to post real acks. Re-send one synthetic request; confirm exactly one threaded reply.
  6. AgentMail: only after a real signature verifier replaces the fail-closed default, set AgentMail.Enabled: true with WebSocketUrl + AGENTMAIL_API_KEY, and (for real acks) SendUrl + DryRun: false.

Rollback (from rollback-kill-switch.md)

  • Stop replies: DryRun: true. Stop a transport: its Enabled: false. Both take effect on restart.
  • Revoke a token at the provider to cut access immediately; the listener goes passive on reconnect.
  • The SQLite DB is the audit record — disable, don't delete.