Skip to content

fix+test: freeze pending nego.open + M3.3 adversarial e2e (#37)#68

Merged
rfxlamia merged 4 commits into
mainfrom
test/m33-adversarial-e2e
Jul 23, 2026
Merged

fix+test: freeze pending nego.open + M3.3 adversarial e2e (#37)#68
rfxlamia merged 4 commits into
mainfrom
test/m33-adversarial-e2e

Conversation

@rfxlamia

@rfxlamia rfxlamia commented Jul 23, 2026

Copy link
Copy Markdown
Owner

Summary

  • Freeze first-open nego.open terms while a session is pending so same-initiator redelivery cannot escalate goal/mandate behind a stale human gate (RT-PEND-1).
  • Add M3.3 adversarial e2e suite covering the six ROADMAP defenses plus nego.open redelivery across pending/live/signed/closed/open_rejected via handleInbox / relay POST.
  • Point the conformance checklist M3.3 row at the new suite (open_expired credited to protocol unit tests).

Closes #37

Type of Change

  • Bug fix (RT-PEND-1 pending term escalation)
  • Tests / hardening (M3.3 adversarial e2e)

Test plan

  • pnpm --filter @agentpair/protocol exec vitest run src/session/state-machine.test.ts src/session/nego-open-pending-mutation.probe.test.ts (132 passed)
  • pnpm --filter agentpair exec vitest run src/e2e/adversarial.test.ts src/e2e/happy-path.test.ts (11 passed)
  • CI green on this PR

Notes

  • Stacked on one branch for a single PR: protocol freeze + mcp-server e2e.
  • Deferred (non-blocking): e2e for open_expired, dual-server.syncInboxeshandleInbox, host inbox-hygiene routing_mismatch case; SPEC N1 wording → M3.5.

Summary by CodeRabbit

  • Bug Fixes

    • Prevented redelivered session-open messages from overwriting existing negotiation terms or changing session status.
    • Preserved pending session progress when duplicate or escalated messages are received.
    • Added protections and validation coverage for tampered routing, replayed messages, oversized payloads, unauthorized senders, and forbidden self-approval.
  • Tests

    • Added end-to-end adversarial coverage for session defenses and redelivery across multiple session states.
    • Expanded conformance documentation to include adversarial scenarios and redelivery coverage.

rfxlamia added 2 commits July 23, 2026 14:27
Same-initiator redelivery must not escalate goal/mandate behind a stale
pending gate (RT-PEND-1); remove preserveProgress overwrite path.
Cover the six ROADMAP defenses plus nego.open redelivery across
pending/live/signed/closed/open_rejected, and point the checklist at it.
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 33028a33-d819-4ec6-b99e-905f898d07b7

📥 Commits

Reviewing files that changed from the base of the PR and between cea6e96 and bfa5e65.

📒 Files selected for processing (5)
  • docs/conformance-checklist.md
  • packages/mcp-server/src/e2e/adversarial.test.ts
  • packages/protocol/src/session/nego-open-pending-mutation.probe.test.ts
  • packages/protocol/src/session/state-machine.test.ts
  • packages/protocol/src/session/state-machine.ts

📝 Walkthrough

Walkthrough

The session state machine now treats pending nego.open redelivery as a no-op. Protocol regression tests verify original terms remain intact, while a new adversarial E2E suite covers relay defenses, self-approval, replay handling, and redelivery across session states.

Changes

Adversarial negotiation handling

Layer / File(s) Summary
Pending redelivery state preservation
packages/protocol/src/session/state-machine.ts, packages/protocol/src/session/state-machine.test.ts, packages/protocol/src/session/nego-open-pending-mutation.probe.test.ts
Pending nego.open redelivery returns the existing pending state without changing session terms; new-session negotiation fields use empty defaults, with regression coverage for store and queue preservation.
Adversarial relay and lifecycle coverage
packages/mcp-server/src/e2e/adversarial.test.ts, docs/conformance-checklist.md
Adds E2E checks for routing mismatch, stale sequences, oversized envelopes, unbonded senders, forbidden self-approval, and redelivery across five session states; the conformance checklist records this coverage.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AdversarialTest
  participant Relay
  participant handleInbox
  participant SessionStore
  AdversarialTest->>Relay: POST adversarial envelope
  Relay->>handleInbox: dispatch accepted envelope
  handleInbox->>SessionStore: process replay or nego.open
  SessionStore-->>handleInbox: reject or preserve session state
  handleInbox-->>AdversarialTest: return result and status
Loading
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR also changes protocol state-machine logic, protocol tests, and docs, which are outside the linked issue's e2e/* scope. Split the protocol/state-machine and docs changes into a separate PR or link a broader issue that explicitly includes them.
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: freezing pending nego.open handling and adding the M3.3 adversarial e2e suite.
Linked Issues check ✅ Passed The PR covers the requested M3.3 adversarial e2e cases: tampered to, replayed seq, oversized envelope, unbonded sender, self-approval, and nego.open redelivery.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch test/m33-adversarial-e2e

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

const { thread } = await openSessionToSigned(alice, bob);
await redeliverNegoOpen(alice, bob, thread, {
...OPEN_PAYLOAD,
goal: HIJACK_GOAL,

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] The nego.open redelivery matrix only overrides goal with HIJACK_GOAL. The real pending-mutation threat (shown in nego-open-pending-mutation.probe.test.ts) also escalates acceptance, budget.max_turns, and especially mandate (e.g. emptying human_required). Because the freeze is all-or-nothing, goal-only checks are a reasonable proxy, but the suite currently claims “terms frozen” while only asserting goal invariance end-to-end. A regression that somehow reapplied mandate/acceptance while leaving goal alone would still pass every e2e case here.

Suggestion: In at least the pending (and ideally live) redelivery case, redeliver a full escalated payload matching the probe, then assert freeze of goal/mandate/acceptance/budget via handleSessionStatus and/or the session_open pending item fields—not only unwrapGoal(status.goal).

await assertStatusAndGoal(bob, thread, "live", OPEN_PAYLOAD.goal);
}, 45000);

it("signed: status + terms unchanged (light msg/report/sign path)", async () => {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] The highest-value pending scenario stops after redelivery + status/goal checks. It never re-runs human approve after the hijacked open and asserts the resulting live session still carries first-open terms. That approve path is exactly where the pre-fix store mutation became harmful (handleApproveOpen promotes the store record, not the redelivered wire payload). The protocol probe covers this; the e2e matrix does not, so “full stack” coverage of the security fix is incomplete despite looking thorough.

Suggestion: After redelivering the escalated nego.open on pending, call handleHumanApprove with the original pendingId / approval code and assert post-approve status live with original goal/mandate (and that no second session_open pending was created).

expect(opened.ok).toBe(true);
if (!opened.ok) return;

const first = await bobMachine.handleIncomingEnvelope({

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] With createLinkedMachines, aliceMachine.handleOpen already delivers nego.open into Bob’s machine. The subsequent first handleIncomingEnvelope is therefore a same-terms redelivery, not the initial open. The test still correctly proves escalated terms are ignored, but the narrative (“first” vs “second”) and setup are slightly misleading. Also, pending-queue assertions only check goal (lines 80/125) while the store checks are thorough—pre-fix ensureRecipientOpenPending already returned the existing pending without rewriting fields, so pending-only checks would not have caught the original bug.

Suggestion: Drop the redundant first delivery (or pause linked delivery for the initial open), and optionally assert pending mandate/acceptance/budget for documentation symmetry with the store checks. Keep store + approve assertions as the primary regression signal.

}

const redelivered = await bobMachine.handleIncomingOpen({
thread: opened.thread,

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] The new unit test only mutates goal via handleIncomingOpen and asserts the return shape { ok: true, status: "pending" }. That is useful for the early-return contract, but it largely duplicates the probe’s weaker axis and does not lock the freeze for mandate/acceptance/budget at the unit layer (those are probe-only).

Suggestion: Either extend this test to assert full term equality (matching the probe’s store expectations) or keep it narrowly as a return-shape test and rely on the probe for field-level freeze—worth a one-line comment so future edits do not treat this as full N1/pending coverage.

rfxlamia added 2 commits July 23, 2026 15:33
Assert full goal/mandate/acceptance/budget freeze in e2e and unit tests,
approve-after-hijack on pending, and drop redundant probe first delivery.
@rfxlamia
rfxlamia merged commit 6fa80dc into main Jul 23, 2026
@rfxlamia
rfxlamia deleted the test/m33-adversarial-e2e branch July 23, 2026 08:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

M3.3 Adversarial e2e suite

1 participant