Skip to content

feat(discord): add expiresAtMs and bindSession to component spec#60763

Open
geekhuashan wants to merge 3 commits intoopenclaw:mainfrom
geekhuashan:pr-60355
Open

feat(discord): add expiresAtMs and bindSession to component spec#60763
geekhuashan wants to merge 3 commits intoopenclaw:mainfrom
geekhuashan:pr-60355

Conversation

@geekhuashan
Copy link
Copy Markdown
Contributor

Summary

Add two new optional fields to DiscordComponentMessageSpec:

  1. expiresAtMs: number — Unix epoch timestamp in ms after which the component(s) expire. When set, the components are registered with an expiry time, making them unavailable after the specified moment.

  2. bindSession: boolean — When false, interactions are not bound to the session that registered the components, allowing cross-session component usage.

Rebased from original PR #60355 with conflicts resolved.

Copilot AI review requested due to automatic review settings April 4, 2026 08:29
@openclaw-barnacle openclaw-barnacle bot added channel: discord Channel integration: discord size: XS labels Apr 4, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 4, 2026

Greptile Summary

Adds expiresAtMs and bindSession to DiscordComponentMessageSpec with matching schema entries and read/build wiring. The implementation is correct: expiresAtMs maps directly to the ms-based expiresAt field used by the component registry, and bindSession === false correctly nulls out the sessionKey for all component and modal entries.

Confidence Score: 5/5

Safe to merge; only remaining finding is a P2 test coverage gap for expiresAtMs.

All logic is correct and follows existing patterns. The only gap is a missing test for expiresAtMs propagation, which is a style/coverage suggestion and not a blocking defect.

No files require special attention.

Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/discord/src/components.test.ts
Line: 83-89

Comment:
**Missing `expiresAtMs` test coverage**

`bindSession=false` has a read-path test, but `expiresAtMs` does not. A parallel test for `readDiscordComponentSpec` and the propagation through `buildDiscordComponentMessage` into the entries/modal would give the new field equal coverage.

```suggestion
  it("reads bindSession=false from spec", () => {
    const spec = readDiscordComponentSpec({
      blocks: [{ type: "text", text: "hello" }],
      bindSession: false,
    });
    expect(spec?.bindSession).toBe(false);
  });

  it("reads expiresAtMs from spec and propagates to entries", () => {
    const ts = Date.now() + 60_000;
    const spec = readDiscordComponentSpec({
      blocks: [{ type: "actions", buttons: [{ label: "Go", callbackData: "x" }] }],
      expiresAtMs: ts,
    });
    expect(spec?.expiresAtMs).toBe(ts);
    const result = buildDiscordComponentMessage({ spec: spec! });
    expect(result.entries[0]?.expiresAt).toBe(ts);
  });
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "feat(discord): add bindSession to compon..." | Re-trigger Greptile

Comment thread extensions/discord/src/components.test.ts
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR extends the Discord components v2 message spec to support explicit expiry and optional cross-session interaction routing, enabling components (buttons/selects/modals) to expire at a caller-chosen time and to route clicks through the channel’s normal session when desired.

Changes:

  • Add expiresAtMs?: number and bindSession?: boolean to DiscordComponentMessageSpec and parse them from raw specs.
  • Propagate expiry (expiresAtMs → registry expiresAt) and session binding behavior (bindSession === false → omit sessionKey) when building component entries and modal entries.
  • Extend the message tool TypeBox schema to accept expiresAtMs and bindSession; add a basic parsing test for bindSession.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
extensions/discord/src/message-tool-schema.ts Adds expiresAtMs/bindSession to the TypeBox schema and updates schema description text.
extensions/discord/src/components.ts Extends component spec type + parsing; propagates expiry and session binding into generated registry entries/modals.
extensions/discord/src/components.test.ts Adds a test ensuring bindSession=false is parsed from the spec.

Comment thread extensions/discord/src/components.test.ts
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 43f1e85911

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread extensions/discord/src/components.ts Outdated
Comment on lines 947 to 949
sessionKey: boundSessionKey,
agentId: params.agentId,
accountId: params.accountId,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Unbind agent/account when bindSession is false

With bindSession=false, this code only clears sessionKey but still stamps each entry with agentId and accountId. Later, interaction dispatch treats those as route overrides, so the handler can run under the original agent/account while using the newly resolved channel session key, producing a mixed route context. This breaks the documented “channel’s normal session” behavior in environments where routing can change (for example multi-agent channels or changed bindings) and can persist session updates under the wrong agent store path.

Useful? React with 👍 / 👎.

@geekhuashan
Copy link
Copy Markdown
Contributor Author

Rebased onto upstream/main (2026.4.14)

This PR has been rebased onto the latest upstream/main and is now mergeable. Ready for review.

Add bindSession?: boolean to createDiscordMessageToolComponentsSchema() so callers
can opt out of session-key binding for cron-owned components. Add test coverage.
@aisle-research-bot
Copy link
Copy Markdown

aisle-research-bot bot commented Apr 15, 2026

🔒 Aisle Security Analysis

We found 2 potential security issue(s) in this PR:

# Severity Title
1 🟠 High Discord component interactions can be executed without session binding when bindSession=false
2 🟡 Medium Unbounded component expiration timestamp allows long-lived registry entries (memory/DoS risk)
1. 🟠 Discord component interactions can be executed without session binding when `bindSession=false`
Property Value
Severity High
CWE CWE-285
Location extensions/discord/src/components.ts:791-805

Description

buildDiscordComponentMessage() now supports spec.bindSession=false, which clears sessionKey, agentId, and accountId on registered component entries/modals.

Downstream, handleDiscordComponentEvent() uses these fields as routeOverrides, and when they are undefined it falls back to the current interaction’s derived route (i.e., the channel’s “normal session”). This means:

  • A component created in a privileged/bound session can be configured to execute in the channel route instead.
  • Any user who can click the component (and passes general guild/channel allowlisting) can trigger the callback to run under the channel route.
  • Because allowedUsers is optional and is not enforced as a requirement when bindSession=false, components may unintentionally become usable by other channel members, potentially causing cross-user action triggering (approval buttons, administrative actions, etc.).

Vulnerable code:

const bindSession = params.spec.bindSession !== false;
const boundSessionKey = bindSession ? params.sessionKey : undefined;
...
entries.push({
  ...entry,
  sessionKey: boundSessionKey,
  agentId: boundAgentId,
  accountId: boundAccountId,
});

This is a broken access-control / authorization-risk pattern because the session/agent/account context is part of the security boundary for interaction routing.

Recommendation

Tie interactive callbacks to a security principal even when routing via the “channel session”. Options:

  1. Require allowedUsers when bindSession=false (fail closed):
if (params.spec.bindSession === false) {
  const hasAnyAllowedUsers = (params.spec.blocks ?? []).some(/* check entries */) ||
    Boolean(params.spec.modal?.allowedUsers?.length);
  if (!hasAnyAllowedUsers) {
    throw new Error("bindSession=false requires allowedUsers to prevent cross-user interaction");
  }
}
  1. Store and enforce the originating user id for every entry/modal (recommended):
  • Add originatingUserId on DiscordComponentEntry/DiscordModalEntry at creation time.
  • In the interaction handlers, reject when interaction.user.id !== originatingUserId unless an explicit allowlist is provided.
  1. If bindSession=false is intended for shared components, make it explicit and safe:
  • rename to something like sharedInChannel=true and require allowlist/role checks.

Also document clearly that disabling session binding changes the authorization model and should not be used for privileged actions.

2. 🟡 Unbounded component expiration timestamp allows long-lived registry entries (memory/DoS risk)
Property Value
Severity Medium
CWE CWE-400
Location extensions/discord/src/components.ts:444-804

Description

expiresAtMs from the untrusted components payload is accepted as any finite number and is propagated directly into the in-memory component registry as expiresAt.

  • Input: components.expiresAtMs (tool payload / message schema)
  • Validation: only typeof number && Number.isFinite (no bounds, no minimum/maximum, no relation to Date.now())
  • Sink/impact: stored in global Map via registerDiscordComponentEntries(); entries are only removed when resolved or when expiresAt <= now. A far-future timestamp keeps entries resident for a very long time, enabling memory growth if many messages/components are created with extremely large expiresAtMs.

Vulnerable propagation:

const expiresAtMs = readOptionalNumber(obj.expiresAtMs);
...
expiresAt: entry.expiresAt ?? params.spec.expiresAtMs,

Recommendation

Enforce safe bounds for expiration timestamps before storing them.

  • Reject values in the past (or treat as immediate expiry explicitly)
  • Enforce a maximum TTL/window (e.g., now + MAX_COMPONENT_TTL_MS) regardless of caller-provided timestamp
  • Consider adding JSON-schema constraints too, but do not rely on schema alone; enforce in code.

Example (code-side validation):

const MAX_COMPONENT_TTL_MS = 30 * 60 * 1000; // or another safe upper bound

function readOptionalExpiresAtMs(value: unknown, now = Date.now()): number | undefined {
  if (typeof value !== "number" || !Number.isFinite(value)) return undefined;// clamp to [now, now + MAX]
  const min = now;
  const max = now + MAX_COMPONENT_TTL_MS;
  if (value < min) return min;
  if (value > max) return max;
  return value;
}

Then use readOptionalExpiresAtMs(obj.expiresAtMs) instead of readOptionalNumber and propagate the validated value.


Analyzed PR: #60763 at commit cfa0f21

Last updated on: 2026-04-15T22:07:24Z

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 78096dfef7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread extensions/discord/src/components.ts Outdated
Comment on lines 896 to 898
sessionKey: boundSessionKey,
agentId: params.agentId,
accountId: params.accountId,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Clear modal route overrides when bindSession is false

When bindSession=false, this block still persists agentId/accountId on modal registry entries even though sessionKey is intentionally unbound. On modal submit, dispatchDiscordComponentEvent applies those values as route overrides, so the event can run under the original sender's agent/account instead of the channel’s current route (e.g., multi-agent channels or changed bindings). This makes the new unbound-session mode inconsistent for forms and can write follow-up state under the wrong route context.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: discord Channel integration: discord size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants