Skip to content

docs(plugin-tinyplace): cursor⇄openhuman bidirectional bridge spike - #252

Merged
senamakel merged 2 commits into
tinyhumansai:mainfrom
CodeGhost21:feat/cursor-bidirectional-bridge
Jul 14, 2026
Merged

docs(plugin-tinyplace): cursor⇄openhuman bidirectional bridge spike#252
senamakel merged 2 commits into
tinyhumansai:mainfrom
CodeGhost21:feat/cursor-bidirectional-bridge

Conversation

@CodeGhost21

@CodeGhost21 CodeGhost21 commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Captures the throwaway prototype that proves a Cursor IDE agent can be driven into a live, two-way tiny.place conversation with OpenHuman over the Signal-encrypted relay — and, more importantly, records the findings that inform the real adapters/cursor.mjs. Lands under sdk/plugin-tinyplace/prototype/cursor-bridge/ (scripts + a findings README). Nothing here ships.

  • Forward (Cursor → OpenHuman): beforeSubmitPrompt / afterAgentResponse hooks send each turn as a SessionEnvelopeV1 DM; OpenHuman renders it as a cursor runtime session.
  • Reverse (OpenHuman → Cursor): a background daemon polls the bridge inbox and pastes OpenHuman DMs into the live Cursor GUI (clipboard + System Events), so they appear in the chat and get answered — answer flows back to OpenHuman. Full loop, demoed live against staging.

Problem

The recognition slice + adapter work needed to know what's actually achievable for a Cursor runtime end-to-end. Rather than guess, this spike built the whole loop against staging and wrote down what works and what doesn't.

Findings (the point of the PR)

  • stop → followup_message is the only in-conversation injection channel Cursor exposes (hooks aren't observe-only). It's turn-triggered, not push.
  • CGEventPostToPid can't reach a backgrounded Electron window — Chromium drops synthetic key events unless it's the key window (postkeys.swift kept as evidence). So there is no zero-focus-steal instant push; instant delivery needs a brief foreground flash (we capture+restore the prior app), or inject only while Cursor is already frontmost.
  • AX value-set doesn't register in React — real key events (foreground) are required.
  • Concurrent FileSessionStore access corrupts the Double Ratchet → HTTP 400 on send; fixed with a cross-process mkdir lock (withLock).
  • SDK ≥ 2.0.2 is mandatory (base58 bundle routing) — reaffirms that the OpenHuman "slash-free identity" idea was the wrong layer (closed openhuman#4779).

Solution

Six files under prototype/cursor-bridge/ (common.mjs, hook.mjs, daemon.mjs, setup.mjs, postkeys.swift, README.md). SDK dist path is resolved relative to the file (repo-portable); OpenHuman address + API come from env. README documents setup and the security caveats.

Impact

  • Docs/prototype only — no package code, no build/test surface changed.
  • ⚠️ Security (called out in the README): the demo hook config auto-approves shell/MCP/file gates so turns don't stall, meaning an injected (untrusted) OpenHuman message can run shell — remove those hooks for manual approval. Provisions a throwaway wallet under ~/.tinyplace-cursorbridge.

Related

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added a Cursor ⇄ OpenHuman prototype bridge that exchanges prompts/responses via encrypted tiny.place messages.
    • Added reverse delivery to auto-insert OpenHuman replies into Cursor.
    • Added macOS automation to paste bridged messages into the active Cursor workflow.
    • Added setup tooling to provision identity, encryption, and initialize contact flow.
  • Documentation
    • Added a complete prototype guide with architecture, setup steps, security caveats, and known limitations/behavior notes (including approval routing and reliability findings).

Capture the throwaway prototype that proves a Cursor IDE agent can be driven
into a live two-way tiny.place conversation with OpenHuman over the Signal
relay, plus the findings that inform the real adapter:

- forward (Cursor→OpenHuman) via beforeSubmitPrompt/afterAgentResponse hooks →
  SessionEnvelopeV1 DMs rendered as a `cursor` runtime.
- reverse (OpenHuman→Cursor) via a daemon that pastes inbox DMs into the live
  GUI (clipboard + System Events), with echo-suppression and focus-restore.
- findings: `stop → followup_message` is the only in-conversation injection
  channel; CGEventPostToPid can't reach a backgrounded Electron window; AX
  value-set doesn't register in React; concurrent FileSessionStore access
  corrupts the ratchet (→ HTTP 400), fixed with a cross-process lock; SDK ≥2.0.2
  required for base58 bundle routing.

Prototype only (README flags the security caveats + auto-approve tradeoff);
nothing here ships. Complements the cursor adapter hardening (tinyhumansai#251).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 13, 2026

Copy link
Copy Markdown

@CodeGhost21 is attempting to deploy a commit to the Vezures Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This prototype adds a bidirectional Cursor–OpenHuman bridge. Cursor hooks forward prompts, responses, and approval requests through encrypted TinyPlace messaging, while a daemon polls inbound messages and injects them into Cursor using macOS automation. Shared state, locking, setup, echo suppression, and operational findings are documented.

Changes

Cursor OpenHuman Bridge

Layer / File(s) Summary
Bridge runtime and provisioning
sdk/plugin-tinyplace/prototype/cursor-bridge/common.mjs, sdk/plugin-tinyplace/prototype/cursor-bridge/setup.mjs
Adds encrypted client construction, persistent identity, session locking, retry handling, message envelopes, echo suppression, and contact provisioning.
Cursor hook forwarding and approvals
sdk/plugin-tinyplace/prototype/cursor-bridge/hook.mjs
Forwards Cursor turns and approval requests to OpenHuman, waits for allow/deny decisions, suppresses injected-message echoes, and leaves reverse stop events to the daemon.
Reverse message delivery
sdk/plugin-tinyplace/prototype/cursor-bridge/daemon.mjs
Pauses during pending approvals, polls inbound messages under a lock, and pastes/submits normalized text into Cursor through clipboard and AppleScript automation.
Prototype operation and automation experiments
sdk/plugin-tinyplace/prototype/cursor-bridge/README.md, sdk/plugin-tinyplace/prototype/cursor-bridge/postkeys.swift
Documents setup, architecture, observed automation and session behavior, recovery steps, security caveats, and targeted key injection experiments.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Cursor
  participant hook.mjs
  participant OpenHuman
  participant daemon.mjs
  Cursor->>hook.mjs: submit prompt, response, or approval request
  hook.mjs->>OpenHuman: send encrypted envelope
  OpenHuman-->>hook.mjs: approval decision when requested
  OpenHuman-->>daemon.mjs: inbound message
  daemon.mjs->>Cursor: paste and submit message
Loading

Possibly related PRs

Poem

A rabbit hops through Cursor’s gate,
Sending encrypted turns in state.
OpenHuman whispers, daemon replies,
Clipboard carrots catch the eyes.
Locks keep ratchets snug and bright—
Bridge the burrow day and night! 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly reflects the prototype docs change for a Cursor↔OpenHuman bidirectional bridge.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.

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

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

Copy link
Copy Markdown

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: 1d1241244e

ℹ️ 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 on lines +80 to +84
const msgs = await withLock(() =>
agent.readMessages(client, signer, { limit: 20 }),
);
const texts = msgs
.filter((m) => m.from === OPENHUMAN)

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 Fail fast when OPENHUMAN_ADDR is missing

If the daemon is started without the same OPENHUMAN_ADDR used during setup, it still calls readMessages before filtering by OPENHUMAN. The SDK's readMessages decrypts and acknowledges the inbox, so a missing or mistyped env var makes this poll permanently drain all queued DMs and then drop them because none match the empty/bad address; the daemon should validate OPENHUMAN before any destructive inbox read.

Useful? React with 👍 / 👎.

Comment on lines +76 to +77
// Give up waiting — proceed unlocked rather than drop the message.
return await fn();

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 Keep Signal operations locked until acquired

When another hook/daemon process holds the lock for more than the default ~8 seconds, withLock runs the SDK operation without the mutex. The surrounding code notes that concurrent FileSessionStore access corrupts the Double Ratchet state, so any slow staging request or key fetch can reintroduce the exact 400-producing corruption this lock is meant to prevent; this should keep waiting or fail the operation rather than proceed unlocked.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 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 `@sdk/plugin-tinyplace/prototype/cursor-bridge/common.mjs`:
- Around line 50-72: Update withLock so stale-lock recovery cannot remove a lock
held by a live operation: record the holder PID and verify that process is no
longer alive before deleting an expired LOCKDIR, or otherwise refresh the lock
timestamp throughout long-running fn executions. Preserve stale-lock cleanup for
crashed or abandoned holders while preventing concurrent access during
legitimate operations.
- Around line 139-162: Protect the read-modify-write operations in recordPush
and consumePush with the existing withLock mechanism or a dedicated lock shared
by both processes. Ensure each function reads, updates, and writes pushed.json
while holding the same lock, preserving the current TTL filtering, matching,
removal, and return behavior.
- Around line 166-188: The envelope() function emits the v1 session shape
without the required bucket field. Add the appropriate bucket value to the scope
object in envelope(), preserving the existing session identifiers and v1
envelope contract.

In `@sdk/plugin-tinyplace/prototype/cursor-bridge/README.md`:
- Line 23: Update the architecture diagram fenced code block in the README to
specify the text language using a text fence, while preserving the diagram
content unchanged.
- Around line 110-115: Update the README concurrency statement to avoid claiming
that withLock fully prevents session corruption, or revise withLock in
common.mjs so timeout handling never invokes the SDK operation without the lock.
Ensure sustained contention fails or retries safely rather than allowing
concurrent FileSessionStore mutation.
- Around line 86-91: Align the README description with the behavior implemented
by hook.mjs: either update the stop/subagentStop handlers to emit the documented
followup_message result, or revise this section to label the mechanism as
historical rather than an available zero-dependency fallback. Keep the claim
that it is currently usable only if hook.mjs actually supports it.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: dc7ecff1-c952-4b98-8981-38712df6a41d

📥 Commits

Reviewing files that changed from the base of the PR and between bae92dc and 1d12412.

📒 Files selected for processing (6)
  • sdk/plugin-tinyplace/prototype/cursor-bridge/README.md
  • sdk/plugin-tinyplace/prototype/cursor-bridge/common.mjs
  • sdk/plugin-tinyplace/prototype/cursor-bridge/daemon.mjs
  • sdk/plugin-tinyplace/prototype/cursor-bridge/hook.mjs
  • sdk/plugin-tinyplace/prototype/cursor-bridge/postkeys.swift
  • sdk/plugin-tinyplace/prototype/cursor-bridge/setup.mjs

Comment on lines +50 to +72
const LOCK_STALE_MS = 15000;

export async function withLock(fn, { retries = 200, delayMs = 40 } = {}) {
mkdirSync(HOME, { recursive: true });
for (let i = 0; i < retries; i++) {
try {
mkdirSync(LOCKDIR); // atomic: throws EEXIST if held
try {
return await fn();
} finally {
try {
rmdirSync(LOCKDIR);
} catch {}
}
} catch (e) {
if (e.code !== "EEXIST") throw e;
// Break a stale lock left by a crashed process.
try {
if (Date.now() - statSync(LOCKDIR).mtimeMs > LOCK_STALE_MS) {
rmdirSync(LOCKDIR);
continue;
}
} catch {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Stale-lock timeout can break a lock held by a live operation.

mkdir sets the lock dir's mtime once; it isn't refreshed while fn() runs. A legitimate SDK send/read that exceeds LOCK_STALE_MS (15s) — plausible on a slow relay round-trip — will be seen as stale by another process, which then rmdirSynces it and proceeds concurrently. That is exactly the concurrent store read/write this mutex exists to prevent (the documented → HTTP 400 ratchet corruption). Consider recording the holder PID and checking liveness, or refreshing mtime during long ops.

🤖 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 `@sdk/plugin-tinyplace/prototype/cursor-bridge/common.mjs` around lines 50 -
72, Update withLock so stale-lock recovery cannot remove a lock held by a live
operation: record the holder PID and verify that process is no longer alive
before deleting an expired LOCKDIR, or otherwise refresh the lock timestamp
throughout long-running fn executions. Preserve stale-lock cleanup for crashed
or abandoned holders while preventing concurrent access during legitimate
operations.

Comment on lines +139 to +162
export function recordPush(text) {
const arr = readPushed()
.filter((e) => Date.now() - e.ts < PUSH_TTL_MS)
.concat([{ text: String(text).trim(), ts: Date.now() }])
.slice(-20);
try {
writeFileSync(PUSHED, JSON.stringify(arr), { mode: 0o600 });
} catch {}
}

// Returns true (and removes the record) if `text` matches a recent push.
export function consumePush(text) {
const arr = readPushed();
const t = String(text).trim();
const i = arr.findIndex(
(e) => e.text === t && Date.now() - e.ts < PUSH_TTL_MS,
);
if (i === -1) return false;
arr.splice(i, 1);
try {
writeFileSync(PUSHED, JSON.stringify(arr), { mode: 0o600 });
} catch {}
return true;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Echo-suppression file is read-modify-written without the mutex.

recordPush (daemon) and consumePush (hook) both do unlocked read-modify-write on pushed.json from separate processes. A concurrent overlap can drop a just-recorded push record, so consumePush misses it and the pasted OpenHuman message gets echoed back → feedback loop. Consider serializing these under withLock (or a dedicated lock) as well.

🤖 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 `@sdk/plugin-tinyplace/prototype/cursor-bridge/common.mjs` around lines 139 -
162, Protect the read-modify-write operations in recordPush and consumePush with
the existing withLock mechanism or a dedicated lock shared by both processes.
Ensure each function reads, updates, and writes pushed.json while holding the
same lock, preserving the current TTL filtering, matching, removal, and return
behavior.

Comment on lines +166 to +188
export function envelope({ role, text, convId, cwd }) {
const sid = convId || "cursor-session";
return JSON.stringify({
envelope_version: "tinyplace.harness.session.v1",
version: 1,
scope: {
type: "session",
key: "cursor",
cwd: cwd || "",
wrapper_session_id: sid,
harness_session_id: sid,
},
harness: { provider: "cursor", command: "cursor", argv: [] },
message: {
id: `${sid}-${Date.now()}`,
line: Date.now(),
role,
text,
timestamp: new Date().toISOString(),
},
source: { path: "cursor", record_type: role },
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP '\.bucket\b' sdk/typescript/src | rg -i 'envelope|session' -C2
ast-grep run --pattern 'foldSessionEnvelopes($$$)' --lang typescript sdk/typescript/src || true
rg -n 'bucket' sdk/typescript/src/cli/harness-consumer.ts -C2

Repository: tinyhumansai/tiny.place

Length of output: 1077


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the session envelope type and parser
sed -n '1,220p' sdk/typescript/src/types/harness.ts
printf '\n--- parseSessionEnvelope ---\n'
rg -n "parseSessionEnvelope|SessionEnvelopeV1|SessionEnvelope" sdk/typescript/src -C3

# Inspect the consumer/wrapper paths that dereference bucket
printf '\n--- harness-wrapper bucket references ---\n'
sed -n '960,1010p' sdk/typescript/src/cli/harness-wrapper.ts
printf '\n--- later bucket references ---\n'
sed -n '1568,1590p' sdk/typescript/src/cli/harness-wrapper.ts

# Find where envelope() is used, to understand the runtime path for cursor envelopes
printf '\n--- envelope() call sites ---\n'
rg -n "envelope\(" sdk -C2

Repository: tinyhumansai/tiny.place

Length of output: 49155


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the Cursor bridge envelope builder and its caller
sed -n '1,260p' sdk/plugin-tinyplace/prototype/cursor-bridge/common.mjs
printf '\n--- hook.mjs ---\n'
sed -n '1,180p' sdk/plugin-tinyplace/prototype/cursor-bridge/hook.mjs

# Find any code in the plugin prototype that parses or inspects this envelope shape
printf '\n--- cursor bridge references ---\n'
rg -n "cursor-session|SessionEnvelopeV1|envelope_version|bucket|parseSessionEnvelope|harness_type_for|cursor-bridge" sdk/plugin-tinyplace/prototype -C2

# Check whether any consumer path in the repo dereferences bucket on v1 cursor envelopes specifically
printf '\n--- bucket + cursor references ---\n'
rg -n "bucket\." sdk | rg -i "cursor|SessionEnvelopeV1|parseSessionEnvelope|harness-consumer|harness-wrapper" -C2

Repository: tinyhumansai/tiny.place

Length of output: 16768


envelope() omits required bucket for the v1 session shape.
sdk/plugin-tinyplace/prototype/cursor-bridge/common.mjs:166-188 SessionEnvelopeV1 requires bucket, so this payload doesn’t match the declared contract. Add it here, or stop emitting a v1 envelope shape.

🤖 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 `@sdk/plugin-tinyplace/prototype/cursor-bridge/common.mjs` around lines 166 -
188, The envelope() function emits the v1 session shape without the required
bucket field. Add the appropriate bucket value to the scope object in
envelope(), preserving the existing session identifiers and v1 envelope
contract.


## Architecture

```

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Specify the fenced block language.

Use ```text for the architecture diagram; markdownlint MD040 currently flags this fence.

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 23-23: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 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 `@sdk/plugin-tinyplace/prototype/cursor-bridge/README.md` at line 23, Update
the architecture diagram fenced code block in the README to specify the text
language using a text fence, while preserving the diagram content unchanged.

Source: Linters/SAST tools

Comment thread sdk/plugin-tinyplace/prototype/cursor-bridge/README.md
Comment thread sdk/plugin-tinyplace/prototype/cursor-bridge/README.md
Extend the cursor⇄openhuman bridge prototype with the tool-approval feature and
transport hardening validated against staging:

- hook.mjs: route beforeShellExecution/beforeMCPExecution to OpenHuman as a v2
  approval_request event and block for the allow/deny decision (falls back to
  Cursor's own prompt on timeout); auto-allow file reads.
- common.mjs: v2 approvalEnvelope builder, extractText, an AWAITING flag (daemon
  pauses inbox draining while an approval is pending), and sendWithRetry which
  self-heals a desynced session (reset + retry on a 400/encrypt error).
- daemon.mjs: pause while an approval is pending so the hook owns the decision DM.
- README: approval-routing section + findings on the two-store ratchet fragility
  (receiving side can't retry a silent drop) and deriving the resolved-card state.

Prototype only; kept repo-portable (relative SDK dist, OPENHUMAN_ADDR from env).
Pairs with the OpenHuman Allow/Deny card PR (tinyhumansai/openhuman#4837).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
sdk/plugin-tinyplace/prototype/cursor-bridge/common.mjs (1)

147-171: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Broad error-matching regex risks unwarranted session resets.

/encrypted ciphertext|HTTP 400|No session|ratchet|decrypt/i matches on bare substrings like "HTTP 400" or "decrypt", which can appear in unrelated errors (rate limiting, malformed payloads, other 400s). Any such error triggers store.removeSession(to), forcing an unnecessary re-handshake and masking the real cause of the failure — compounding the "two-store ratchet fragility" already called out for this prototype.

♻️ Narrow the match to session/ratchet-specific signals
-    if (
-      !/encrypted ciphertext|HTTP 400|No session|ratchet|decrypt/i.test(
-        String(e?.message),
-      )
-    ) {
+    if (
+      !/body must be encrypted ciphertext|No session|ratchet desync|failed to decrypt/i.test(
+        String(e?.message),
+      )
+    ) {
       throw e;
     }
🤖 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 `@sdk/plugin-tinyplace/prototype/cursor-bridge/common.mjs` around lines 147 -
171, Update the error filter in sendWithRetry to match only explicit session,
ratchet, or encrypted-ciphertext failure signals, avoiding broad substrings such
as bare “HTTP 400” or “decrypt” that can represent unrelated errors. Preserve
rethrowing non-matching errors and the existing session reset and single retry
behavior for matched failures.
🤖 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 `@sdk/plugin-tinyplace/prototype/cursor-bridge/common.mjs`:
- Around line 240-279: Update approvalEnvelope to include the required top-level
bucket field in the SessionEnvelopeV2 JSON payload, using the same bucket value
expected by the existing v2 session envelopes. Keep the current scope, event,
and source structure unchanged.

In `@sdk/plugin-tinyplace/prototype/cursor-bridge/hook.mjs`:
- Around line 29-32: Clamp the BRIDGE_APPROVAL_WAIT_MS override in the
APPROVAL_WAIT_MS definition so its effective value remains safely below the
daemon’s 360,000 ms stale cutoff. Preserve the existing 270,000 ms default and
ensure oversized environment values cannot let routeApproval() poll past the
cutoff.
- Around line 64-113: Update routeApproval to poll only messages correlated with
its approval request, using requestId/call_id metadata before parsing
allow/deny; leave unrelated OpenHuman messages available for Cursor or other
handlers. Ensure overlapping approvals cannot consume each other’s decisions
while preserving the existing timeout and fallback behavior.

---

Nitpick comments:
In `@sdk/plugin-tinyplace/prototype/cursor-bridge/common.mjs`:
- Around line 147-171: Update the error filter in sendWithRetry to match only
explicit session, ratchet, or encrypted-ciphertext failure signals, avoiding
broad substrings such as bare “HTTP 400” or “decrypt” that can represent
unrelated errors. Preserve rethrowing non-matching errors and the existing
session reset and single retry behavior for matched failures.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 84a93555-0214-405f-8c68-f1d3b99bf6bf

📥 Commits

Reviewing files that changed from the base of the PR and between 1d12412 and 091763d.

📒 Files selected for processing (4)
  • sdk/plugin-tinyplace/prototype/cursor-bridge/README.md
  • sdk/plugin-tinyplace/prototype/cursor-bridge/common.mjs
  • sdk/plugin-tinyplace/prototype/cursor-bridge/daemon.mjs
  • sdk/plugin-tinyplace/prototype/cursor-bridge/hook.mjs
🚧 Files skipped from review as they are similar to previous changes (2)
  • sdk/plugin-tinyplace/prototype/cursor-bridge/daemon.mjs
  • sdk/plugin-tinyplace/prototype/cursor-bridge/README.md

Comment on lines +240 to +279

// SessionEnvelopeV2 with a typed `approval_request` event. OpenHuman's orchestration
// ingest (classify_v2) maps this to eventKind "approval_request" (display → body,
// tool_name, call_id) and the SessionTranscript renders an Allow/Deny card. Uses the
// SAME wrapper_session_id as the chat turns so it threads into the same session; the
// user's button reply comes back as a plain "allow"/"deny" DM.
export function approvalEnvelope({
toolName,
display,
convId,
cwd,
requestId,
}) {
const sid = convId || "cursor-session";
return JSON.stringify({
envelope_version: "tinyplace.harness.session.v2",
version: 2,
scope: {
type: "session",
key: "cursor",
cwd: cwd || "",
wrapper_session_id: sid,
harness_session_id: sid,
},
harness: { provider: "cursor", command: "cursor", argv: [] },
event: {
id: requestId,
seq: Date.now(),
ts: new Date().toISOString(),
role: "agent",
kind: "approval_request",
payload: {
tool_name: toolName || "shell",
display: display || "",
call_id: requestId,
},
},
source: { path: "cursor", record_type: "approval_request" },
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check SessionEnvelopeV2 scope requirements
rg -n "SessionEnvelopeV2" sdk/typescript/src -C6
rg -n "interface.*Scope|type.*Scope" sdk/typescript/src/types/harness.ts -C10

Repository: tinyhumansai/tiny.place

Length of output: 11693


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the approval envelope implementation and nearby helpers.
FILE="sdk/plugin-tinyplace/prototype/cursor-bridge/common.mjs"
wc -l "$FILE"
sed -n '200,320p' "$FILE"

# Inspect the v2 envelope type and any builders that populate bucket.
sed -n '190,240p' sdk/typescript/src/types/harness.ts
sed -n '1,140p' sdk/typescript/src/cli/harness-envelope.ts

Repository: tinyhumansai/tiny.place

Length of output: 7492


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find where approvalEnvelope is used and whether any v2 envelope JSON is produced without bucket elsewhere.
rg -n "approvalEnvelope\(|tinyplace\.harness\.session\.v2|record_type: \"approval_request\"|approval_request" sdk/plugin-tinyplace -C3

Repository: tinyhumansai/tiny.place

Length of output: 9245


Add the required top-level bucket to the v2 approval envelope.
SessionEnvelopeV2 includes bucket at the top level, and approvalEnvelope() omits it entirely. That makes this payload fail the v2 schema/ingest contract on the approval path.

🤖 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 `@sdk/plugin-tinyplace/prototype/cursor-bridge/common.mjs` around lines 240 -
279, Update approvalEnvelope to include the required top-level bucket field in
the SessionEnvelopeV2 JSON payload, using the same bucket value expected by the
existing v2 session envelopes. Keep the current scope, event, and source
structure unchanged.

Comment on lines +29 to +32
// How long the approval hook waits for an OpenHuman decision before falling back
// to Cursor's own GUI prompt. MUST be < the hook's timeout in ~/.cursor/hooks.json
// (300s there → 270s here leaves a buffer so we return before Cursor kills us).
const APPROVAL_WAIT_MS = Number(process.env.BRIDGE_APPROVAL_WAIT_MS) || 270_000;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the daemon's staleness cutoff constant hasn't drifted from what's assumed here
rg -n "360_000|approvalPending" sdk/plugin-tinyplace/prototype/cursor-bridge/daemon.mjs -C4

Repository: tinyhumansai/tiny.place

Length of output: 980


🏁 Script executed:

#!/bin/bash
ast-grep outline sdk/plugin-tinyplace/prototype/cursor-bridge/hook.mjs --view expanded
rg -n "BRIDGE_APPROVAL_WAIT_MS|routeApproval|withLock|AWAITING|approvalPending|session store|inbox" sdk/plugin-tinyplace/prototype/cursor-bridge -C4
sed -n '1,220p' sdk/plugin-tinyplace/prototype/cursor-bridge/hook.mjs

Repository: tinyhumansai/tiny.place

Length of output: 30142


🏁 Script executed:

#!/bin/bash
sed -n '107,170p' sdk/plugin-tinyplace/prototype/cursor-bridge/daemon.mjs

Repository: tinyhumansai/tiny.place

Length of output: 992


Cap BRIDGE_APPROVAL_WAIT_MS below the daemon stale cutoff. If it exceeds 360_000 ms, daemon.mjs can resume draining the inbox while routeApproval() is still polling, and the OpenHuman allow/deny reply may get consumed before the hook sees it.

🔒 Clamp the override to stay safely under the daemon cutoff
-const APPROVAL_WAIT_MS = Number(process.env.BRIDGE_APPROVAL_WAIT_MS) || 270_000;
+// Must stay comfortably below daemon.mjs's approvalPending() stale cutoff (360s),
+// or the daemon will resume draining the inbox while we're still polling it.
+const APPROVAL_WAIT_MS = Math.min(
+  Number(process.env.BRIDGE_APPROVAL_WAIT_MS) || 270_000,
+  300_000,
+);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// How long the approval hook waits for an OpenHuman decision before falling back
// to Cursor's own GUI prompt. MUST be < the hook's timeout in ~/.cursor/hooks.json
// (300s there → 270s here leaves a buffer so we return before Cursor kills us).
const APPROVAL_WAIT_MS = Number(process.env.BRIDGE_APPROVAL_WAIT_MS) || 270_000;
// How long the approval hook waits for an OpenHuman decision before falling back
// to Cursor's own GUI prompt. MUST be < the hook's timeout in ~/.cursor/hooks.json
// (300s there → 270s here leaves a buffer so we return before Cursor kills us).
// Must stay comfortably below daemon.mjs's approvalPending() stale cutoff (360s),
// or the daemon will resume draining the inbox while we're still polling it.
const APPROVAL_WAIT_MS = Math.min(
Number(process.env.BRIDGE_APPROVAL_WAIT_MS) || 270_000,
300_000,
);
🤖 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 `@sdk/plugin-tinyplace/prototype/cursor-bridge/hook.mjs` around lines 29 - 32,
Clamp the BRIDGE_APPROVAL_WAIT_MS override in the APPROVAL_WAIT_MS definition so
its effective value remains safely below the daemon’s 360,000 ms stale cutoff.
Preserve the existing 270,000 ms default and ensure oversized environment values
cannot let routeApproval() poll past the cutoff.

Comment on lines +64 to +113
// Route a Cursor tool-execution gate to OpenHuman: post the request, then block
// (draining the inbox) until the user replies allow/deny there — so approvals
// happen in OpenHuman without switching to Cursor. Returns "allow" | "deny" |
// "ask" (fallback to Cursor's own prompt on timeout/error). While waiting we hold
// the AWAITING flag so the reverse daemon doesn't steal the decision DM.
async function routeApproval(payload, ev, convId, cwd) {
const label = describeCall(payload, ev);
const toolName = ev === "beforeShellExecution" ? "shell" : "mcp";
const requestId = `appr-${Date.now()}`;
const { signer, client, agent, store } = await build();
writeFileSync(AWAITING, String(Date.now()));
try {
// V2 approval_request event → OpenHuman renders an Allow/Deny card; the user's
// button reply comes back as a plain "allow"/"deny" DM we parse below.
await withLock(() =>
sendWithRetry(
{ client, signer, agent, store },
OPENHUMAN,
approvalEnvelope({ toolName, display: label, convId, cwd, requestId }),
),
);
log(`APPROVAL request -> OH (${requestId}): ${String(label).slice(0, 80)}`);
const deadline = Date.now() + APPROVAL_WAIT_MS;
while (Date.now() < deadline) {
const msgs = await withLock(() =>
agent.readMessages(client, signer, { limit: 10 }),
);
for (const m of msgs) {
if (m.from !== OPENHUMAN) continue;
const d = parseDecision(extractText(m.text));
if (d) {
log(`APPROVAL decision=${d} for ${String(label).slice(0, 50)}`);
return d;
}
}
await sleep(2000);
}
log(
`APPROVAL timed out -> ask (Cursor GUI fallback): ${String(label).slice(0, 50)}`,
);
return "ask";
} catch (e) {
log(`APPROVAL error -> ask: ${e.message}`);
return "ask";
} finally {
try {
rmSync(AWAITING);
} catch {}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

set -euo pipefail

printf '\n## Files\n'
git ls-files 'sdk/plugin-tinyplace/prototype/cursor-bridge/*' | sed -n '1,200p'

printf '\n## Outline hook.mjs\n'
ast-grep outline sdk/plugin-tinyplace/prototype/cursor-bridge/hook.mjs --view expanded || true

printf '\n## Outline daemon.mjs\n'
ast-grep outline sdk/plugin-tinyplace/prototype/cursor-bridge/daemon.mjs --view expanded || true

printf '\n## Search for approval/requestId/parseDecision/readMessages\n'
rg -n --hidden --glob 'sdk/plugin-tinyplace/prototype/cursor-bridge/*' \
  'requestId|parseDecision|readMessages|approvalEnvelope|AWAITING|OPENHUMAN|allow|deny' \
  sdk/plugin-tinyplace/prototype/cursor-bridge

Repository: tinyhumansai/tiny.place

Length of output: 6930


🏁 Script executed:

set -euo pipefail

printf '\n## hook.mjs selected lines\n'
nl -ba sdk/plugin-tinyplace/prototype/cursor-bridge/hook.mjs | sed -n '1,220p'

printf '\n## daemon.mjs selected lines\n'
nl -ba sdk/plugin-tinyplace/prototype/cursor-bridge/daemon.mjs | sed -n '1,260p'

Repository: tinyhumansai/tiny.place

Length of output: 229


🏁 Script executed:

set -euo pipefail

printf '\n## Search for message queue semantics and request correlation\n'
rg -n --hidden --glob 'sdk/plugin-tinyplace/prototype/cursor-bridge/*' \
  'readMessages\(|sendWithRetry\(|approval_request|approvalEnvelope|requestId|conversation|convId|parseDecision|extractText|from !== OPENHUMAN|AWAITING' \
  sdk/plugin-tinyplace/prototype/cursor-bridge

Repository: tinyhumansai/tiny.place

Length of output: 5477


🏁 Script executed:

set -euo pipefail

printf '\n## hook.mjs relevant excerpt\n'
sed -n '1,220p' sdk/plugin-tinyplace/prototype/cursor-bridge/hook.mjs | nl -ba | sed -n '1,220p'

printf '\n## daemon.mjs relevant excerpt\n'
sed -n '1,260p' sdk/plugin-tinyplace/prototype/cursor-bridge/daemon.mjs | nl -ba | sed -n '1,260p'

printf '\n## Search related helpers\n'
rg -n --hidden --glob 'sdk/plugin-tinyplace/prototype/cursor-bridge/*' \
  'function parseDecision|const parseDecision|function extractText|const extractText|approvalEnvelope|approval_request|readMessages\(' \
  sdk/plugin-tinyplace/prototype/cursor-bridge

Repository: tinyhumansai/tiny.place

Length of output: 231


🏁 Script executed:

set -euo pipefail

# Show the relevant files and correlations.
for f in sdk/plugin-tinyplace/prototype/cursor-bridge/hook.mjs \
         sdk/plugin-tinyplace/prototype/cursor-bridge/daemon.mjs \
         sdk/plugin-tinyplace/prototype/cursor-bridge/*.mjs
do
  [ -f "$f" ] && printf '\n### %s\n' "$f" && nl -ba "$f" | sed -n '1,260p'
done

printf '\n### Search for requestId and approval routing\n'
rg -n --hidden --glob 'sdk/plugin-tinyplace/prototype/cursor-bridge/*' \
  'requestId|approvalEnvelope|parseDecision|extractText|readMessages\(|withLock\(|AWAITING|OPENHUMAN|beforeShellExecution|beforeMcp' \
  sdk/plugin-tinyplace/prototype/cursor-bridge

Repository: tinyhumansai/tiny.place

Length of output: 260


🏁 Script executed:

set -euo pipefail

printf '\n### Files mentioning readMessages or agent methods\n'
rg -n --hidden --glob '!**/node_modules/**' 'readMessages\s*\(' .

printf '\n### Files mentioning parseDecision/extractText\n'
rg -n --hidden --glob '!**/node_modules/**' 'parseDecision|extractText' .

Repository: tinyhumansai/tiny.place

Length of output: 3303


🏁 Script executed:

set -euo pipefail
python3 - <<'PY'
from pathlib import Path

files = [
    "sdk/plugin-tinyplace/prototype/cursor-bridge/common.mjs",
    "sdk/plugin-tinyplace/prototype/cursor-bridge/daemon.mjs",
    "sdk/typescript/src/agent/messaging.ts",
]
for path in files:
    print(f"\n### {path}")
    lines = Path(path).read_text().splitlines()
    for i, line in enumerate(lines, 1):
        if path.endswith("common.mjs") and 1 <= i <= 320:
            if i in list(range(1, 120)) + list(range(150, 290)):
                print(f"{i:4d}: {line}")
        elif path.endswith("daemon.mjs") and 1 <= i <= 220:
            if i in list(range(1, 170)):
                print(f"{i:4d}: {line}")
        elif path.endswith("messaging.ts") and 1 <= i <= 260:
            if i in list(range(1, 240)):
                print(f"{i:4d}: {line}")
PY

Repository: tinyhumansai/tiny.place

Length of output: 21581


🏁 Script executed:

set -euo pipefail
python3 - <<'PY'
from pathlib import Path

def show(path, ranges):
    print(f"\n### {path}")
    lines = Path(path).read_text().splitlines()
    for start, end in ranges:
        for i in range(start, min(end, len(lines)) + 1):
            print(f"{i:4d}: {lines[i-1]}")
        print()

show("sdk/plugin-tinyplace/prototype/cursor-bridge/common.mjs", [(1,120), (140,290)])
show("sdk/plugin-tinyplace/prototype/cursor-bridge/daemon.mjs", [(1,170)])
show("sdk/typescript/src/agent/messaging.ts", [(1,240)])
PY

Repository: tinyhumansai/tiny.place

Length of output: 22039


🏁 Script executed:

set -euo pipefail
python3 - <<'PY'
from pathlib import Path
path = Path("sdk/plugin-tinyplace/prototype/cursor-bridge/hook.mjs")
lines = path.read_text().splitlines()
for start, end in [(115, 220)]:
    print(f"\n### {path} {start}-{end}")
    for i in range(start, min(end, len(lines)) + 1):
        print(f"{i:4d}: {lines[i-1]}")
PY

Repository: tinyhumansai/tiny.place

Length of output: 3370


🏁 Script executed:

set -euo pipefail
python3 - <<'PY'
from pathlib import Path
for path in [
    "sdk/plugin-tinyplace/prototype/cursor-bridge/README.md",
    "sdk/plugin-tinyplace/prototype/cursor-bridge/common.mjs",
    "sdk/plugin-tinyplace/prototype/cursor-bridge/hook.mjs",
]:
    print(f"\n### {path}")
    text = Path(path).read_text().splitlines()
    for i, line in enumerate(text, 1):
        if any(k in line.lower() for k in ["approval", "parallel", "concurrent", "one at a time", "requestid", "call_id", "allow/deny", "awaiting"]):
            print(f"{i:4d}: {line}")
PY

Repository: tinyhumansai/tiny.place

Length of output: 3936


🏁 Script executed:

set -euo pipefail
python3 - <<'PY'
from pathlib import Path

for path in [
    "sdk/plugin-tinyplace/prototype/cursor-bridge/README.md",
    "sdk/plugin-tinyplace/prototype/cursor-bridge/common.mjs",
    "sdk/plugin-tinyplace/prototype/cursor-bridge/hook.mjs",
]:
    print(f"\n### {path}")
    lines = Path(path).read_text().splitlines()
    for i, line in enumerate(lines, 1):
        low = line.lower()
        if any(k in low for k in ["approval", "parallel", "concurrent", "one at a time", "requestid", "call_id", "allow/deny", "awaiting"]):
            print(f"{i:4d}: {line}")
PY

Repository: tinyhumansai/tiny.place

Length of output: 3936


🏁 Script executed:

set -euo pipefail
python3 - <<'PY'
from pathlib import Path
path = Path("sdk/plugin-tinyplace/prototype/cursor-bridge/README.md")
lines = path.read_text().splitlines()
for start, end in [(1, 90), (120, 170)]:
    print(f"\n### {path} {start}-{end}")
    for i in range(start, min(end, len(lines)) + 1):
        print(f"{i:4d}: {lines[i-1]}")
PY

Repository: tinyhumansai/tiny.place

Length of output: 8732


Approval polling needs inbox filtering and request correlation

  • routeApproval() drains agent.readMessages() while waiting for allow/deny, so any other OpenHuman DM that arrives in that window is acknowledged here and never reaches Cursor.
  • The reply is matched by text only; requestId/call_id is never checked, so overlapping approvals can consume the wrong allow/deny.
🤖 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 `@sdk/plugin-tinyplace/prototype/cursor-bridge/hook.mjs` around lines 64 - 113,
Update routeApproval to poll only messages correlated with its approval request,
using requestId/call_id metadata before parsing allow/deny; leave unrelated
OpenHuman messages available for Cursor or other handlers. Ensure overlapping
approvals cannot consume each other’s decisions while preserving the existing
timeout and fallback behavior.

@senamakel
senamakel merged commit 9d0b968 into tinyhumansai:main Jul 14, 2026
9 of 10 checks passed
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