Skip to content

feat(api-service,human): cap keyless human interactions and push the claim link - #3

Open
evanmarshall wants to merge 1 commit into
nextfrom
ito-mirror/upstream-12544
Open

feat(api-service,human): cap keyless human interactions and push the claim link#3
evanmarshall wants to merge 1 commit into
nextfrom
ito-mirror/upstream-12544

Conversation

@evanmarshall

Copy link
Copy Markdown

Mirror of novuhq#12544 for Ito QA evaluation.

Upstream PR: novuhq#12544
Upstream author: @scopsy
Upstream head commit: 8e1e7e00845abe1aee013b195e2c676aaa6b12c1


What changed? Why was the change needed?

A keyless @novu/human environment (npx @novu/human setup without an account) could create interactions forever. The 24h keyless expiry is only enforced on the Inbox session path, and the existing KEYLESS_DEMO_REPLY_CAP gate lives in the agents inbound-turn handler, which POST /v1/human/interactions never goes through.

This adds a server-side cap to the human interactions path and pushes the connect claim link, mirroring the novu connect keyless CTA:

API

  • CreateInteraction counts every interaction row in a keyless environment (including tell and the setup smoke test). Past KEYLESS_HUMAN_INTERACTION_CAP (default 5) it does not deliver the prompt. It sends a "Sign up & keep this setup" card with the claim link on the channel the prompt would have used, once per environment so a retrying agent does not spam the human, and returns a 429 with a structured body: code: KEYLESS_HUMAN_CAP_REACHED, claimUrl, cap, plus a message that includes the link.
  • ClaimKeylessConnect now also moves HumanInteraction rows, so human wait <id> / human list keep working after the claim.
  • A keyless credential for an already-claimed environment gets a 403 pointing at human setup --secret-key <key> / NOVU_SECRET_KEY, instead of the misleading "Relay agent not found. Run human setup" (which would mint a fresh keyless env).
  • New ConnectClaimTokenService.isEnvironmentClaimed, HumanDeliveryService.deliverContent, buildKeylessHumanSignupCard, and resolveKeylessHumanInteractionCap (read at call time so it is testable and tunable).

CLI (packages/human)

  • Detects the cap 429 (by code, falling back to the message wording) and prints the claim link plus the recovery command. Exit code stays 1 per the existing contract.
  • README and the agent SKILL.md describe the limit and tell agents to stop retrying and surface the link.

Out of scope, as discussed: a human login device-auth flow so the CLI can switch to the claimed environment without pasting a key; a "N free messages left" hint; enforcing the 24h keyless expiry in KeylessStrategy.

Screenshots

N/A (API + CLI).

Expand for optional sections

Related enterprise PR

None.

Special notes for your reviewer

  • Tests: 3 new e2e cases in human-interactions.e2e.ts (cap + card once + no rows persisted, non-keyless untouched, claim moves rows and stale credential gets 403) and 6 new unit cases in create-interaction.usecase.spec.ts. Full human e2e file (24) and the human CLI vitest suite (47) pass.
  • Two pre-existing unit tests in create-interaction.usecase.spec.ts ("keeps the row, stamps successful deliveries…" and "moves the primary subscriber…") fail on next without this change: they assert a top-level subscriberId on the stamp call that the lifecycle stopped writing in refactor(api-service,dal,shared): collapse HumanInteraction onto deliveries fixes NV-8703 novuhq/novu#12478. Not touched here.
  • The cap covers only POST /v1/human/interactions. In-thread ctx.ask-style interactions from framework agents already sit behind the existing keyless reply cap.

🤖 Generated with Claude Code

Greptile Summary

This PR caps keyless human interactions, sends a claim CTA when the allowance is exhausted, migrates human-interaction rows during claim, and teaches the CLI how to present recovery guidance.

  • Adds server-side keyless usage enforcement and structured cap errors.
  • Adds direct CTA-card delivery and claimed-environment detection.
  • Moves human interaction ownership during keyless claims.
  • Updates CLI error handling, tests, README guidance, and the installed agent skill.

Confidence Score: 2/5

The PR should not merge until the keyless cap and CTA deduplication are made concurrency-safe and claimed environments can be recognized after transient claim-token state disappears.

Concurrent interaction requests can exceed the configured allowance and duplicate the supposedly one-time CTA, while cache expiry causes stale claimed credentials to revert to the misleading setup path.

Files Needing Attention: apps/api/src/app/human/usecases/create-interaction/create-interaction.usecase.ts; apps/api/src/app/connect/services/connect-claim-token.service.ts

Important Files Changed

Filename Overview
apps/api/src/app/human/usecases/create-interaction/create-interaction.usecase.ts Adds the cap and CTA flow, but its count-before-create enforcement and post-delivery deduplication are vulnerable to concurrent requests.
apps/api/src/app/connect/services/connect-claim-token.service.ts Adds best-effort claimed-state detection whose cache-only state expires and therefore cannot reliably identify claimed environments.
apps/api/src/app/connect/usecases/claim-keyless-connect/claim-keyless-connect.usecase.ts Correctly includes HumanInteraction rows in the transactional ownership migration, but does not persist durable claimed state for the source environment.
packages/human/src/commands/interact.ts Recognizes the structured cap response, preserves compatibility with older message-based responses, and prints recovery guidance.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[POST human interaction] --> B{Keyless organization?}
  B -->|No| G[Resolve and deliver interaction]
  B -->|Yes| C{Claim cache says claimed?}
  C -->|Yes| D[Return 403 re-auth guidance]
  C -->|No| E[Count environment interactions]
  E -->|Below cap| G
  E -->|At cap| F[Issue claim link and send CTA]
  F --> H[Return structured 429]
  I[Connect claim] --> J[Move agents, channels, and human interactions]
  J --> K[Consume claim token]
Loading

Fix all with Greploop Fix All in Cursor

Prompt To Fix All With AI
### Issue 1
apps/api/src/app/human/usecases/create-interaction/create-interaction.usecase.ts:128-131
**Interaction cap check races**

If multiple interaction requests arrive concurrently when a keyless environment is just below its cap, each request can observe `used < cap` before separately creating and delivering its interaction, causing the configured allowance to be exceeded.

### Issue 2
apps/api/src/app/human/usecases/create-interaction/create-interaction.usecase.ts:191-199
**CTA reservation happens too late**

If capped requests execute concurrently, each can read the CTA marker as absent and deliver the card before either calls `tryMarkSignupCtaPosted`; only one request wins the marker afterward, but the human has already received duplicate signup cards.

### Issue 3
apps/api/src/app/connect/services/connect-claim-token.service.ts:98-103
**Claimed state expires from cache**

When the seven-day environment-token mapping or used-token marker expires, this lookup reports the claimed source environment as unclaimed. A stale keyless credential then searches the emptied source environment and receives the misleading `Run human setup` error instead of the new re-authentication guidance.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "feat(api-service,human): cap keyless hum..." | Re-trigger Greptile

Greptile also left 3 inline comments on this PR.

…claim link

A keyless `@novu/human` environment could create interactions forever: the
24h keyless expiry is only enforced on the Inbox session path, and the
existing keyless reply cap lives in the agents inbound-turn handler, which
`POST /v1/human/interactions` never goes through.

After `KEYLESS_HUMAN_INTERACTION_CAP` interactions (default 5, every kind
counts) the API no longer delivers the prompt. It sends a "Sign up & keep
this setup" card with the connect claim link on the channel the prompt would
have used (once per environment) and returns a 429 whose body carries
`code: KEYLESS_HUMAN_CAP_REACHED`, `claimUrl`, and `cap`, mirroring the
`novu connect` keyless CTA.

Claiming now also moves HumanInteraction rows, and a keyless credential for
an already-claimed environment gets a 403 pointing at
`human setup --secret-key` instead of a misleading "run setup".

The CLI detects the structured 429, prints the claim link and the recovery
command, and exits 1 per the existing contract. README and the agent skill
doc describe the limit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown

Hey there and thank you for opening this pull request! 👋

We require pull request titles to follow specific formatting rules and it looks like your proposed title needs to be adjusted.

Your PR title is: feat(api-service,human): cap keyless human interactions and push the claim link

Requirements:

  1. Follow the Conventional Commits specification
  2. As a team member, include Linear ticket ID at the end: fixes TICKET-ID or include it in your branch name

Expected format: feat(scope): Add fancy new feature fixes NOV-123

Details:

PR title must end with 'fixes TICKET-ID' (e.g., 'fixes NOV-123') or include ticket ID in branch name

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.

2 participants