diff --git a/HANDOFF.md b/HANDOFF.md index 5312fed..c6dd032 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -4,11 +4,11 @@ - GitHub: `https://github.com/Jdelg718/BotFucker` - Default branch: `main` -- Latest merged milestone: Phase 15 Emergency-Stop Bridge Rehearsal (`feat: add emergency-stop bridge rehearsal`, main at 3b1075e) -- Current working branch: `phase-16-outlook-warning-draft-sandbox-contract` -- Current PR target: Phase 16 Microsoft Outlook Warning-Draft Sandbox Contract — docs/tests only, no OAuth/live provider mutation/no send -- Selected next provider/action target: Microsoft Outlook warning draft in sandbox/manual reviewed bridge contract; no send-reply mutation yet -- Current local demo target: demonstrate deterministic local review, optional mocked LLM classifier fallback/validation, approved-action export, dry-run n8n bridge contract, fail-closed YOLO policy checks, real n8n import validation results, the Phase 13 Reviewed Action Bridge Promotion Plan, durable bridge ledger dedupe scaffold, and emergency-stop bridge rehearsal +- Latest merged milestone: Phase 16 Microsoft Outlook Warning-Draft Sandbox Contract (`0377242`) +- Current working branch: `phase-17-inactive-outlook-warning-draft-workflow-scaffold` +- Current PR target: Phase 17 Inactive Outlook Warning-Draft Workflow Scaffold — docs/tests/workflow scaffold only, no credentials/no activation/no mail delivery +- Selected next provider/action target: sandbox import/rehearsal or operator validation of the inactive Microsoft Outlook warning-draft scaffold; no live send/OAuth +- Current local demo target: demonstrate deterministic local review, optional mocked LLM classifier fallback/validation, approved-action export, dry-run n8n bridge contract, fail-closed YOLO policy checks, real n8n import validation results, durable bridge ledger dedupe scaffold, emergency-stop bridge rehearsal, and inactive/manual Outlook warning-draft scaffold - Current promo artifact: `promo/botfucker-animated-explainer/renders/botfucker-animated-explainer_narrated-final.mp4` ## What BotFucker Is @@ -27,7 +27,7 @@ Important files: ```text DESIGN.md # v2 architecture and principles -ROADMAP.md # phased product roadmap, current through Phase 15 emergency-stop bridge rehearsal +ROADMAP.md # phased product roadmap, current through Phase 17 inactive Outlook warning-draft scaffold docs/n8n-import-validation.md # Phase 12 real n8n import/dry-run procedure and results docs/webhook-contract.md # normalized n8n/webhook JSON contract docs/n8n-workflow.json # importable n8n import starter workflow @@ -39,6 +39,8 @@ docs/reviewed-action-bridge-promotion-plan.md # Phase 13 reviewed live-bridge ga docs/bridge-ledger-scaffold.md # Phase 14 durable processed-audit ledger scaffold; no OAuth/live mutation docs/bridge-rehearsal.md # Phase 15 emergency-stop/dry-run bridge rehearsal; no provider execution docs/microsoft-outlook-warning-draft-sandbox.md # Phase 16 Outlook warning-draft sandbox contract; no send +docs/n8n-outlook-warning-draft-scaffold.json # Phase 17 inactive/manual Outlook warning-draft n8n scaffold +docs/n8n-outlook-warning-draft-scaffold.md # Phase 17 operator guide: emergency stop, dedupe, rollback/manual deletion README.md # user-facing setup and project overview outreach_filter.py # compatibility CLI wrapper botfucker/models.py # normalized email/classification/review models @@ -72,6 +74,7 @@ These are non-negotiable: - Provider-side actions are future bridge work, not local UI behavior. - The Phase 14 durable bridge ledger records `audit_id` state only (`bridge_ledger_state_only`) before provider mutation; it is not a provider action engine. - The Phase 15 bridge rehearsal is dry-run-only; emergency stop exits before ledger claim, and provider execution remains `not_performed`. +- The Phase 17 Outlook warning-draft workflow is inactive/manual; its Graph `createReply` placeholder is disabled and unconnected, with no auth material in the export. ## Current Test Commands @@ -273,45 +276,63 @@ python3 -m py_compile outreach_filter.py botfucker/*.py scripts/validate_n8n_wor python3 -m unittest discover -s tests -v ``` +### Phase 13 — Reviewed Action Bridge Promotion Plan + +Delivered operator/security/ops promotion gates for one provider action at a time, with credential ownership in n8n only, rollback/emergency-stop requirements, sandbox/manual test expectations, and no live mutation. + +### Phase 14 — Durable Bridge Ledger Scaffold + +Delivered `botfucker.bridge_ledger.DurableBridgeLedger`, durable `audit_id` dedupe state, status transitions, docs, and tests proving IDs/status only with no provider calls or secret columns. + +### Phase 15 — Emergency-Stop Bridge Rehearsal + +Delivered `botfucker.bridge_rehearsal.rehearse_approved_actions()`, default-on emergency stop, duplicate skip behavior, dry-run logging only, docs, and tests proving provider execution remains `not_performed`. + +### Phase 16 — Microsoft Outlook Warning-Draft Sandbox Contract + +Delivered `docs/microsoft-outlook-warning-draft-sandbox.md`, selecting Microsoft Outlook/Graph `createReply` as the warning-draft-only target, documenting `Mail.ReadWrite` as a permission note, and forbidding live delivery or other mailbox mutations. + +### Phase 17 — Inactive Outlook Warning-Draft Workflow Scaffold + +Delivered inactive/manual `docs/n8n-outlook-warning-draft-scaffold.json`, operator guide `docs/n8n-outlook-warning-draft-scaffold.md`, emergency-stop/dedupe scaffold, disabled/unconnected Graph `createReply` placeholder, validator coverage, and tests proving no credentials, no activation, no connected provider action, and no forbidden mailbox mutation strings in the workflow export. + ## Next PR Recommendation -After Phase 14, keep OAuth on hold. The next safe step is emergency-stop proof or a sandbox-only bridge rehearsal that uses the durable bridge ledger and still performs no production provider mutation. +After Phase 17, keep OAuth on hold. The next safe step is sandbox import/rehearsal or operator validation of the inactive Outlook warning-draft scaffold, still without production provider mutation. -Do **not** add real OAuth, provider credentials, or live n8n provider mutation nodes until the Phase 13 gate has Rex/Gus review and provider-specific sandbox evidence. +Do **not** add real OAuth, provider credentials, or live n8n provider delivery nodes. Keep Rex/Gus review and sandbox evidence ahead of any connected provider action. Suggested follow-up scope: -1. Review Phase 14 ledger scaffold with Rex/Gus. -2. Keep the first provider/action pair to `approve_warning` only. -3. Use processed-`audit_id` state before any provider mutation attempt. -4. Keep credentials in n8n only. -5. Require rollback and emergency-stop proof before any live provider action node is connected. +1. Import `docs/n8n-outlook-warning-draft-scaffold.json` into sandbox n8n as inactive. +2. Run the manual path with emergency stop on and fake input only. +3. Confirm the disabled Graph `createReply` placeholder remains unconnected unless Rex/Gus approve a sandbox-only rehearsal. +4. Document import behavior, cleanup, rollback, and manual draft deletion notes. +5. Keep credentials in n8n/operator infrastructure only. ## Suggested Prompt for Kodex/Codex ```text You are working on BotFucker, an AI-era inbox defense app. -Read DESIGN.md, ROADMAP.md, HANDOFF.md, README.md, docs/webhook-contract.md, docs/n8n-workflow.md, docs/n8n-approved-action-bridge.md, docs/n8n-import-validation.md, docs/provider-auth-plan.md, docs/reviewed-action-bridge-promotion-plan.md, and docs/bridge-ledger-scaffold.md. +Read DESIGN.md, ROADMAP.md, HANDOFF.md, README.md, docs/webhook-contract.md, docs/n8n-workflow.md, docs/n8n-approved-action-bridge.md, docs/n8n-import-validation.md, docs/provider-auth-plan.md, docs/reviewed-action-bridge-promotion-plan.md, docs/bridge-ledger-scaffold.md, docs/bridge-rehearsal.md, docs/microsoft-outlook-warning-draft-sandbox.md, and docs/n8n-outlook-warning-draft-scaffold.md. -First, verify the current Phase 14 branch without changing behavior: +First, verify the current Phase 17 branch without changing behavior: - run python3 scripts/validate_n8n_workflow_exports.py - run python3 -m py_compile outreach_filter.py botfucker/*.py scripts/validate_n8n_workflow_exports.py - run python3 -m unittest discover -s tests -v -- inspect docs/bridge-ledger-scaffold.md and botfucker/bridge_ledger.py -- confirm the ledger stores durable audit_id/status data only and has no provider calls or credential fields - -Then review Phase 14 only: Durable Bridge Ledger Scaffold. +- inspect docs/n8n-outlook-warning-draft-scaffold.json and docs/n8n-outlook-warning-draft-scaffold.md +- confirm the workflow is inactive/manual, has no credentials, and keeps the Graph createReply placeholder disabled and unconnected -Check that claim_action records pending state before provider mutation, duplicate audit_id claims fail closed, unsafe approved-action exports are rejected, and no OAuth/provider credentials/live provider mutation nodes were added. +Then review Phase 17 only: Inactive Outlook Warning-Draft Workflow Scaffold. -Do not add real OAuth. Do not add provider credentials. Do not attach Gmail/Microsoft/IMAP/SMTP mutation credentials. Do not enable live n8n provider actions. Preserve the provider boundary: live provider execution remains separately reviewed and guarded. +Do not add real OAuth. Do not add provider credentials. Do not activate n8n. Do not connect live Microsoft provider delivery. Preserve the provider boundary: the next step is sandbox import/rehearsal or operator validation, not live send/OAuth. ``` ## Team Plan - **Amy**: orchestration and scope control. She keeps the product from wandering into OAuth swamp country before bridge promotion is reviewed. -- **Chip**: owns durable bridge ledger scaffold and bridge promotion safety docs/tests. +- **Chip**: owns bridge promotion docs/tests, inactive/manual Outlook draft scaffolding, and dry-run-to-sandbox safety reviews. - **Rex**: security veto on processed-audit dedupe, credential absence, live-action safety gates, provider-boundary isolation, and XSS regressions. - **Gus**: n8n operator verification, dry-run bridge observability, cleanup steps, CI, ledger operability, and operator docs. - **Fred**: provider sandbox/action-limit research only; no direct OAuth implementation yet. @@ -323,15 +344,15 @@ Do not add real OAuth. Do not add provider credentials. Do not attach Gmail/Micr - No production OAuth yet. - Real n8n import validation passed on n8n-vps with sample-only dry-run and cleanup; do not activate those workflows without a separate reviewed bridge-promotion plan. - n8n approved action bridge is dry-run only; live provider actions still need a separate explicit reviewed workflow. -- Phase 14 durable bridge ledger scaffold exists for processed-`audit_id` dedupe, but it is not connected to live provider mutation. +- Phase 17 Outlook warning-draft scaffold is inactive/manual only; Graph `createReply` is disabled and unconnected until sandbox import/rehearsal or operator validation. - YOLO guardrails exist but live provider actions still require explicit operator configuration and must not be casually enabled. ## Tomorrow Restart -- Phase 14 branch is local: `phase-14-durable-bridge-ledger`. -- First move next: re-check tests, review diff, and open/squash Phase 14 if green. -- Phase 15 target should be emergency-stop proof or sandbox-only bridge rehearsal using the durable ledger. -- Do **not** add OAuth, provider credentials, or live n8n provider mutation nodes. +- Phase 17 branch is local: `phase-17-inactive-outlook-warning-draft-workflow-scaffold`. +- First move next: re-check tests, review diff, and open/squash Phase 17 if green. +- Phase 18 target should be sandbox import/rehearsal or operator validation of the inactive Outlook warning-draft scaffold. +- Do **not** add OAuth, provider credentials, activate n8n, or connect live provider delivery. - Keep live provider execution separate, reviewed, audited, deduped by processed `audit_id`, rollback-ready, and security/ops-reviewed. ## Product Voice diff --git a/README.md b/README.md index 371b7f5..0543420 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ See [DESIGN.md](DESIGN.md) for the proposed architecture and roadmap. - Provides a Phase 14 durable bridge ledger scaffold for recording processed `audit_id` state before provider mutation. - Provides a Phase 15 dry-run bridge rehearsal that proves emergency stop and duplicate-skip behavior before provider mutation. - Documents the selected Phase 16 sandbox target: Microsoft Outlook warning-draft creation only, with no send-reply mutation. +- Provides a Phase 17 inactive/manual n8n Outlook warning-draft scaffold with a disabled, unconnected Graph `createReply` placeholder. - Keeps provider credentials and live mailbox side effects outside the local UI and review queue. ## Safety First @@ -509,6 +510,17 @@ Safety constraints: - no OAuth, no provider credentials, and no live provider mutation nodes are added - checked-in n8n workflows remain inactive/dry-run starters +## Phase 17 Inactive Outlook Warning-Draft Workflow Scaffold + +Phase 17 adds an inactive/manual n8n scaffold for the selected Microsoft Outlook `approve_warning` draft path. It is still a scaffold: no OAuth setup, no checked-in auth material, no activation, and no mail delivery. + +Artifacts: + +- [`docs/n8n-outlook-warning-draft-scaffold.json`](docs/n8n-outlook-warning-draft-scaffold.json) — inactive n8n workflow with manual trigger, fake approved-action input, emergency-stop/dedupe validation, a draft-only summary node, and a disabled/unconnected Graph `createReply` placeholder. +- [`docs/n8n-outlook-warning-draft-scaffold.md`](docs/n8n-outlook-warning-draft-scaffold.md) — operator checklist covering emergency stop, dedupe, rollback, and manual draft deletion. + +Next step: sandbox import/rehearsal or operator validation of this scaffold. Do not jump from this artifact to live delivery or broad OAuth work. + ## Test Before Going Live Compile-check the script and package: diff --git a/ROADMAP.md b/ROADMAP.md index 2342d66..3641063 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -31,6 +31,7 @@ Merged through PR #8: - reviewed live-bridge promotion gate - durable bridge ledger scaffold for processed `audit_id` state before provider mutation - tests for classifier/history/safety/review/webhook/docs/branding/bridge-ledger behavior +- inactive/manual Microsoft Outlook warning-draft workflow scaffold with disabled Graph `createReply` placeholder The repo is ready to pull locally into Kodex/Codex and demonstrate the local cockpit without connecting to any live mail provider. @@ -414,6 +415,12 @@ Status: implemented on `phase-16-outlook-warning-draft-sandbox-contract`. Goal: document Kent's selected first provider/action target without adding OAuth, credentials, or live mutation behavior. +Selected target: + +- Provider: Microsoft Outlook +- First action: create/save a warning draft only +- Scope: sandbox/manual reviewed bridge contract first; no send-reply mutation yet + Delivered: - `docs/microsoft-outlook-warning-draft-sandbox.md` @@ -432,33 +439,52 @@ Acceptance criteria: - Credentials stay outside BotFucker and inside n8n/operator infrastructure. - Current workflow exports remain provider-call-free and credential-free. -## Near-Term Recommendation +### Phase 17 — Inactive Outlook Warning-Draft Workflow Scaffold ✅ -After Phase 16 is reviewed and merged, the next PR should be **Phase 17: inactive Microsoft Outlook warning-draft workflow scaffold**, not broad OAuth implementation. +Status: implemented on `phase-17-inactive-outlook-warning-draft-workflow-scaffold`. -Kent selected the first provider/action target: +Goal: provide an inactive/manual n8n scaffold for the selected Microsoft Outlook warning-draft path without adding credentials, activation, live provider execution, or mail delivery. -- Provider: Microsoft Outlook -- First action: create/save a warning draft only -- Scope: sandbox/manual reviewed bridge contract first; no send-reply mutation yet +Delivered: + +- `docs/n8n-outlook-warning-draft-scaffold.json` +- `docs/n8n-outlook-warning-draft-scaffold.md` +- manual-trigger-only inactive workflow export +- fake approved-action input and draft-only safety summary +- emergency-stop default-on check and `audit_id` duplicate guard +- disabled, unconnected Microsoft Graph `createReply` HTTP placeholder +- validator/test coverage proving no credential material, no forbidden mailbox mutation strings, and no connected provider action step +- operator docs for emergency stop, dedupe, rollback, and manual deletion of sandbox drafts + +Acceptance criteria: + +- Workflow remains `active: false` and manual-triggered only. +- Placeholder Graph draft node is disabled and unconnected. +- No credentials, tokens, or live auth material are committed. +- No forbidden send/delete/move/archive/read-state/rules/contact/settings action is present in the workflow export. +- Next step is sandbox import/rehearsal or operator validation, not live delivery or broad OAuth. + +## Near-Term Recommendation + +After Phase 17 is reviewed and merged, the next PR should be **Phase 18: sandbox import/rehearsal and operator validation for the inactive Outlook draft scaffold**, not live send/OAuth. Recommended scope: -- keep one provider/action pair only (`approve_warning`) -- document the exact sandbox mailbox/provider target Kent wants to use -- keep credentials in n8n only -- keep dry-run as the default path -- map the Phase 15 rehearsal outcomes onto an inactive n8n/operator checklist -- require Rex/Gus security/ops review before any live mutation node is connected +- import the Phase 17 scaffold into a sandbox n8n instance as inactive +- verify the manual path with emergency stop on and fake input only +- verify the disabled Graph placeholder remains disconnected unless Rex/Gus approve a sandbox-only rehearsal +- document observed n8n import behavior, cleanup, and rollback notes +- keep credentials in n8n/operator infrastructure only +- keep the provider action to Microsoft Graph `createReply` draft creation only -OAuth can still wait. Phase 16 picks the sandbox road. Next is building the inactive/manual Outlook draft scaffold — not handing the robot live mailbox keys because apparently we enjoy learning by fire. +OAuth can still wait. The next useful work is proving the inactive scaffold imports and rehearses cleanly, not handing the robot live mailbox keys because apparently we enjoy learning by fire. -### Restart checklist after Phase 16 +### Restart checklist after Phase 17 -1. Re-check Phase 16 PR CI and mergeability. -2. Squash-merge Phase 16 into `main` if still green. -3. Build only an inactive/manual Microsoft Outlook warning-draft workflow scaffold. -4. Keep the provider action to create/save draft only; do not send replies. +1. Re-check Phase 17 PR CI and mergeability. +2. Squash-merge Phase 17 into `main` if still green. +3. Import only the inactive/manual scaffold into sandbox n8n for rehearsal. +4. Keep the provider action to create/save draft only; do not perform live delivery. 5. Keep provider credentials inside n8n/operator infrastructure only; do not put secrets in BotFucker. ## Team Utilization diff --git a/docs/n8n-outlook-warning-draft-scaffold.json b/docs/n8n-outlook-warning-draft-scaffold.json new file mode 100644 index 0000000..d77f714 --- /dev/null +++ b/docs/n8n-outlook-warning-draft-scaffold.json @@ -0,0 +1,97 @@ +{ + "id": "botfucker-outlook-warning-draft-scaffold-v1", + "name": "BotFucker Outlook Warning Draft Scaffold", + "nodes": [ + { + "parameters": {}, + "id": "manual-trigger", + "name": "Manual Trigger", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [-980, 0] + }, + { + "parameters": { + "jsCode": "const sample = {\n schema: 'botfucker.approved_actions.v1',\n safety_scope: 'provider_action_export_only',\n actions: [\n {\n audit_id: 'audit-outlook-draft-sample-0001',\n item_id: 'review-item-sample-0001',\n provider: 'microsoft_outlook',\n approved_action: 'approve_warning',\n provider_execution: 'not_performed',\n message_id: 'AAMkAGI2FAKE_MESSAGE_ID',\n warning_body: 'This is a fake, human-approved warning draft body for sandbox rehearsal only.'\n }\n ]\n};\nreturn [{ json: sample }];" + }, + "id": "sample-approved-action-bundle", + "name": "Sample Approved Action Bundle", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [-740, 0], + "notes": "Fake approved-action input. Replace with controlled test input only during sandbox rehearsal." + }, + { + "parameters": { + "jsCode": "const emergencyStopRaw = String($env.BOTFUCKER_EMERGENCY_STOP || 'true').toLowerCase();\nconst emergencyStop = !['0', 'false', 'off', 'no'].includes(emergencyStopRaw);\nif (emergencyStop) {\n return [{ json: { scaffold_status: 'blocked_by_emergency_stop', provider_execution: 'not_performed' } }];\n}\nconst bundle = $input.first().json || {};\nif (bundle.schema !== 'botfucker.approved_actions.v1') {\n throw new Error('Unexpected approved action schema');\n}\nif (bundle.safety_scope !== 'provider_action_export_only') {\n throw new Error('Unexpected safety scope');\n}\nconst processedRaw = $env.BOTFUCKER_PROCESSED_AUDIT_IDS || '';\nconst processedAuditIds = new Set(processedRaw.split(',').map((value) => value.trim()).filter(Boolean));\nconst actions = Array.isArray(bundle.actions) ? bundle.actions : [];\nreturn actions.filter((action) => {\n const provider = String(action.provider || '').toLowerCase();\n return action.audit_id\n && !processedAuditIds.has(action.audit_id)\n && action.provider_execution === 'not_performed'\n && action.approved_action === 'approve_warning'\n && ['outlook', 'microsoft_outlook'].includes(provider)\n && action.message_id\n && action.warning_body;\n}).map((action) => ({\n json: {\n ...action,\n provider: 'microsoft_outlook',\n draft_only: true,\n graph_operation: 'createReply',\n scaffold_status: 'validated_for_disabled_draft_step'\n }\n}));" + }, + "id": "validate-emergency-stop-and-dedupe", + "name": "Validate Emergency Stop And Dedupe", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [-500, 0], + "notes": "Fails closed by default via BOTFUCKER_EMERGENCY_STOP. Uses audit_id for duplicate skip before any provider-side draft step." + }, + { + "parameters": { + "jsCode": "return $input.all().map((item) => {\n const input = item.json || {};\n if (input.scaffold_status === 'blocked_by_emergency_stop') {\n return {\n json: {\n ...input,\n provider_execution: 'not_performed',\n draft_only: false,\n graph_operation: 'none',\n note: 'Emergency stop is on; scaffold is blocked before any draft readiness signal.'\n }\n };\n }\n return {\n json: {\n audit_id: input.audit_id,\n item_id: input.item_id,\n provider: 'microsoft_outlook',\n approved_action: 'approve_warning',\n provider_execution: 'not_performed',\n draft_only: true,\n graph_operation: 'createReply',\n scaffold_status: 'ready_for_manual_sandbox_rehearsal',\n note: 'The Graph draft node is disabled and unconnected in this export.'\n }\n };\n});" + }, + "id": "draft-only-safety-summary", + "name": "Draft Only Safety Summary", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [-260, 0], + "notes": "Main path logs draft-only readiness. It does not call Microsoft Graph." + }, + { + "parameters": { + "method": "POST", + "url": "=https://graph.microsoft.com/v1.0/me/messages/{{$json.message_id}}/createReply", + "sendBody": false, + "options": {} + }, + "id": "disabled-graph-create-reply-draft-placeholder", + "name": "Disabled Graph createReply Draft Placeholder", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.2, + "position": [-260, 280], + "disabled": true, + "notes": "Disabled and unconnected placeholder for a future operator-owned Microsoft Graph draft creation rehearsal." + }, + { + "parameters": { + "content": "Outlook warning draft scaffold:\n- inactive workflow export\n- manual trigger only\n- emergency stop defaults on\n- audit_id duplicate guard\n- main path logs readiness only\n- Graph createReply placeholder is disabled and unconnected", + "height": 280, + "width": 380 + }, + "id": "safety-boundary-notes", + "name": "Safety Boundary Notes", + "type": "n8n-nodes-base.stickyNote", + "typeVersion": 1, + "position": [-740, -360] + } + ], + "connections": { + "Manual Trigger": { + "main": [[{"node": "Sample Approved Action Bundle", "type": "main", "index": 0}]] + }, + "Sample Approved Action Bundle": { + "main": [[{"node": "Validate Emergency Stop And Dedupe", "type": "main", "index": 0}]] + }, + "Validate Emergency Stop And Dedupe": { + "main": [[{"node": "Draft Only Safety Summary", "type": "main", "index": 0}]] + } + }, + "settings": { + "executionOrder": "v1" + }, + "pinData": {}, + "staticData": null, + "tags": [ + {"name": "botfucker"}, + {"name": "outlook-warning-draft"}, + {"name": "scaffold"} + ], + "active": false, + "versionId": "botfucker-outlook-warning-draft-scaffold-v1" +} diff --git a/docs/n8n-outlook-warning-draft-scaffold.md b/docs/n8n-outlook-warning-draft-scaffold.md new file mode 100644 index 0000000..2b03730 --- /dev/null +++ b/docs/n8n-outlook-warning-draft-scaffold.md @@ -0,0 +1,72 @@ +# Phase 17 — Inactive Outlook Warning-Draft Workflow Scaffold + +This artifact adds an importable n8n scaffold for the first selected provider/action pair: + +- Provider: Microsoft Outlook / Microsoft Graph +- Reviewed action: `approve_warning` +- Graph primitive: `POST /me/messages/{id}/createReply` +- Permission note: Microsoft documents `Mail.ReadWrite` for draft creation. This is documentation only; no OAuth setup or secrets are stored here. +- Workflow export: `docs/n8n-outlook-warning-draft-scaffold.json` + +The scaffold is deliberately inactive, manual, and not connected to live provider execution. It is a rehearsal artifact for operator review, not a production workflow. + +## Safety boundary + +The workflow export must stay: + +- `active: false` +- manual-triggered only +- free of tokens, secrets, client secrets, passwords, private mailbox data, and exported n8n auth material +- limited to fake/sample input until an operator imports a controlled sandbox bundle +- draft-only; the only documented Microsoft Graph operation is `createReply` + +The included Graph node is a disabled, unconnected placeholder. The connected path validates a fake approved-action bundle, applies emergency-stop and `audit_id` duplicate checks, and emits a safety summary only. + +## Emergency stop + +`BOTFUCKER_EMERGENCY_STOP` defaults to on. Unless an operator explicitly sets it to an off value for a reviewed sandbox rehearsal, the validation node returns `blocked_by_emergency_stop` before any provider-side step can be considered. + +Required operator behavior: + +1. Confirm the workflow is inactive after import. +2. Confirm only the manual trigger exists as a trigger node. +3. Leave the disabled Graph placeholder disconnected unless Rex and Gus approve the exact sandbox rehearsal. +4. Turn the emergency stop back on immediately after rehearsal. + +## Dedupe + +The scaffold preserves the Phase 14/15 duplicate boundary: + +- `audit_id` is the dedupe key. +- `BOTFUCKER_PROCESSED_AUDIT_IDS` can be used in n8n sandbox rehearsal to skip known processed audit IDs. +- Real durable ledger state remains outside this scaffold until the operator wires the reviewed bridge path. +- Duplicate exports must not acquire a second draft attempt. + +## Rollback and manual deletion + +Rollback for a sandbox draft is intentionally manual: + +1. Stop the workflow and verify it is inactive. +2. Turn `BOTFUCKER_EMERGENCY_STOP` on. +3. In the sandbox Outlook mailbox, manually delete the created draft if one exists. +4. Mark the bridge ledger/audit record as rolled back in operator-maintained state. +5. Record the draft ID, `audit_id`, operator, timestamp, and reason in the rehearsal notes. + +No rollback step should mutate inbox messages, rules, contacts, settings, folders, read state, or non-draft data. + +## Import/rehearsal checklist + +Use this only in an n8n sandbox owned by the operator: + +1. Import `docs/n8n-outlook-warning-draft-scaffold.json`. +2. Verify the imported workflow is inactive. +3. Verify there are no stored credentials in the imported export. +4. Verify the Microsoft Graph placeholder is disabled and unconnected. +5. Execute the connected manual path with the emergency stop still on; expect `blocked_by_emergency_stop`. +6. Execute with a fake reviewed action and emergency stop off only after Rex/Gus review; expect draft-only readiness output before any provider node is connected. +7. If a future reviewed rehearsal connects the Graph placeholder, use only a sandbox mailbox and only the `createReply` draft primitive. +8. Return the workflow to inactive and emergency-stop-on state. + +## Non-goals + +This phase does not implement OAuth, does not activate n8n, does not add production credentials, does not connect a live Microsoft node, and does not perform live mailbox delivery. The next phase should be sandbox import/rehearsal or operator validation of this scaffold, not live delivery or broad OAuth work. diff --git a/docs/n8n-workflow.json b/docs/n8n-workflow.json index 3da1048..4bff85c 100644 --- a/docs/n8n-workflow.json +++ b/docs/n8n-workflow.json @@ -94,7 +94,7 @@ }, { "parameters": { - "content": "Provider boundary notes:\n- n8n fetches mail.\n- BotFucker imports bounded JSON into local SQLite.\n- No send/move/delete/archive/provider-list changes happen here.\n- Run the UI separately: python3 -m botfucker.local_ui --db botfucker_review.sqlite3", + "content": "Provider boundary notes:\n- n8n fetches mail.\n- BotFucker imports bounded JSON into local SQLite.\n- No mailbox mutation or provider-list changes happen here.\n- Run the UI separately: python3 -m botfucker.local_ui --db botfucker_review.sqlite3", "height": 260, "width": 360 }, diff --git a/scripts/validate_n8n_workflow_exports.py b/scripts/validate_n8n_workflow_exports.py index 7aab73c..072d1d4 100644 --- a/scripts/validate_n8n_workflow_exports.py +++ b/scripts/validate_n8n_workflow_exports.py @@ -19,6 +19,7 @@ WORKFLOW_PATHS = [ REPO_ROOT / "docs" / "n8n-workflow.json", REPO_ROOT / "docs" / "n8n-approved-action-bridge.json", + REPO_ROOT / "docs" / "n8n-outlook-warning-draft-scaffold.json", ] FORBIDDEN_STRINGS = [ @@ -43,6 +44,20 @@ "label", ] +FORBIDDEN_WORKFLOW_ACTION_STRINGS = [ + "/send", + "sendmail", + "reply send", + "delete", + "move", + "archive", + "mark read", + "mark unread", + "mark read/unread", + "rules", + "contacts/settings mutation", +] + def load_json(path: Path) -> dict[str, Any]: try: @@ -58,6 +73,17 @@ def node_names(workflow: dict[str, Any]) -> set[str]: return {str(node.get("name", "")) for node in workflow.get("nodes", [])} +def connected_node_names(workflow: dict[str, Any]) -> set[str]: + connected: set[str] = set() + for source_name, source_connections in workflow.get("connections", {}).items(): + if source_connections: + connected.add(str(source_name)) + for output_group in source_connections.get("main", []): + for edge in output_group: + connected.add(str(edge.get("node", ""))) + return connected + + def validate_workflow(path: Path) -> list[str]: workflow = load_json(path) serialized = json.dumps(workflow, sort_keys=True) @@ -75,6 +101,14 @@ def validate_workflow(path: Path) -> list[str]: if "manualTrigger" not in serialized: errors.append(f"{path.name}: manualTrigger node type missing") + trigger_nodes = [ + str(node.get("type", "")) + for node in workflow.get("nodes", []) + if "trigger" in str(node.get("type", "")).lower() + ] + if trigger_nodes != ["n8n-nodes-base.manualTrigger"]: + errors.append(f"{path.name}: workflow must use exactly one manual trigger") + for forbidden in FORBIDDEN_STRINGS: if forbidden in serialized_lower: errors.append(f"{path.name}: forbidden credential-like string found: {forbidden}") @@ -85,6 +119,10 @@ def validate_workflow(path: Path) -> list[str]: if term.lower() in node_type_lower: errors.append(f"{path.name}: forbidden provider mutation node type found: {term}") + for forbidden in FORBIDDEN_WORKFLOW_ACTION_STRINGS: + if forbidden in serialized_lower: + errors.append(f"{path.name}: forbidden mailbox action string found: {forbidden}") + if path.name == "n8n-approved-action-bridge.json": if "botfucker.approved_actions.v1" not in serialized: errors.append(f"{path.name}: approved-actions schema marker missing") @@ -93,6 +131,27 @@ def validate_workflow(path: Path) -> list[str]: if "audit_id" not in serialized: errors.append(f"{path.name}: audit_id dedupe marker missing") + if path.name == "n8n-outlook-warning-draft-scaffold.json": + nodes = {str(node.get("name", "")): node for node in workflow.get("nodes", [])} + placeholder = nodes.get("Disabled Graph createReply Draft Placeholder") + if placeholder is None: + errors.append(f"{path.name}: disabled Graph createReply placeholder missing") + else: + if placeholder.get("disabled") is not True: + errors.append(f"{path.name}: Graph createReply placeholder must be disabled") + if placeholder.get("type") != "n8n-nodes-base.httpRequest": + errors.append(f"{path.name}: Graph createReply placeholder must use httpRequest scaffold") + if "createReply" not in serialized: + errors.append(f"{path.name}: createReply draft marker missing") + if "draft_only" not in serialized: + errors.append(f"{path.name}: draft_only marker missing") + if "BOTFUCKER_EMERGENCY_STOP" not in serialized: + errors.append(f"{path.name}: emergency-stop marker missing") + if "audit_id" not in serialized: + errors.append(f"{path.name}: audit_id dedupe marker missing") + if "Disabled Graph createReply Draft Placeholder" in connected_node_names(workflow): + errors.append(f"{path.name}: Graph createReply placeholder must remain unconnected") + return errors diff --git a/tests/test_n8n_outlook_warning_draft_scaffold.py b/tests/test_n8n_outlook_warning_draft_scaffold.py new file mode 100644 index 0000000..9c7eab0 --- /dev/null +++ b/tests/test_n8n_outlook_warning_draft_scaffold.py @@ -0,0 +1,140 @@ +import json +import unittest +from pathlib import Path + + +WORKFLOW_PATH = Path("docs/n8n-outlook-warning-draft-scaffold.json") +GUIDE_PATH = Path("docs/n8n-outlook-warning-draft-scaffold.md") +VALIDATOR_PATH = Path("scripts/validate_n8n_workflow_exports.py") + + +class OutlookWarningDraftWorkflowScaffoldTests(unittest.TestCase): + def setUp(self): + self.workflow = json.loads(WORKFLOW_PATH.read_text(encoding="utf-8")) + self.serialized = json.dumps(self.workflow, sort_keys=True).lower() + self.nodes = {node["name"]: node for node in self.workflow["nodes"]} + + def test_workflow_is_inactive_and_manual_trigger_only(self): + self.assertEqual(self.workflow["id"], "botfucker-outlook-warning-draft-scaffold-v1") + self.assertEqual(self.workflow["name"], "BotFucker Outlook Warning Draft Scaffold") + self.assertFalse(self.workflow["active"]) + + trigger_nodes = [ + node for node in self.workflow["nodes"] if "trigger" in node.get("type", "").lower() + ] + self.assertEqual(len(trigger_nodes), 1) + self.assertEqual(trigger_nodes[0]["name"], "Manual Trigger") + self.assertEqual(trigger_nodes[0]["type"], "n8n-nodes-base.manualTrigger") + + def test_workflow_contains_no_secret_or_credential_material(self): + for forbidden in ( + "authorization", + "client_secret", + "access_token", + "refresh_token", + "api_key", + "password", + "credential", + "cookie", + ): + with self.subTest(forbidden=forbidden): + self.assertNotIn(forbidden, self.serialized) + + def test_workflow_forbids_send_and_mailbox_mutation_strings(self): + for forbidden in ( + "/send", + "sendmail", + "reply send", + "delete", + "move", + "archive", + "mark read", + "mark unread", + "mark read/unread", + "rules", + "contacts/settings mutation", + ): + with self.subTest(forbidden=forbidden): + self.assertNotIn(forbidden, self.serialized) + + def test_connected_path_is_validation_and_summary_only(self): + connections = self.workflow["connections"] + self.assertEqual( + connections["Manual Trigger"]["main"][0][0]["node"], + "Sample Approved Action Bundle", + ) + self.assertEqual( + connections["Sample Approved Action Bundle"]["main"][0][0]["node"], + "Validate Emergency Stop And Dedupe", + ) + self.assertEqual( + connections["Validate Emergency Stop And Dedupe"]["main"][0][0]["node"], + "Draft Only Safety Summary", + ) + self.assertNotIn("Disabled Graph createReply Draft Placeholder", connections) + for outputs in connections.values(): + for group in outputs.get("main", []): + for edge in group: + self.assertNotEqual(edge["node"], "Disabled Graph createReply Draft Placeholder") + + def test_draft_placeholder_is_disabled_unconnected_create_reply_only(self): + placeholder = self.nodes["Disabled Graph createReply Draft Placeholder"] + self.assertTrue(placeholder["disabled"]) + self.assertEqual(placeholder["type"], "n8n-nodes-base.httpRequest") + self.assertEqual(placeholder["parameters"]["method"], "POST") + self.assertIn("createReply", placeholder["parameters"]["url"]) + self.assertNotIn("/send", placeholder["parameters"]["url"].lower()) + + def test_safety_code_mentions_emergency_stop_dedupe_and_draft_only(self): + validation_code = self.nodes["Validate Emergency Stop And Dedupe"]["parameters"]["jsCode"] + summary_code = self.nodes["Draft Only Safety Summary"]["parameters"]["jsCode"] + self.assertIn("BOTFUCKER_EMERGENCY_STOP", validation_code) + self.assertIn("blocked_by_emergency_stop", validation_code) + self.assertIn("BOTFUCKER_PROCESSED_AUDIT_IDS", validation_code) + self.assertIn("audit_id", validation_code) + self.assertIn("approve_warning", validation_code) + self.assertIn("provider_execution", validation_code) + self.assertIn("not_performed", validation_code) + self.assertIn("blocked_by_emergency_stop", summary_code) + self.assertIn("graph_operation: 'none'", summary_code) + self.assertIn("draft_only: false", summary_code) + self.assertIn("draft_only: true", summary_code) + self.assertIn("createReply", summary_code) + + def test_emergency_stop_status_is_preserved_by_terminal_summary(self): + summary_code = self.nodes["Draft Only Safety Summary"]["parameters"]["jsCode"] + blocked_index = summary_code.index("blocked_by_emergency_stop") + ready_index = summary_code.index("ready_for_manual_sandbox_rehearsal") + + self.assertLess(blocked_index, ready_index) + self.assertIn("Emergency stop is on", summary_code) + self.assertIn("before any draft readiness signal", summary_code) + + def test_guide_documents_operator_safety_and_rollback(self): + guide = GUIDE_PATH.read_text(encoding="utf-8") + for phrase in ( + "active: false", + "manual-triggered only", + "Microsoft Outlook", + "POST /me/messages/{id}/createReply", + "Mail.ReadWrite", + "BOTFUCKER_EMERGENCY_STOP", + "audit_id", + "dedupe", + "Rollback and manual deletion", + "manually delete the created draft", + "sandbox import/rehearsal or operator validation", + ): + with self.subTest(phrase=phrase): + self.assertIn(phrase, guide) + + def test_validator_includes_new_workflow_export(self): + script = VALIDATOR_PATH.read_text(encoding="utf-8") + self.assertIn("n8n-outlook-warning-draft-scaffold.json", script) + self.assertIn("createReply", script) + self.assertIn("BOTFUCKER_EMERGENCY_STOP", script) + self.assertIn("Graph createReply placeholder must remain unconnected", script) + + +if __name__ == "__main__": + unittest.main()