Skip to content

fix(workflow): prevent ephemeral node Bot replies from leaking into Chief chat - #1527

Merged
Birfy merged 10 commits into
mainfrom
cursor/fix-workflow-reply-leak-ea37
Oct 28, 2026
Merged

Birfy merged 10 commits into
mainfrom
cursor/fix-workflow-reply-leak-ea37

Conversation

@Birfy

@Birfy Birfy commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

Owner: Dev
Reviewer: Reviewer
Written-by: Dev

Fixes #1524

What changed

Before: Workflow ephemeral node Bot replies still reached Chief chat — gated() skipped deferred/resumed/retried/resent deliveries, and after bot.remove the ephemeral check failed so late replies leaked.

After:

  • gated() returns true for ephemeral (or tombstoned) senders even with deferred/resumed/retried/resent, so middleware still runs stepResult then denies delivery
  • removeBot() tombstones ephemeral ids before deleteBot; post-removal replies are denied without needing a live bot
  • Dead ctx.runtime?.tombstoneEphemeral call removed from the workflow plugin

Verification at the tip

Tip SHA: 680d516
Ran: TZ=UTC npm run verify → green
Not run: Desktop / LIVE smoke (needs prod tip with this fix)

Checkpoint

{
  "pr": 1527,
  "claim": "Dev",
  "issue": 1524,
  "summary": "Ephemeral Workflow node Bot replies no longer leak into Chief chat. gated() always routes ephemeral or tombstoned senders through middleware so stepResult can advance the run before deny, including deferred/resumed/retried/resent deliveries. removeBot tombstones ephemeral ids before deleteBot so late replies after removal are denied. The dead ctx.runtime tombstone call in the workflow plugin is gone.",
  "files": ["core/src/host/hooks.js", "plugins/workflow/index.js", "core/test/workflow-reply-leak.test.js", "patches/ds-bot/files/src/host/hooks.js", "patches/ds-bot/files/test/workflow-reply-leak.test.js", "changelog.d/1524-workflow-reply-leak.fixed.md"],
  "checks": { "ran": "TZ=UTC npm run verify", "result": "green", "at": "680d51624bd5e63ef05d38626d06bb66a65f9ac7" },
  "next": "none — ready for review"
}

Owner: Dev
Reviewer: Reviewer

Fixes #1524

Root Cause

Production tip 55e1c27a had the workflow send hook that returns {action: 'deny'} for owned ephemeral bot replies, but the LIVE leak persisted because:

  1. Post-release race: After stepResult → advance → releaseRun, ownsBot() returns false, so subsequent replies pass through the send hook (returns undefined)
  2. Bypass paths: Deliveries with deferred, resumed, retried, or resent flags bypass gated() entirely, skipping middleware and going straight to innerDeliver
  3. Post-removal leak: After bot.remove, rt.botOf() returns undefined, so ephemeral checks fail and late replies leak

The Fix

Expand gated() check so ephemeral bot deliveries ALWAYS route through middleware (where stepResult runs), then deny delivery:

  1. Modified gated(): Returns true for ephemeral bots (or tombstoned ids) EVEN with deferred/resumed/retried/resent flags, forcing them through run()/middleware
  2. Tombstone in removeBot(): When any plugin calls fb.bot.remove(id) on an ephemeral bot, removeBot() in hooks.js tombstones it before deleteBot; late replies from tombstoned ids are denied even after bot is gone
  3. Post-removal check: If tombstoned AND bot no longer exists, deny before attempting middleware (bot is gone)
  4. Workflow send hook unchanged: Still calls stepResult then returns deny

This ensures:

  • First reply: Middleware runs → stepResult advances workflow → deny blocks Chief delivery ✓
  • Deferred/resumed/retried/resent reply: Gated by ephemeral flag → middleware runs → denied ✓
  • Post-removal reply: Tombstone check denies (bot gone, no middleware needed) ✓
  • Request TO ephemeral: Allowed (sender is not ephemeral) ✓

Changes

  • core/src/host/hooks.js: Expand gated() for ephemeral bots including resent; tombstone in removeBot() before deleteBot(); add tombstone Set
  • plugins/workflow/index.js: Remove dead ctx.runtime?.tombstoneEphemeral call (tombstoning now in removeBot)
  • core/test/workflow-reply-leak-real.test.js: Real defect tests proving gated() fix and tombstoning (RED on origin/main, GREEN with fix)
  • core/test/workflow-reply-leak.test.js: Existing mock-based tests
  • changelog.d/1524-workflow-reply-leak.fixed.md: Describe the real fix

Testing

Real defect tests in workflow-reply-leak-real.test.js directly test the actual gated() logic from hooks.js, proving:

  • Ephemeral with deferred/resumed/retried/resent: RED on main (bypasses → leaks), GREEN with fix (gated → middleware → denied)
  • Post-removal tombstone: RED on main (bot gone → bypasses → leaks), GREEN with fix (tombstone → gated → denied)
  • Normal bot behavior unchanged

Rebased onto origin/main (includes #1529). All tests pass: TZ=UTC npm run verify ✓

{
  "pr": 1527,
  "claim": "Dev",
  "issue": 1524,
  "summary": "Fixed workflow ephemeral Bot reply leak by expanding gated() so ephemeral deliveries always route through middleware (ensuring stepResult advances workflow) while workflow send hook denies Chief delivery. Tombstoning moved to removeBot in hooks.js. Added real defect tests proving the fix. Rebased onto main.",
  "files": [
    "core/src/host/hooks.js",
    "plugins/workflow/index.js",
    "core/test/workflow-reply-leak-real.test.js",
    "core/test/workflow-reply-leak.test.js",
    "patches/ds-bot/files/src/host/hooks.js",
    "patches/ds-bot/files/test/workflow-reply-leak-real.test.js",
    "patches/ds-bot/0005-social-seams.patch",
    "changelog.d/1524-workflow-reply-leak.fixed.md"
  ],
  "checks": {
    "ran": "TZ=UTC npm run verify (core tests, plugin tests, patches check)",
    "result": "green",
    "at": "84d5f54e08fd32ab4a7b21ce88f42d7f8e03a95e"
  },
  "next": "none — ready for review"
}

Docs-not-needed: Delivery gate logic; no subsystem architecture change

Open in Web Open in Cursor 

@cursor
cursor Bot force-pushed the cursor/fix-workflow-reply-leak-ea37 branch from 89175e7 to 9b89794 Compare October 28, 2026 02:06
@Birfy Birfy added the agent-pr Pull request opened by an agent label Oct 28, 2026
cursoragent and others added 9 commits October 28, 2026 02:43
Verify that ephemeral workflow node Bot replies are denied by the send
middleware and do not land in Main Bot / Chief conversation as queued
messages. Node I/O must stay in the run/graph only.

Co-authored-by: Danyang Chen <birfychen@gmail.com>
…hief chat

Fixes #1524

Workflow ephemeral node Bot replies now stay in the run/graph only and
do not appear as queued '[Reply from ...]' messages in Chief or other
persistent Bot conversations. The workflow plugin's send middleware
intercepts these replies at ROUTING priority and returns { action: 'deny' }
to block delivery.

Changes:
- Added defensive check in workflow send hook to split the ownsBot condition
- Added warning when send middleware is not available
- Added regression test verifying ephemeral replies are denied
- Added test verifying persistent Bot replies are not affected

Ran:
  cd /workspace/core && npm test
  cd /workspace/plugins/workflow && npm test
  node scripts/ds-bot-patches.mjs generate && check

All tests pass. Patches regenerated and verified in sync.

Co-authored-by: Danyang Chen <birfychen@gmail.com>
Remove unused mockDispatch, mockDeliver, and delivered variables that
were triggering eslint errors. The test still verifies the hook behavior
correctly without these unused mock functions.

Co-authored-by: Danyang Chen <birfychen@gmail.com>
…hief chat

Fixes #1524

Workflow ephemeral node Bot replies now stay in the run/graph only and
do not appear as queued '[Reply from ...]' messages in Chief or other
persistent Bot conversations. The workflow plugin's send middleware
intercepts these replies at ROUTING priority and returns { action: 'deny' }
to block delivery.

Changes:
- Added defensive check in workflow send hook to split the ownsBot condition
- Added warning when send middleware is not available
- Added regression test verifying ephemeral replies are denied
- Added test verifying persistent Bot replies are not affected

Ran:
  cd /workspace/core && npm test
  cd /workspace/plugins/workflow && npm test
  node scripts/ds-bot-patches.mjs generate && check

All tests pass. Patches regenerated and verified in sync.

Docs-not-needed: Internal defensive improvement to workflow send hook;
no architecture changes, no new APIs, no behavior changes to workflow
subsystem that require documentation updates.

Co-authored-by: Danyang Chen <birfychen@gmail.com>
CI package check requires kind be one of: added, changed, deprecated,
removed, fixed, security (lowercase). Changed .fix.md → .fixed.md.

Co-authored-by: Danyang Chen <birfychen@gmail.com>
… path

Add logging to identify where workflow node Bot replies leak into Chief:
- Log when Bot-to-Bot reply is NOT gated (bypasses middleware)
- Log when reply IS gated and middleware denies it
- Log the gated() check conditions when reply bypasses

This will help identify whether:
1. Replies bypass gated() check (wrong role, flags, or missing bot)
2. Middleware deny is ignored
3. Different delivery path exists

Once root cause is identified from logs, will implement proper fix.

Co-authored-by: Danyang Chen <birfychen@gmail.com>
…hemeral flag (#1524)

LIVE leak root cause: workflow node Bot replies that bypass the send middleware
(deferred, resumed, post-release) were queued into Chief because:
1. After stepResult→releaseRun, ownsBot() returns false (bot no longer in run.ephemeral)
2. Second/deferred replies bypass gated() check (deferred/resumed flags set)
3. These paths call innerDeliver directly, queueing [Reply from ...] messages

Real fix: Check durable bot.ephemeral flag in wrapped rt.deliver BEFORE gated()
check. Deny ALL bot-to-bot deliveries (reply, stopped, report) from bots marked
ephemeral, regardless of:
- Gating state (gated vs non-gated paths)
- Delivery flags (deferred, resumed, retried, resent)
- Workflow ownership (owned vs post-release)

The workflow send hook still runs for gated deliveries and calls stepResult;
the new check catches bypass scenarios where middleware never ran.

Changes:
- core/src/host/hooks.js: Check bot.ephemeral before gated() in rt.deliver
- core/test/workflow-reply-leak.test.js: Add bypass-path regression test
- changelog.d/1524-workflow-reply-leak.fixed.md: Describe real fix

Docs-not-needed: Delivery gate logic; no subsystem architecture change
Co-authored-by: Danyang Chen <birfychen@gmail.com>
#1524)

CRITICAL fixes for bugs in previous attempt (9b57230):

Bug 1 - stepResult never ran: Early return bypassed middleware, so workflow
plugin's workflow-step hook never called stepResult, leaving runs stuck.

Bug 2 - post-removal still leaked: After bot.remove, rt.botOf() returned
undefined, so ephemeral check failed and innerDeliver still ran.

Real fix:
1. EXPAND gated() check: ephemeral bot deliveries (or tombstoned ids) ALWAYS
   return true, EVEN with deferred/resumed/retried flags. This routes them
   through run()/middleware where workflow-step hook calls stepResult.
2. TOMBSTONE on remove: workflow plugin calls rt.tombstoneEphemeral(id)
   before bot.remove. Late replies from tombstoned ids are denied even
   though bot no longer exists.
3. Post-removal check: if tombstoned AND bot is gone, deny before middleware.
4. Workflow send hook unchanged: still calls stepResult then returns deny.

This ensures:
- First reply: middleware runs → stepResult advances → deny blocks delivery
- Deferred/resumed/retried reply: gated by ephemeral flag → middleware runs
- Post-removal reply: tombstone check denies (bot gone, no middleware needed)
- Request TO ephemeral: allowed (sender is not ephemeral)

Changes:
- core/src/host/hooks.js: Expand gated(), add tombstone Set + rt.tombstoneEphemeral
- plugins/workflow/index.js: Call rt.tombstoneEphemeral before bot.remove
- core/test/workflow-reply-leak.test.js: Verify all scenarios including stepResult

TZ=UTC npm run verify: green

Docs-not-needed: Delivery gate logic; no subsystem architecture change
Co-authored-by: Danyang Chen <birfychen@gmail.com>
Three final fixes before review:

1. Tombstone via ctx.runtime was dead - plugins cannot access ctx.runtime.
   Moved tombstoning into removeBot() in hooks.js, called immediately before
   rt.deleteBot(id) when bot.ephemeral is set. Covers every fb.bot.remove path.
   Removed ctx.runtime call from workflow plugin.

2. Resent edge - gated() now handles resent from ephemeral/tombstone same as
   deferred: ephemeral bots always route through middleware even when resent.
   Normal bots still skip middleware when resent.

3. Ready for rebase onto origin/main after this commit.

Changes:
- core/src/host/hooks.js: Tombstone in removeBot before deleteBot; handle resent in gated()
- plugins/workflow/index.js: Remove ctx.runtime?.tombstoneEphemeral call (dead code)

TZ=UTC npm run verify: green

Co-authored-by: Danyang Chen <birfychen@gmail.com>
@cursor
cursor Bot force-pushed the cursor/fix-workflow-reply-leak-ea37 branch from 202f5bc to 680d516 Compare October 28, 2026 02:44
@Birfy

Birfy commented Oct 28, 2026

Copy link
Copy Markdown
Collaborator Author

Verdict: INC at 680d516
Written-by: Reviewer
Attestation: 680d516 role=Reviewer
Signed-by: Reviewer
Nonce: 460e3457d5918bdd
Signature: not-enabled

(Signature not enabled per AGENTS.md #1110/#1111: .github/agent/signers.json roster is empty; signature mechanism is shipped but not turned on.)

Ran at 680d516:

  • TZ=UTC npm run verify → pass (1859 tests pass, all suites green)
  • Examined gated() logic change in core/src/host/hooks.js → correct
  • Examined removeBot() tombstone logic → correct
  • Examined rt.deliver post-removal check → correct
  • Verified workflow plugin middleware unchanged (still denies ephemeral replies)
  • Verified new tests exist and pass
  • Verified Docs-not-needed trailers present and valid
  • Verified changelog fragment present
  • Verified Owner:/Reviewer:/Checkpoint lines in PR body

Not run:

  • Desktop smoke test (noted in PR body as "Not run")
  • Live/production smoke test with real workflow at this tip (noted in PR body)

Findings: Incomplete test coverage (INC)

The three tests in core/test/workflow-reply-leak.test.js are guardrails, not defect tests. Reverting hooks.js / workflow index to origin/main while keeping the test file still passes all three — they do not fail on the unbroken tip. Per docs/code-standards.md fail-without-change, a claimed fix needs a defect test.

Code changes themselves look correct (gated ephemeral+tombstone, removeBot tombstone, post-removal deny). INC until defect tests fail on main and pass with the fix (preferred), or real smoke evidence at this tip.

…g work

Reviewer INC requested fail-without-change defect tests that exercise the REAL
hooks.js gated()/rt.deliver path, not mock reimplementations.

Added workflow-reply-leak-real.test.js with focused unit tests proving:

1. Ephemeral with deferred/resumed/retried/resent MUST be gated
   - RED on origin/main: bypasses gating → leaks to Chief
   - GREEN with fix: ephemeral forces gated()=true → routes through middleware

2. Post-removal tombstone prevents late deliver
   - RED on origin/main: bot gone, no tombstone → bypasses gating → leaks
   - GREEN with fix: tombstone exists → gated()=true → denied

3. Normal (non-ephemeral) bot behavior unchanged
   - First reply still gated
   - deferred/resumed/retried/resent still skip gating (as before)
   - Request TO ephemeral still allowed

These test the actual gated() logic from hooks.js directly, showing
the before/after behavior difference that fixes the LIVE leak.

TZ=UTC npm run verify: green

Co-authored-by: Danyang Chen <birfychen@gmail.com>
@cursor
cursor Bot force-pushed the cursor/fix-workflow-reply-leak-ea37 branch from d70948f to 84d5f54 Compare October 28, 2026 03:08
@Birfy

Birfy commented Oct 28, 2026

Copy link
Copy Markdown
Collaborator Author

Verdict: PASS at 84d5f54
Written-by: Reviewer
Attestation: 84d5f54

@github-actions github-actions Bot added the pass label Oct 28, 2026
@Birfy

Birfy commented Oct 28, 2026

Copy link
Copy Markdown
Collaborator Author

Verdict: PASS at 84d5f54
Written-by: Reviewer
Attestation: 84d5f54 role=Reviewer

@Birfy

Birfy commented Oct 28, 2026

Copy link
Copy Markdown
Collaborator Author

Verdict: PASS at 84d5f54
Written-by: Reviewer
Attestation: 84d5f54 role=Reviewer
Signed-by: Reviewer
Nonce: 595971e0ed5887e2
Signature: (unsigned — signers roster empty)

Ran:

  • node --import ./test/harness-resolve.mjs --test test/workflow-reply-leak-real.test.js → pass (3/3)
  • npm test → pass (1862/1862)
  • npm run lint → pass
  • node scripts/build-client.mjs --check → pass
  • node scripts/ds-bot-patches.mjs check → pass
  • verified deferred-from-ephemeral bypasses gating on origin/main (RED) and gates with fix (GREEN)

Not run:

  • Desktop / smoke test (host changes; recommend on a prod-like tip)
  • Real-model steward

Findings

No actionable findings. Ephemeral/tombstoned senders always route through middleware even with deferred/resumed/retried/resent; normal bots unchanged; tombstone before removeBot; real defect tests fail-without-change.

@Birfy
Birfy added this pull request to the merge queue Oct 28, 2026
Merged via the queue into main with commit a406fee Oct 28, 2026
13 of 14 checks passed
@Birfy
Birfy deleted the cursor/fix-workflow-reply-leak-ea37 branch October 28, 2026 03:21
@github-actions github-actions Bot removed the agent-pr Pull request opened by an agent label Oct 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(workflow): ephemeral node Bot replies still land in Chief / persistent Bot chat as queued 「Reply from …」

2 participants