Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions HANDOFF.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@

- GitHub: `https://github.com/Jdelg718/BotFucker`
- Default branch: `main`
- Latest merged milestone: Phase 13 Reviewed Action Bridge Promotion Plan (`docs: plan reviewed action bridge promotion`, main at 4619bf5)
- Current working branch: `phase-14-durable-bridge-ledger`
- Current PR target: Phase 14 Durable Bridge Ledger Scaffold — code/docs/tests only, no OAuth/live provider mutation
- 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, reviewed bridge-promotion gate, and durable bridge ledger dedupe scaffold
- Latest merged milestone: Phase 14 Durable Bridge Ledger Scaffold (`feat: add durable bridge ledger scaffold`, main at 8904d67)
- Current working branch: `phase-15-emergency-stop-bridge-rehearsal`
- Current PR target: Phase 15 Emergency-Stop Bridge Rehearsal — code/docs/tests only, dry-run-only, no OAuth/live provider mutation
- 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
- Current promo artifact: `promo/botfucker-animated-explainer/renders/botfucker-animated-explainer_narrated-final.mp4`

## What BotFucker Is
Expand All @@ -26,7 +27,7 @@ Important files:

```text
DESIGN.md # v2 architecture and principles
ROADMAP.md # phased product roadmap, current through Phase 12 n8n import validation
ROADMAP.md # phased product roadmap, current through Phase 15 emergency-stop bridge rehearsal
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
Expand All @@ -36,6 +37,7 @@ docs/n8n-approved-action-bridge.md # approved-action bridge operator guide
docs/provider-auth-plan.md # provider auth/action boundary plan
docs/reviewed-action-bridge-promotion-plan.md # Phase 13 reviewed live-bridge gate; no OAuth/live mutation
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
README.md # user-facing setup and project overview
outreach_filter.py # compatibility CLI wrapper
botfucker/models.py # normalized email/classification/review models
Expand All @@ -44,6 +46,7 @@ botfucker/history.py # SQLite sender history + strike state
botfucker/review_queue.py # review item/audit models and sample data helpers
botfucker/review_store.py # durable SQLite review queue and audit store
botfucker/bridge_ledger.py # durable bridge processed-audit ledger scaffold
botfucker/bridge_rehearsal.py # dry-run bridge rehearsal proving emergency stop and duplicate skip
botfucker/review_cli.py # durable local review CLI
botfucker/webhook_contract.py # n8n/webhook payload sanitizer/import adapter
botfucker/local_ui.py # local browser review UI server
Expand All @@ -67,6 +70,7 @@ These are non-negotiable:
- Local UI and review CLI actions affect SQLite review state only.
- 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`.

## Current Test Commands

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ The current core is split into reusable modules under `botfucker/`:
- `history.py` tracks sender history, warning counts, and strike levels in SQLite.
- `review_store.py` persists local review queue items and audit events in SQLite.
- `bridge_ledger.py` provides a durable bridge ledger scaffold keyed by approved-action `audit_id` values.
- `bridge_rehearsal.py` proves emergency-stop and dry-run bridge behavior without provider execution.
- `webhook_contract.py` normalizes bounded n8n/webhook email JSON into local review items.
- `review_cli.py` provides a provider-safe local review workflow around seeded/imported items.
- `responses.py` contains human-reviewable warning templates.
Expand All @@ -39,6 +40,7 @@ See [DESIGN.md](DESIGN.md) for the proposed architecture and roadmap.
- Exports approved local audit events as an idempotent JSON bundle for an n8n/provider bridge.
- Provides an inactive n8n approved-action bridge starter that validates/dedupes actions in dry-run mode.
- 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.
- Keeps provider credentials and live mailbox side effects outside the local UI and review queue.

## Safety First
Expand Down
50 changes: 39 additions & 11 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,29 @@ Acceptance criteria:
- Ledger stores IDs/status only, not message body/header/credential material.
- No OAuth, no provider credentials, no live provider mutation nodes, and no checked-in n8n activation changes are added.

### Phase 15 — Emergency-Stop Bridge Rehearsal ✅

Status: implemented on `phase-15-emergency-stop-bridge-rehearsal`.

Goal: prove the emergency-stop and dry-run bridge path against the durable ledger before any live provider mutation exists.

Delivered:

- `botfucker.bridge_rehearsal.rehearse_approved_actions()` dry-run-only bridge rehearsal
- emergency-stop default that exits before claiming the durable ledger
- dry-run path that claims the ledger and marks `dry_run_logged` without provider execution
- duplicate replay handling that returns `duplicate_skipped`
- docs in `docs/bridge-rehearsal.md`
- tests proving emergency stop, dry-run logging, duplicate skip, live-mode rejection, and unsafe-action rejection

Acceptance criteria:

- Emergency stop creates no ledger row and no provider execution.
- Dry-run remains mandatory; `dry_run=False` fails closed.
- Duplicate approved-action exports are skipped by durable `audit_id`.
- Provider execution remains `not_performed`.
- No OAuth, provider credentials, provider API calls, or live mutation nodes are added.

## Local Kodex/Codex Demo Plan

Kent is pulling this locally onto Kodex/Codex next. The demo should show what exists now, not pretend Phase 8 is already done. Revolutionary concept, apparently.
Expand Down Expand Up @@ -387,27 +410,32 @@ Use fake or sanitized JSON only. Real mailbox payloads stay out of the repo.

## Near-Term Recommendation

After Phase 14 is reviewed and merged, the next PR should be **Phase 15: emergency-stop proof or sandbox-only bridge rehearsal**, not broad OAuth implementation.
After Phase 15 is reviewed and merged, the next PR should be **Phase 16: Microsoft Outlook warning-draft sandbox contract**, not broad OAuth implementation.

Kent selected the first provider/action target:

- Provider: Microsoft Outlook
- First action: create/save a warning draft only
- Scope: sandbox/manual reviewed bridge contract first; no send-reply mutation yet

Recommended scope:

- keep one provider/action pair only (`approve_warning`)
- use the durable bridge ledger before any provider mutation
- document the exact sandbox mailbox/provider target Kent wants to use
- keep credentials in n8n only
- keep dry-run as the default path
- prove emergency stop exits before provider mutation
- require provider-specific sandbox/manual tests
- 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

OAuth can still wait. Phase 14 gives the bridge a seatbelt: durable dedupe before action. Next is proving the brakes, not flooring it into a live inbox.
OAuth can still wait. Phase 15 proves the brakes in code. Next is picking the sandbox road — not handing the robot live mailbox keys because apparently we enjoy learning by fire.

### Restart checklist after Phase 14
### Restart checklist after Phase 15

1. Re-check Phase 14 branch CI and mergeability.
2. Squash-merge Phase 14 into `main` if still green.
3. Pull updated `main` and create a Phase 15 branch.
4. Keep exactly one sandbox provider/action pair, likely `approve_warning` only if Kent explicitly wants reply-send tested.
5. Prove emergency-stop and dry-run behavior against the durable ledger before wiring any provider mutation.
1. Re-check Phase 15 PR CI and mergeability.
2. Squash-merge Phase 15 into `main` if still green.
3. Use Microsoft Outlook as the selected sandbox provider target.
4. Limit the first provider/action pair to warning draft creation/save only; do not send replies.
5. Keep provider credentials inside n8n/operator infrastructure only; do not put secrets in BotFucker.

## Team Utilization

Expand Down
21 changes: 19 additions & 2 deletions botfucker/bridge_ledger.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
PROCESSED = "processed"
FAILED = "failed"
ROLLED_BACK = "rolled_back"
VALID_STATUSES = {PENDING, PROCESSED, FAILED, ROLLED_BACK}
DRY_RUN_LOGGED = "dry_run_logged"
VALID_STATUSES = {PENDING, PROCESSED, FAILED, ROLLED_BACK, DRY_RUN_LOGGED}


class BridgeLedgerError(ValueError):
Expand Down Expand Up @@ -211,6 +212,21 @@ def mark_rolled_back(
processed_by_workflow=processed_by_workflow,
)

def mark_dry_run_logged(
self,
audit_id: str,
*,
processed_by_workflow: str | None = None,
) -> BridgeLedgerRecord:
"""Record a dry-run rehearsal without provider execution."""

return self._update_status(
audit_id,
DRY_RUN_LOGGED,
provider_result_id="",
processed_by_workflow=processed_by_workflow,
)

def _update_status(
self,
audit_id: str,
Expand Down Expand Up @@ -302,9 +318,10 @@ def _validate_status_transition(current: str, target: str) -> None:
if current == target:
return
allowed = {
PENDING: {PROCESSED, FAILED, ROLLED_BACK},
PENDING: {PROCESSED, FAILED, ROLLED_BACK, DRY_RUN_LOGGED},
FAILED: {ROLLED_BACK},
PROCESSED: {ROLLED_BACK},
DRY_RUN_LOGGED: {ROLLED_BACK},
ROLLED_BACK: set(),
}
if target not in allowed[current]:
Expand Down
122 changes: 122 additions & 0 deletions botfucker/bridge_rehearsal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
"""Dry-run bridge rehearsal with emergency-stop proof.

This module is deliberately provider-free. It does not call Gmail, Microsoft,
IMAP, SMTP, n8n, HTTP APIs, or any provider mutation surface. It proves the
operator-side gates that must happen before a future reviewed live bridge:

1. emergency stop is checked first;
2. dry-run remains mandatory;
3. the durable bridge ledger is claimed before any would-execute result;
4. duplicate audit IDs are skipped rather than replayed.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Any

from botfucker.bridge_ledger import (
BridgeLedgerError,
DurableBridgeLedger,
REQUIRED_PROVIDER_EXECUTION,
)

BRIDGE_REHEARSAL_WORKFLOW = "botfucker-bridge-rehearsal-v1"


@dataclass(frozen=True)
class BridgeRehearsalOutcome:
"""One action's dry-run bridge rehearsal result."""

audit_id: str
approved_action: str
provider: str
status: str
dry_run: bool
ledger_acquired: bool
provider_execution: str = REQUIRED_PROVIDER_EXECUTION
would_execute: bool = False
reason: str = ""


def rehearse_approved_actions(
bundle: dict[str, Any],
ledger: DurableBridgeLedger,
*,
emergency_stop: bool = True,
dry_run: bool = True,
processed_by_workflow: str = BRIDGE_REHEARSAL_WORKFLOW,
) -> list[BridgeRehearsalOutcome]:
"""Rehearse approved actions through the safety gates without side effects.

Phase 15 intentionally supports dry-run only. Passing ``dry_run=False`` is an
error, not a secret live mode. When ``emergency_stop`` is enabled, actions
are blocked before the ledger is claimed so operators can prove the stop
switch exits before any provider mutation slot is acquired.
"""

if not dry_run:
raise BridgeLedgerError("Bridge rehearsal is dry-run only; live provider execution is out of scope")

actions = bundle.get("actions")
if not isinstance(actions, list):
raise BridgeLedgerError("Approved-actions bundle must include an actions list")

outcomes: list[BridgeRehearsalOutcome] = []
for action in actions:
if not isinstance(action, dict):
raise BridgeLedgerError("Approved action entries must be objects")

if emergency_stop:
outcomes.append(
BridgeRehearsalOutcome(
audit_id=str(action.get("audit_id") or ""),
approved_action=str(action.get("approved_action") or ""),
provider=str(action.get("provider") or ""),
status="blocked_by_emergency_stop",
dry_run=True,
ledger_acquired=False,
would_execute=False,
reason="Emergency stop enabled before ledger claim or provider mutation",
)
)
continue

claim = ledger.claim_action(
bundle,
action,
processed_by_workflow=processed_by_workflow,
dry_run=True,
)
if claim.acquired:
record = ledger.mark_dry_run_logged(
claim.record.audit_id,
processed_by_workflow=processed_by_workflow,
)
outcomes.append(
BridgeRehearsalOutcome(
audit_id=record.audit_id,
approved_action=record.approved_action,
provider=record.provider,
status=record.status,
dry_run=True,
ledger_acquired=True,
would_execute=True,
reason="Dry-run rehearsal logged; provider execution not performed",
)
)
else:
outcomes.append(
BridgeRehearsalOutcome(
audit_id=claim.record.audit_id,
approved_action=claim.record.approved_action,
provider=claim.record.provider,
status="duplicate_skipped",
dry_run=True,
ledger_acquired=False,
would_execute=False,
reason="Durable ledger already contains this audit_id",
)
)

return outcomes
Loading
Loading