Skip to content

feat(cli): announce co-located agent to OpenHuman before contact request#242

Open
oxoxDev wants to merge 1 commit into
tinyhumansai:mainfrom
oxoxDev:feat/cli-openhuman-handshake
Open

feat(cli): announce co-located agent to OpenHuman before contact request#242
oxoxDev wants to merge 1 commit into
tinyhumansai:mainfrom
oxoxDev:feat/cli-openhuman-handshake

Conversation

@oxoxDev

@oxoxDev oxoxDev commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Summary

When a wrapped agent (Codex/Claude) connects to its OpenHuman owner, drop a same-machine handshake file so the owner can auto-accept the contact request without a manual click.

Problem

A wrapped agent's first DM to its OpenHuman owner is gated behind a tiny.place contact request the owner must accept. A fresh CLI identity therefore sits pending — its session stream is dropped by the owner's sender gate — until a human manually accepts it. A tiny.place contact request carries no owner declaration on the wire, so the owner cannot recognise a legitimately co-located CLI from the request alone.

Solution

Before sending the contact request (in SessionEnvelopePublisher.ensureContactNow), write { agentId, owner, cwd, provider, ts } to ~/.openhuman/local-agents.json. A same-machine OpenHuman reads this file and auto-accepts a pending request whose requester id + declared owner match a fresh entry — same-user filesystem access is the trust proof.

  • The write is best-effort: any filesystem error is swallowed, so the contact request still fires and a human can accept manually.
  • mergeLocalAgentEntry (pure, unit-tested) replaces this agent's own prior row and prunes expired (>1h TTL) / undated rows, mirroring the OpenHuman-side reader.

The OpenHuman side that consumes this file is a companion change in tinyhumansai/openhuman.

Testing

  • New unit tests for mergeLocalAgentEntry (add / self-replace / keep-others / prune-stale-and-undated): 4 passing.
  • Full SDK suite green (358 passed).
  • Verified live: launching tinyplace codex writes a valid fresh entry to ~/.openhuman/local-agents.json.

Notes

This branches off main, which has a single contact-request path (ensureContactNow). The feat/tui-session-history branch adds a second path (InboundMessageReceiver.ensureOwnerContact); a follow-up will add the same announce there once that lands.

Summary by CodeRabbit

  • New Features

    • Added automatic same-device pairing support for local agents.
    • Local agent pairing information is maintained automatically and refreshed as needed.
    • Stale or invalid pairing records are cleaned up automatically.
  • Bug Fixes

    • Improved contact request forwarding so eligible local pairing requests can be accepted automatically without interrupting messaging.

A wrapped agent's first DM to its OpenHuman owner is gated behind a
contact request the owner must accept; a fresh CLI identity therefore
sits pending until a human clicks accept, and its session stream is
dropped meanwhile. A tiny.place contact request carries no owner
declaration on the wire, so the owner cannot recognise a co-located CLI
from the request alone.

Before sending the request, write { agentId, owner, cwd, provider, ts }
to ~/.openhuman/local-agents.json. A same-machine OpenHuman reads this
file and auto-accepts a pending request whose id + owner match a fresh
entry, so a local agent connects with no manual step. The write is
best-effort (any fs error is swallowed — the request still fires), and
mergeLocalAgentEntry replaces our own prior row while pruning expired
(>1h) and undated entries, mirroring the OpenHuman-side TTL.
@vercel

vercel Bot commented Jul 10, 2026

Copy link
Copy Markdown

@oxoxDev 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 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The CLI now maintains a TTL-pruned local agent registry and announces the current agent before submitting an unaccepted contact request. Tests cover insertion, replacement, retention, and pruning behavior.

Changes

Local handshake

Layer / File(s) Summary
Registry contract and pruning
sdk/typescript/src/cli/harness-wrapper.ts, sdk/typescript/tests/local-agents-handshake.test.ts
Adds typed registry entries, TTL-based merging, best-effort filesystem handling, and tests for insertion, replacement, retention, and stale-entry pruning.
Contact request announcement
sdk/typescript/src/cli/harness-wrapper.ts
Records signer and contact metadata before requesting an unaccepted contact, while swallowing filesystem errors.

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

Sequence Diagram(s)

sequenceDiagram
  participant ensureContactNow
  participant announceLocalAgent
  participant LocalRegistry
  participant contacts.request
  ensureContactNow->>announceLocalAgent: announce agent and owner metadata
  announceLocalAgent->>LocalRegistry: read, merge, prune, and write entry
  announceLocalAgent-->>ensureContactNow: finish best-effort update
  ensureContactNow->>contacts.request: submit contact request
Loading

Poem

A rabbit hops where local agents meet,
Pruning stale rows with paws so neat.
A signer’s name, an owner too,
Gets tucked away before requests go through.
“Handshake complete!” the burrow sings.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: announcing a co-located agent before sending the contact request.
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.
✨ Finishing Touches
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch feat/cli-openhuman-handshake

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: cb3a7bc001

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

// We're about to send a contact request that OpenHuman must accept before any
// DM lands. Drop a co-location entry naming this owner FIRST, so the same-
// machine OpenHuman can auto-accept the pending request without a manual click.
announceLocalAgent(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Wait for asynchronous auto-accept before failing

When OpenHuman is running in a separate process, it can only accept after it observes both this file and the newly created pending request. This code writes the file and then the existing flow immediately does a single status read; if OpenHuman accepts even a moment later, after.status is still pending, and the rejected contactPromise is then reused for all queued envelopes, so the whole fresh session fails despite the local handshake succeeding shortly afterward. Please poll for a bounded interval after contacts.request or clear/retry the cached contact promise when the only failure is pending approval.

Useful? React with 👍 / 👎.

Comment on lines +616 to +617
mkdirSync(dir, { recursive: true });
writeFileSync(path, `${JSON.stringify(next, undefined, 2)}\n`, "utf8");

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 Create the handshake file with private modes

Because OpenHuman treats write access to ~/.openhuman/local-agents.json as the trust proof, relying on Node's default directory/file modes (0777/0666 masked only by the user's umask) weakens that boundary on multi-user machines with permissive or group-writable umasks. In that environment another local account in the group can write a matching entry and have OpenHuman auto-accept its contact request; this should explicitly create/verify the directory as private and write the file with 0600-style permissions.

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

🧹 Nitpick comments (1)
sdk/typescript/src/cli/harness-wrapper.ts (1)

615-617: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Write atomically to avoid the OpenHuman reader seeing a partial file.

writeFileSync is not atomic. Since OpenHuman reads this file concurrently to auto-accept, a mid-write read yields truncated JSON and silently misses an otherwise-valid handshake. Write to a temp file then renameSync for an atomic swap (add renameSync to the node:fs import).

♻️ Proposed atomic write
     mkdirSync(dir, { recursive: true });
-    writeFileSync(path, `${JSON.stringify(next, undefined, 2)}\n`, "utf8");
+    const tmp = `${path}.${process.pid}.tmp`;
+    writeFileSync(tmp, `${JSON.stringify(next, undefined, 2)}\n`, "utf8");
+    renameSync(tmp, 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/typescript/src/cli/harness-wrapper.ts` around lines 615 - 617, Update the
local-agent persistence logic around mergeLocalAgentEntry to write atomically:
import renameSync from node:fs, write the JSON contents to a temporary file in
the same directory, then renameSync it to the target path after ensuring the
directory exists. Avoid writing directly with writeFileSync to the final path.
🤖 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/typescript/src/cli/harness-wrapper.ts`:
- Around line 611-621: Update announceLocalAgent to create the handshake
directory with mode 0o700 and the local-agents.json file with mode 0o600, then
explicitly chmod both existing paths to enforce owner-only permissions
regardless of umask. Preserve the existing merge/write behavior and keep
permission failures handled by the current catch block.

---

Nitpick comments:
In `@sdk/typescript/src/cli/harness-wrapper.ts`:
- Around line 615-617: Update the local-agent persistence logic around
mergeLocalAgentEntry to write atomically: import renameSync from node:fs, write
the JSON contents to a temporary file in the same directory, then renameSync it
to the target path after ensuring the directory exists. Avoid writing directly
with writeFileSync to the final path.
🪄 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: d017e9f5-3475-469c-9ffd-08c9aa6dd279

📥 Commits

Reviewing files that changed from the base of the PR and between adedeb7 and cb3a7bc.

📒 Files selected for processing (2)
  • sdk/typescript/src/cli/harness-wrapper.ts
  • sdk/typescript/tests/local-agents-handshake.test.ts

Comment on lines +611 to +621
function announceLocalAgent(entry: LocalAgentEntry, now: number): void {
try {
const dir = localAgentsDir();
const path = join(dir, "local-agents.json");
const next = mergeLocalAgentEntry(readLocalAgentsFile(path), entry, now);
mkdirSync(dir, { recursive: true });
writeFileSync(path, `${JSON.stringify(next, undefined, 2)}\n`, "utf8");
} catch {
// Ignore — co-location is an optimization; contact request still fires.
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the file and inspect the relevant region with line numbers.
wc -l sdk/typescript/src/cli/harness-wrapper.ts
sed -n '560,660p' sdk/typescript/src/cli/harness-wrapper.ts

# Find the trust-boundary docs and related filesystem helpers.
rg -n "Same-user filesystem access|local-agents.json|announceLocalAgent|localAgentsDir|mkdirSync\\(|writeFileSync\\(" sdk/typescript/src/cli/harness-wrapper.ts sdk/typescript/src -S

Repository: tinyhumansai/tiny.place

Length of output: 6130


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the documented trust boundary and the structure of the handshake entry.
sed -n '532,590p' sdk/typescript/src/cli/harness-wrapper.ts

# See whether this path ever sets restrictive permissions elsewhere.
rg -n "chmod|mode: 0o7|0o600|0o700|local-agents.json|\\.openhuman" sdk/typescript/src -S

# Inspect the LocalAgentEntry fields to judge sensitivity.
sed -n '1,120p' sdk/typescript/src/cli/harness-wrapper.ts

Repository: tinyhumansai/tiny.place

Length of output: 6204


Create the handshake dir/file with owner-only permissions. ~/.openhuman/local-agents.json is the trust proof for same-user access, but mkdirSync/writeFileSync currently rely on the process umask. With a typical 022 umask, other local users can read this file and learn cwd, provider, and owner metadata. Set mode: 0o700/0o600, and chmod existing paths if they already exist.

🧰 Tools
🪛 ast-grep (0.44.1)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { spawn as spawnChild } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 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/typescript/src/cli/harness-wrapper.ts` around lines 611 - 621, Update
announceLocalAgent to create the handshake directory with mode 0o700 and the
local-agents.json file with mode 0o600, then explicitly chmod both existing
paths to enforce owner-only permissions regardless of umask. Preserve the
existing merge/write behavior and keep permission failures handled by the
current catch block.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant