Skip to content

fix(api-service): add Redis-backed action tokens for chat card buttons fixes NV-7956#11430

Merged
scopsy merged 7 commits into
nextfrom
cursor/platform-action-token-indirection
Jun 3, 2026
Merged

fix(api-service): add Redis-backed action tokens for chat card buttons fixes NV-7956#11430
scopsy merged 7 commits into
nextfrom
cursor/platform-action-token-indirection

Conversation

@scopsy
Copy link
Copy Markdown
Contributor

@scopsy scopsy commented Jun 3, 2026

Summary

  • Adds AgentActionTokenService to mint short at: tokens (16-byte entropy) in Redis with 3-day TTL (AGENT_ACTION_TOKEN_TTL).
  • Egress: OutboundGateway tokenizes card button actions on a delivery-only clone before posting/editing; history persists the original card with raw action IDs. Slack-native blocks are unchanged.
  • Ingress: ChatInstanceRegistry resolves at: tokens back to real { id, value } before dispatch; missing/expired/mismatched tokens no-op with a warning.

Fixes Telegram tool-approval cards (and any long custom action IDs) failing silently when callback_data exceeds 64 bytes.

Linear: https://linear.app/novu/issue/NV-7956

Architecture

flowchart TD
    subgraph egress [OutboundGateway]
        card[Card with real button ids] --> clone[Clone card]
        clone --> mint["mint at: token per button -> Redis"]
        mint --> deliver[Deliver tokenized card]
        card --> persist[Persist original card]
    end
    deliver --> platform[Telegram / WhatsApp / Teams / Slack portable]
    platform --> click[User clicks button]
    click --> onAction[ChatInstanceRegistry.onAction]
    subgraph ingress [Ingress]
        onAction --> check{starts with at:?}
        check -- no --> passthru[Pass through raw id]
        check -- yes --> lookup[Redis GET + binding check]
        lookup --> restore[Restore id + value]
    end
    passthru --> dispatch[managed / bridge onAction]
    restore --> dispatch
Loading

Breaking changes

None. Non-prefixed action IDs continue to pass through unchanged.

Test plan

  • Unit: AgentActionTokenService mint/resolve/binding/tokenize card
  • Unit: OutboundGateway delivery clone + fallback on mint failure
  • Unit: ChatInstanceRegistry token resolution + raw passthrough
  • Manual: Telegram — trigger tool approval (requires_action), confirm card delivers and Approve/Deny round-trips
  • Manual: Slack — confirm tool approval still works (native blocks unchanged)

Made with Cursor

What changed

Introduces Redis-backed opaque action tokens (prefix "at:") for agent chat-card buttons to avoid platform payload limits (notably Telegram’s 64‑byte callback_data). Buttons that would produce oversized callback payloads are replaced with short, random tokens (16 bytes entropy, URL-safe) stored in Redis with a configurable TTL (default 3 days); the original IDs/values are preserved in persisted history. Outbound delivery tokenizes cards; inbound handling resolves tokens and validates binding claims (agent, integration, environment, organization). Token resolution failures fall back to safe behaviors and log warnings.

Affected areas

  • api: Adds AgentActionTokenService, shared opaque-token utilities, a card-callback-button walker, and tests; OutboundGateway now tokenizes cards for post/send/edit flows and accepts an actionTokenBinding option; ChatInstanceRegistry resolves inbound tokens before dispatch; AgentEmailActionTokenService now uses the shared token utilities.
  • dashboard: UX tweak for onboarding — default use case changed to "Inbox" and a new telemetry event USECASE_SELECTED added.

Key technical decisions

  • Tokens are cryptographically random 16‑byte values, base64url-encoded and stored under opaque Redis keys with TTL to limit exposure and cleanup.
  • Resolution enforces strict binding equality (agentId, integrationIdentifier, environmentId, organizationId) to prevent token reuse across contexts.
  • Delivery tokenization deep-clones cards so persisted payloads retain original action IDs/values; failures during tokenization fall back to posting raw content and log warnings.

Testing

New unit tests cover AgentActionTokenService (mint/resolve/tokenize), opaque-token utilities, OutboundGateway tokenization and fallback, and ChatInstanceRegistry token resolution; manual Telegram/Slack checks and full e2e verification remain pending.

Greptile Summary

This PR introduces AgentActionTokenService, which mints short Redis-backed opaque tokens (16-byte entropy, at: prefix, 3-day TTL) to replace long button action IDs in chat cards before delivery, solving Telegram's 64-byte callback_data limit. Outbound paths deep-clone cards and tokenize only buttons whose encoded payload would exceed the limit; history persists the original IDs; inbound resolution validates a four-field binding and restores the original { id, value } before dispatch.

  • Egress: OutboundGateway now applies applyActionTokensForDelivery in postToConversation, sendDirectMessage, editInConversation, and optionally replyOnThread (via new actionTokenBinding opt-in param); Slack-native block paths are untouched.
  • Ingress: ChatInstanceRegistry.onAction resolves at: tokens via resolveForDispatch; missing/expired/binding-mismatched tokens no-op with a warning; raw (non-prefixed) action IDs pass through unchanged.
  • Shared utils: Opaque-token helpers (mintRandomToken, isMintedOpaqueActionId, buildOpaqueStorageKey, parseTtlFromEnv) are extracted and reused by the pre-existing AgentEmailActionTokenService.

Confidence Score: 5/5

Safe to merge — token mint, binding validation, and card-clone logic are all correct; existing callers of replyOnThread use link-buttons or markdown only so no live path bypasses tokenization.

The implementation correctly deep-clones cards before mutating, enforces four-field binding equality on resolution, and passes through raw action IDs unchanged. All new Redis operations are wrapped with proper error handling and fallbacks. Unit tests cover mint/resolve round-trips, binding mismatches, cache failures, partial mint failures, and the raw-passthrough path.

No files require special attention. The one style note is in agent-action-token.service.ts (redundant ternary in the warning log).

Important Files Changed

Filename Overview
apps/api/src/app/agents/conversation-runtime/action-token/agent-action-token.service.ts Core token service: mint, resolve, binding validation, and card walk/tokenize — correctly deep-clones the card before mutating, sequential minting prevents interleaving, partial failures propagate cleanly.
apps/api/src/app/agents/conversation-runtime/egress/outbound.gateway.ts Three delivery paths now tokenize cards; replyOnThread gained an actionTokenBinding opt-in. All current callers of replyOnThread use markdown or link-button only, so no existing path is affected.
apps/api/src/app/agents/conversation-runtime/ingress/chat-instance.registry.ts Inbound onAction handler now resolves action tokens before dispatch; binding check uses live config values; raw IDs pass through correctly.
apps/api/src/app/agents/conversation-runtime/action-token/card-callback-button.walker.ts Walker correctly traverses section → actions → button hierarchy; link-button and other non-button types are skipped; byte-length calculation matches the Telegram chat:{"a":"..."} encoding format.
apps/api/src/app/agents/conversation-runtime/action-token/opaque-token.util.ts Extracted shared utilities for random token generation, TTL parsing, storage key building, and token identity checks — mathematically correct base64url length formula for any byte count.
apps/api/src/app/agents/email/agent-email-action-token.service.ts Refactored to use shared opaque-token utilities; no functional changes to email token logic.

Sequence Diagram

sequenceDiagram
    participant Agent as Agent Runtime
    participant OG as OutboundGateway
    participant ATS as AgentActionTokenService
    participant Redis as Redis (CacheService)
    participant Platform as Telegram / WhatsApp / Teams
    participant CIR as ChatInstanceRegistry (onAction)

    Note over Agent,Platform: Egress — card delivery
    Agent->>OG: deliver(target, msg, persist)
    OG->>OG: applyActionTokensForDelivery(card, binding)
    OG->>ATS: tokenizeCardForDelivery(card, binding)
    ATS->>ATS: structuredClone(card)
    loop for each callback button needing tokenization
        ATS->>Redis: "SET agent:action:{token} claims TTL=3d"
    end
    ATS-->>OG: tokenized card clone
    OG->>Platform: "thread.post({ card: tokenizedCard })"
    OG->>OG: persistDelivered(originalMsg) [original IDs kept]

    Note over Platform,CIR: Ingress — user clicks button
    Platform->>CIR: "onAction(event.actionId = at:xxx...)"
    CIR->>ATS: resolveForDispatch(actionId, value, binding)
    alt actionId is a minted token
        ATS->>Redis: "GET agent:action:{token}"
        Redis-->>ATS: claims JSON
        ATS->>ATS: claimsMatchBinding?
        ATS-->>CIR: "{ id: originalId, value: originalValue }"
        CIR->>Agent: callbacks.onAction(resolvedAction)
    else actionId is a raw (non-token) id
        ATS-->>CIR: "{ id: actionId, value }"
        CIR->>Agent: callbacks.onAction(rawAction)
    else token missing / expired / binding mismatch
        ATS-->>CIR: null
        CIR->>CIR: no-op + warn log
    end
Loading

Comments Outside Diff (1)

  1. apps/api/src/app/agents/conversation-runtime/egress/outbound.gateway.ts, line 157-189 (link)

    P1 replyOnThread bypasses action-token tokenization

    replyOnThread passes its payload directly to toThreadPostArg without going through applyActionTokensForDelivery. In bridge.runtime.ts line 133–135 this method is already called with { card: reply }, and the signature accepts any OutboundMessage including ones with button children. Any card with long action IDs posted through this path — on Telegram in particular — would still exceed the 64-byte callback_data limit. The three delivery methods fixed by this PR (postToConversation, sendDirectMessage, editInConversation) won't help if this path is used.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/api/src/app/agents/conversation-runtime/egress/outbound.gateway.ts
    Line: 157-189
    
    Comment:
    **`replyOnThread` bypasses action-token tokenization**
    
    `replyOnThread` passes its payload directly to `toThreadPostArg` without going through `applyActionTokensForDelivery`. In `bridge.runtime.ts` line 133–135 this method is already called with `{ card: reply }`, and the signature accepts any `OutboundMessage` including ones with `button` children. Any card with long action IDs posted through this path — on Telegram in particular — would still exceed the 64-byte `callback_data` limit. The three delivery methods fixed by this PR (`postToConversation`, `sendDirectMessage`, `editInConversation`) won't help if this path is used.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Cursor

Fix All in Cursor

Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
apps/api/src/app/agents/conversation-runtime/action-token/agent-action-token.service.ts:115
By the time this log statement is reached, the early-return guard on line 104 has already established that `actionId` IS a minted token — the inner `this.isActionToken(actionId)` ternary is always true and the `else` branch (`actionId`) is dead code. Writing the redacted prefix directly makes the intent clearer and avoids the repeated regex/check.

```suggestion
          actionId: `${AGENT_ACTION_TOKEN_PREFIX}[redacted]`,
```

Reviews (5): Last reviewed commit: "api(agents): fix CardElement casts for n..." | Re-trigger Greptile

Short-lived at: tokens fix Telegram callback_data limits and keep
long action ids out of platform payloads while preserving raw ids in history.
@linear-code
Copy link
Copy Markdown

linear-code Bot commented Jun 3, 2026

NV-7956

@netlify
Copy link
Copy Markdown

netlify Bot commented Jun 3, 2026

Deploy Preview for dashboard-v2-novu-staging canceled.

Name Link
🔨 Latest commit 06736e6
🔍 Latest deploy log https://app.netlify.com/projects/dashboard-v2-novu-staging/deploys/6a201bc4781b0e00089cdc2f

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 3, 2026

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds a complete action tokenization system for agent-driven button actions. It introduces AgentActionTokenService to mint opaque, cache-backed tokens prefixed with at:, integrates tokenization into outbound message delivery pipelines, and adds token resolution to inbound action handling with binding validation.

Changes

Agent Action Token Minting, Delivery, and Resolution

Layer / File(s) Summary
Shared Opaque Token Utilities
apps/api/src/app/agents/conversation-runtime/action-token/opaque-token.util.ts, apps/api/src/app/agents/conversation-runtime/action-token/opaque-token.util.spec.ts
parseTtlFromEnv, mintRandomToken, buildOpaqueStorageKey, token-body length calculation, and isMintedOpaqueActionId are added with tests.
Card Action Button Traversal
apps/api/src/app/agents/conversation-runtime/action-token/card-callback-button.walker.ts
Adds Telegram callback payload constants, byte-length calculation, callbackPayloadNeedsTokenization, type guards, and forEachCallbackButton recursive traversal.
Action Token Service Implementation and Tests
apps/api/src/app/agents/conversation-runtime/action-token/agent-action-token.service.ts, agent-action-token.service.spec.ts
Adds AgentActionTokenService with isActionToken, mintActionToken, resolveActionToken, resolveForDispatch, and tokenizeCardForDelivery; includes comprehensive tests covering minting, resolution, binding checks, tokenization, error propagation, and reuse.
Outbound Gateway Tokenization
apps/api/src/app/agents/conversation-runtime/egress/outbound.gateway.ts, outbound.gateway.action-token.spec.ts
Injects AgentActionTokenService into OutboundGateway, tokenizes portable card deliveries via applyActionTokensForDelivery/buildThreadPostArg, and adds tests for successful tokenization, error fallback, and markdown-only bypass.
Inbound Gateway Token Resolution
apps/api/src/app/agents/conversation-runtime/ingress/chat-instance.registry.ts
Injects AgentActionTokenService and emailActionTokenService, switches email URL signing to emailActionTokenService, and resolves inbound action tokens with binding validation in chat.onAction.
Email Service Utility Delegation
apps/api/src/app/agents/email/agent-email-action-token.service.ts
Refactors to use parseTtlFromEnv, mintRandomToken, and buildOpaqueStorageKey from shared utilities instead of local crypto and concat logic.
Module Dependency Registration
apps/api/src/app/agents/agents.module.ts
Adds AgentActionTokenService import and registers it in the module's providers.
Dashboard usecase & telemetry
apps/dashboard/src/pages/usecase-select-page.tsx, apps/dashboard/src/utils/telemetry.ts
Reorders usecase options to default to Inbox, records TelemetryEvent.USECASE_SELECTED on continue, and adds the telemetry enum member.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • novuhq/novu#9010: Modifies onboarding/telemetry enum entries related to usecase selection; relevant to the telemetry enum change in this PR.
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title does not follow Conventional Commits format; it includes 'fixes NV-7956' at the end instead of following the required 'type(scope): description fixes XXX-XXXX' pattern. Reorder the title to 'fix(api): add Redis-backed action tokens for chat card buttons fixes NV-7956' to comply with Conventional Commits format with Linear ticket reference at the end.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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 and usage tips.


if (!resolved) {
this.logger.warn(
{ agentId, integrationIdentifier: config.integrationIdentifier, actionId },
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 Agentic Security Review
Severity: MEDIUM

actionId is logged on token-resolution failures, and for tokenized actions this value is the raw at: bearer token. Because these tokens remain valid for up to 3 days, anyone with log access can replay a leaked token within its binding scope.

Consider redacting actionId here (or logging a one-way hash/short fingerprint) so failed resolutions cannot leak replayable token material.

Fix in Cursor Fix in Web

Reviewed by Cursor Security Reviewer for commit 8fef358. Configure here.

Share opaque-token utilities with email, add typed card walker and
resolveForDispatch with full env binding, all-or-nothing mint, and
replyOnThread tokenization hook.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/api/src/app/agents/conversation-runtime/action-token/agent-action-token.service.ts (1)

13-32: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Use interface for these backend object contracts.

AgentActionTokenClaims, AgentActionTokenBinding, and MintedButtonToken are plain object shapes in apps/api, so they should be declared as interfaces to follow the repo rule. As per coding guidelines, **/*.{ts,tsx}: On the backend: use interface for type definitions; on the frontend: use type for type definitions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/api/src/app/agents/conversation-runtime/action-token/agent-action-token.service.ts`
around lines 13 - 32, Replace the three plain type aliases with interface
declarations: change AgentActionTokenClaims, AgentActionTokenBinding, and
MintedButtonToken to interfaces (keeping exported status for the ones currently
exported), preserve all fields and optional modifiers (e.g., value?: string) and
the referenced ButtonElement type on MintedButtonToken; ensure the names remain
identical so existing usages of AgentActionTokenClaims, AgentActionTokenBinding,
and MintedButtonToken continue to work.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@apps/api/src/app/agents/conversation-runtime/action-token/agent-action-token.service.ts`:
- Around line 46-47: isActionToken currently treats any id starting with
AGENT_ACTION_TOKEN_PREFIX as an opaque token; change it to only return true when
the id matches the full minted agent-action token format (i.e., the exact token
shape used to create tokens in this module) so raw action ids like "at:approve"
pass through; update isActionToken (and the same logic block referenced around
lines 115–130) to validate against the canonical token pattern used by
AgentActionToken/AGENT_ACTION_TOKEN_PREFIX and not just a prefix; additionally,
add a validation/rejection at card-construction time for ids that would collide
with the reserved minted token shape to prevent accidental reservation of the
prefix.

---

Outside diff comments:
In
`@apps/api/src/app/agents/conversation-runtime/action-token/agent-action-token.service.ts`:
- Around line 13-32: Replace the three plain type aliases with interface
declarations: change AgentActionTokenClaims, AgentActionTokenBinding, and
MintedButtonToken to interfaces (keeping exported status for the ones currently
exported), preserve all fields and optional modifiers (e.g., value?: string) and
the referenced ButtonElement type on MintedButtonToken; ensure the names remain
identical so existing usages of AgentActionTokenClaims, AgentActionTokenBinding,
and MintedButtonToken continue to work.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 03d34a23-60d2-4f38-9eaa-f507f5aa7b68

📥 Commits

Reviewing files that changed from the base of the PR and between 8fef358 and 468049a.

📒 Files selected for processing (9)
  • apps/api/src/app/agents/conversation-runtime/action-token/agent-action-token.service.spec.ts
  • apps/api/src/app/agents/conversation-runtime/action-token/agent-action-token.service.ts
  • apps/api/src/app/agents/conversation-runtime/action-token/card-callback-button.walker.ts
  • apps/api/src/app/agents/conversation-runtime/action-token/opaque-token.util.spec.ts
  • apps/api/src/app/agents/conversation-runtime/action-token/opaque-token.util.ts
  • apps/api/src/app/agents/conversation-runtime/egress/outbound.gateway.action-token.spec.ts
  • apps/api/src/app/agents/conversation-runtime/egress/outbound.gateway.ts
  • apps/api/src/app/agents/conversation-runtime/ingress/chat-instance.registry.ts
  • apps/api/src/app/agents/email/agent-email-action-token.service.ts
✅ Files skipped from review due to trivial changes (1)
  • apps/api/src/app/agents/conversation-runtime/action-token/opaque-token.util.spec.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/api/src/app/agents/conversation-runtime/ingress/chat-instance.registry.ts
  • apps/api/src/app/agents/conversation-runtime/action-token/agent-action-token.service.spec.ts
  • apps/api/src/app/agents/conversation-runtime/egress/outbound.gateway.action-token.spec.ts

@scopsy
Copy link
Copy Markdown
Contributor Author

scopsy commented Jun 3, 2026

@cursoragent review comments and fix the e2e test

@cursor
Copy link
Copy Markdown
Contributor

cursor Bot commented Jun 3, 2026

Addressed the Greptile/CodeRabbit review comments and fixed the failing e2e test. Pushed to cursor/platform-action-token-indirection in commit 9ab4847c58.

E2E fix (agent-slack-roundtrip.e2e.ts)

The test failed because every card button was tokenized (at:wbYQ...), while Slack expects raw ids like confirm / cancel.

Fix: Tokenize only when the encoded Telegram callback_data would exceed 64 bytes (callbackPayloadNeedsTokenization). Short Slack action ids stay unchanged; long MCP approval ids (the NV-7956 case) still get tokens.

Verified locally: serializes Card replies into Slack Block Kit with intact action_ids passes.

Review comment fixes

Issue Fix
Misleading test name Renamed to propagates error when mint fails mid-card after an earlier button was tokenized; uses two long action ids that actually need tokenization
Binding log missing organizationId Added organizationId / tokenOrganizationId to the mismatch warning
at: prefix collision isActionToken now requires full minted shape at: + 22-char base64url body; at:approve passes through on ingress
Security (token in logs) Inbound warnings redact minted tokens as at:[redacted]
CodeRabbit interfaces Switched backend types to interface

Not changed (already handled or out of scope)

  • replyOnThread tokenization — already opt-in via actionTokenBinding; bridge cards without callback buttons don’t need it
  • Section-only recursion — left as-is (P2; no failing test)

Unit tests (19) and the Slack e2e case above pass on this branch.

Open in Web Open in Cursor 

Only mint action tokens when Telegram callback_data would exceed 64
bytes, so Slack keeps raw action_ids. Match the full at:<22-char>
minted shape on ingress so ids like at:approve pass through. Add
organizationId to binding-mismatch logs, redact tokens in warnings,
and rename the mid-card mint failure spec.

Co-authored-by: Dima Grossman <dima@grossman.io>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/api/src/app/agents/conversation-runtime/ingress/chat-instance.registry.ts (1)

451-458: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Redact action tokens in the error path too.

The warning branch masks opaque at: tokens, but this catch block still logs and reports the raw event.actionId. Those tokens stay valid until TTL expiry, so leaking them to logs/Sentry weakens the protection this PR adds.

Proposed fix
       } catch (err) {
-        this.logger.error(err, `[agent:${agentId}] Error handling action ${event.actionId}`);
+        const actionIdForLogs = this.agentActionTokenService.isActionToken(event.actionId)
+          ? `${event.actionId.slice(0, 3)}[redacted]`
+          : event.actionId;
+
+        this.logger.error(err, `[agent:${agentId}] Error handling action ${actionIdForLogs}`);
         captureAgentException(err, {
           component: 'chat-instance-registry',
           operation: 'on-action',
           agentId,
-          extra: { actionId: event.actionId },
+          extra: { actionId: actionIdForLogs },
         });
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/api/src/app/agents/conversation-runtime/ingress/chat-instance.registry.ts`
around lines 451 - 458, The catch block currently logs and reports the raw
event.actionId, leaking opaque "at:" tokens; update the catch to mask/redact the
action id using the same masking utility used in the warning branch before
calling this.logger.error and before passing extra.actionId to
captureAgentException (i.e., replace direct event.actionId with the
masked/redacted value when invoking this.logger.error and captureAgentException
in the chat-instance-registry on-action error path). Ensure you reference the
same masking function the warning branch uses so both logs and Sentry receive
the redacted token.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In
`@apps/api/src/app/agents/conversation-runtime/ingress/chat-instance.registry.ts`:
- Around line 451-458: The catch block currently logs and reports the raw
event.actionId, leaking opaque "at:" tokens; update the catch to mask/redact the
action id using the same masking utility used in the warning branch before
calling this.logger.error and before passing extra.actionId to
captureAgentException (i.e., replace direct event.actionId with the
masked/redacted value when invoking this.logger.error and captureAgentException
in the chat-instance-registry on-action error path). Ensure you reference the
same masking function the warning branch uses so both logs and Sentry receive
the redacted token.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3e44782d-d15e-43b6-82d5-60448c621a9d

📥 Commits

Reviewing files that changed from the base of the PR and between 468049a and 9ab4847.

📒 Files selected for processing (6)
  • apps/api/src/app/agents/conversation-runtime/action-token/agent-action-token.service.spec.ts
  • apps/api/src/app/agents/conversation-runtime/action-token/agent-action-token.service.ts
  • apps/api/src/app/agents/conversation-runtime/action-token/card-callback-button.walker.ts
  • apps/api/src/app/agents/conversation-runtime/action-token/opaque-token.util.spec.ts
  • apps/api/src/app/agents/conversation-runtime/action-token/opaque-token.util.ts
  • apps/api/src/app/agents/conversation-runtime/ingress/chat-instance.registry.ts
✅ Files skipped from review due to trivial changes (1)
  • apps/api/src/app/agents/conversation-runtime/action-token/opaque-token.util.spec.ts

Reorder usecase options to surface Inbox first and change the default selection to 'inbox'. Add a new TelemetryEvent (USECASE_SELECTED) and call telemetry when the user continues from the selector. Adjust handleContinue navigation flow to ensure correct early return after navigating to inbox.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@apps/api/src/app/agents/conversation-runtime/action-token/agent-action-token.service.ts`:
- Around line 131-140: The tokenization predicate
callbackPayloadNeedsTokenization currently only checks button.id, so update the
check used by forEachCallbackButton (and the predicate in
card-callback-button.walker.ts) to evaluate the combined/encoded callback
payload derived from both button.id and button.value (e.g., treat the button as
CallbackPayloadButton with 'id' and 'value'), and return true when the
serialized/encoded payload would exceed the platform callback-data budget; then
use that predicate before calling this.mintActionToken so long button.value is
tokenized/removed when required (refer to forEachCallbackButton,
callbackPayloadNeedsTokenization, and mintActionToken).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 67959bf3-812b-4ac8-af9d-032e050b0ad6

📥 Commits

Reviewing files that changed from the base of the PR and between 9ab4847 and dfdcb31.

📒 Files selected for processing (8)
  • apps/api/src/app/agents/conversation-runtime/action-token/agent-action-token.service.spec.ts
  • apps/api/src/app/agents/conversation-runtime/action-token/agent-action-token.service.ts
  • apps/api/src/app/agents/conversation-runtime/action-token/card-callback-button.walker.ts
  • apps/api/src/app/agents/conversation-runtime/egress/outbound.gateway.action-token.spec.ts
  • apps/api/src/app/agents/conversation-runtime/egress/outbound.gateway.ts
  • apps/api/src/app/agents/conversation-runtime/ingress/chat-instance.registry.ts
  • apps/dashboard/src/pages/usecase-select-page.tsx
  • apps/dashboard/src/utils/telemetry.ts
💤 Files with no reviewable changes (1)
  • apps/api/src/app/agents/conversation-runtime/ingress/chat-instance.registry.ts
✅ Files skipped from review due to trivial changes (1)
  • apps/dashboard/src/utils/telemetry.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/api/src/app/agents/conversation-runtime/action-token/card-callback-button.walker.ts
  • apps/api/src/app/agents/conversation-runtime/egress/outbound.gateway.ts
  • apps/api/src/app/agents/conversation-runtime/egress/outbound.gateway.action-token.spec.ts
  • apps/api/src/app/agents/conversation-runtime/action-token/agent-action-token.service.spec.ts

Comment on lines +131 to +140
await forEachCallbackButton(clone, async (button) => {
if (!callbackPayloadNeedsTokenization(button.id)) {
return;
}

const token = await this.mintActionToken({
...binding,
id: button.id,
value: typeof button.value === 'string' ? button.value : undefined,
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Tokenization check ignores oversized button.value.

This gate only evaluates button.id, but this method also persists button.value into the token claims and strips it from the delivered button once tokenized. That means a button with a short id and a long value can still exceed the platform callback-data budget and slip through un-tokenized, which defeats the Telegram fix on that path.

Suggested direction
-      if (!callbackPayloadNeedsTokenization(button.id)) {
+      if (!callbackPayloadNeedsTokenization(button)) {
         return;
       }
// In apps/api/src/app/agents/conversation-runtime/action-token/card-callback-button.walker.ts,
// make the predicate evaluate the encoded callback payload from both fields.
type CallbackPayloadButton = Pick<ButtonElement, 'id' | 'value'>;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/api/src/app/agents/conversation-runtime/action-token/agent-action-token.service.ts`
around lines 131 - 140, The tokenization predicate
callbackPayloadNeedsTokenization currently only checks button.id, so update the
check used by forEachCallbackButton (and the predicate in
card-callback-button.walker.ts) to evaluate the combined/encoded callback
payload derived from both button.id and button.value (e.g., treat the button as
CallbackPayloadButton with 'id' and 'value'), and return true when the
serialized/encoded payload would exceed the platform callback-data budget; then
use that predicate before calling this.mintActionToken so long button.value is
tokenized/removed when required (refer to forEachCallbackButton,
callbackPayloadNeedsTokenization, and mintActionToken).

@scopsy scopsy merged commit 0b254c7 into next Jun 3, 2026
40 checks passed
@scopsy scopsy deleted the cursor/platform-action-token-indirection branch June 3, 2026 12:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants