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:Jira/Confluence already resolve from the existing// ~/.fleet-secrets/slack.json { "app_token": "xapp-…", "bot_token": "xoxb-…" } // ~/.fleet-secrets/agentmail.json { "api_key": "…" }~/.fleet-secrets/jira.json(api_token) andatlassian.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¶
- Set the
appsettingsnon-secret values (Step 1). Keep allEnabledfalse,DryRun: true. - Set the secrets (Step 2).
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, anAccepted slack intake …line plus a[DRY-SEND slack] …acknowledgement (not yet posted).- 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. - Flip
DryRun: falseto post real acks. Re-send one synthetic request; confirm exactly one threaded reply. - AgentMail: only after a real signature verifier replaces the fail-closed default, set
AgentMail.Enabled: truewithWebSocketUrl+AGENTMAIL_API_KEY, and (for real acks)SendUrl+DryRun: false.
Rollback (from rollback-kill-switch.md)¶
- Stop replies:
DryRun: true. Stop a transport: itsEnabled: 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.