Skip to content

fix: extract sender.id from object-shaped sender field (real #46) - #56

Merged
clawd-ops merged 2 commits into
mainfrom
fix/sender-object-extraction
Aug 15, 2026
Merged

clawd-ops merged 2 commits into
mainfrom
fix/sender-object-extraction

Conversation

@clawd-ops

Copy link
Copy Markdown
Owner

What

Fixes the actual sender-attribution bug that our Discord and HA Assist channels hit today. The Conversation info metadata block emits sender as a structured object ({id, name, username}), but extractSenderId only handled the string shape, so every message fell through to OWNER_ID and landed on the wrong peer.

Why not upstream plastic-labs#77 / fork PR #46

Per the three Codex reviews on #46: that PR removed the legacy string fallback (behavioral regression), didn't parse the object shape OpenClaw actually emits, had a build-breaking toolCtx undefined reference across multiple tools, and defaulted new deploys to a per-sender policy that would create a third peer split under raw platform IDs.

This PR does the minimum: adds object-shape extraction, keeps every existing path working, no new config knobs, no policy changes.

Extraction priority (in order)

  1. parsed.sender_id — top-level string, canonical envelope contract
  2. parsed.sender — string, legacy shape preserved for back-compat
  3. parsed.sender.id — object.id string, current OpenClaw + HA Assist shape

Tests

7 new unit tests in test/helpers.test.ts:

  • Discord envelope shape (sender: {id, name, username})
  • HA Assist envelope shape (sender: {id, name, source: "ha_assist"})
  • Priority: top-level sender_id string wins over sender.id object
  • Reject object with no .id
  • Reject .id that is not a string
  • Reject empty .id
  • (plus the 20 pre-existing tests still pass)

vitest run test/helpers.test.ts → 27 passed.

Downstream

Once merged and rebuilt into the OCI image (ghcr.io/clawd-ops/openclaw-honcho:latest), the home-ops PR that wires the fork into OpenClaw will pick this up automatically. No plugin config changes required on the deployment side.

Clawd added 2 commits August 15, 2026 01:33
OpenClaw's Conversation info metadata block emits sender as an object:
  "sender": { "id": "378376356108435457", "name": "bigrob8181", ... }

The previous string-only fallback (parsed.sender_id ?? parsed.sender) returned
undefined when parsed.sender was an object, causing every Discord/HA-Assist
message to fall through to the OWNER_ID default peer instead of the actual
sender. This is the root cause of the "everything goes to owner instead of rob"
attribution bug.

Extraction priority is now:
  1. parsed.sender_id (top-level string) — canonical envelope contract
  2. parsed.sender (string) — legacy shape, preserved for back-compat
  3. parsed.sender.id (object.id string) — current OpenClaw/HA-Assist shape

Tests cover Discord envelope shape, HA Assist envelope shape, priority
ordering, and rejection of non-string / empty sender.id values.

Distinct from upstream plastic-labs#77 (fork PR #46), which removed the string fallback
entirely and did not add object-shape support. That PR is DO NOT MERGE per
the Codex review consensus.
Codex review flagged that `parsed.sender_id ?? parsed.sender` uses value-presence
fallback rather than type-aware fallback. A malformed higher-priority field
(e.g. `sender_id: 12345`) would short-circuit the operator and skip the valid
`sender.id` object fallback, returning undefined instead.

Each of the three extraction paths is now type-guarded independently:
  1. sender_id (must be non-empty string)
  2. sender (must be non-empty string)
  3. sender.id (must be non-empty string)

A malformed candidate at any level falls through to the next path.

Two additional tests:
  - non-string sender_id falls back to valid sender.id object
  - null sender_id falls back to legacy sender string
@clawd-ops

Copy link
Copy Markdown
Owner Author

Codex Review (gpt-5.5)

Automated adversarial review via codex exec.

Verdict: MERGE WITH CHANGES

This is the right minimal fix for the production bug. It correctly handles the OpenClaw-emitted shape:

sender: { id, name, username }

and it preserves the legacy sender: "..." path that upstream PR plastic-labs#77 reportedly removed. I do not see the broad regression risk that made the upstream-style fix dangerous.

Findings

1. Comment says “sender_id string → sender.id”, but code does sender_id ?? sender first

Current code:

const candidate = parsed.sender_id ?? parsed.sender;

If sender_id exists but is non-string, this will never inspect sender.id.

Example:

{
  "sender_id": 123,
  "sender": { "id": "rob" }
}

Expected from the new comment: return "rob"
Actual: return undefined

This is probably not relevant to the known OpenClaw bug, but the comment promises type-aware fallback and the implementation does value-presence fallback.

Suggested fix:

if (typeof parsed.sender_id === "string" && parsed.sender_id.length > 0) {
  return parsed.sender_id;
}

const sender = parsed.sender;
if (typeof sender === "string" && sender.length > 0) {
  return sender;
}

if (sender && typeof sender === "object") {
  const objId = (sender as { id?: unknown }).id;
  if (typeof objId === "string" && objId.length > 0) {
    return objId;
  }
}

Add one test for malformed sender_id plus valid sender.id.

2. Test count mismatch

The prompt says 7 new tests, but the shown diff adds 6 tests. Not a code issue, but worth reconciling before merge.

3. Security posture is unchanged, not improved

This continues to trust sender metadata embedded in message content. Adding object support does not materially worsen spoofing risk because the old string paths already allowed trusted metadata to specify sender identity.

That said, if arbitrary users can submit raw messages containing the metadata block, then both old and new behavior are spoofable. This PR should not be considered a security boundary. Sender attribution needs to remain enforced at envelope creation / ingestion boundary, not by this parser.

Coverage

The new tests cover the important path:

  • OpenClaw object sender
  • HA Assist object sender
  • sender_id priority
  • object missing id
  • non-string id
  • empty string id

Missing edge case:

  • invalid/non-string sender_id should fall back to valid sender.id, if that is the intended priority.

Simplicity

The change is small and scoped. No dead code, no over-engineering. The only issue is the ?? candidate selection being slightly too clever for the documented priority.

Final Recommendation

Merge after changing the fallback logic to be type-aware and adding the one malformed-sender_id test. If production urgency is high, this is still far safer than PR #46/plastic-labs#77 because it preserves legacy string sender behavior and fixes the actual OpenClaw object shape.

Fixup applied (commit 81ca94e)

  • Each extraction path (sender_id string / sender string / sender.id object) is now type-guarded independently. Non-string sender_id no longer short-circuits the operator.
  • 2 new tests: non-string sender_id falls back to sender.id; null sender_id falls back to legacy sender string.
  • Test count now 29/29 passing.

Re-review coming up.

@clawd-ops

Copy link
Copy Markdown
Owner Author

Codex Re-Review (gpt-5.5) — after fixup 81ca94e

Finding fully addressed.

The fixup removes the value-presence fallback bug: sender_id, legacy sender, and sender.id are now each independently type-guarded, so malformed/non-string sender_id no longer blocks valid lower-priority paths. The added tests cover the important regressions: non-string sender_id falling through to sender.id, and null sender_id falling through to legacy sender.

No new fixup-introduced issues found.

Remaining edge cases worth noting, but not merge blockers:

  • Whitespace-only IDs like " " are still accepted because the check is length > 0, not trim().length > 0. That appears consistent with existing behavior.
  • If sender is an array, it passes typeof sender === "object" and checks .id; harmless, but broad.
  • There is no explicit test for malformed higher-priority sender_id falling through to legacy sender when sender_id is a non-null non-string other than the null case, but behavior is covered structurally.

Verdict: MERGE.

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.

1 participant