feat(cli): announce co-located agent to OpenHuman before contact request#242
feat(cli): announce co-located agent to OpenHuman before contact request#242oxoxDev wants to merge 1 commit into
Conversation
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.
|
@oxoxDev is attempting to deploy a commit to the Vezures Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughThe 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. ChangesLocal handshake
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
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches⚔️ Resolve merge conflicts
Comment |
There was a problem hiding this comment.
💡 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( |
There was a problem hiding this comment.
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 👍 / 👎.
| mkdirSync(dir, { recursive: true }); | ||
| writeFileSync(path, `${JSON.stringify(next, undefined, 2)}\n`, "utf8"); |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
sdk/typescript/src/cli/harness-wrapper.ts (1)
615-617: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winWrite atomically to avoid the OpenHuman reader seeing a partial file.
writeFileSyncis 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 thenrenameSyncfor an atomic swap (addrenameSyncto thenode:fsimport).♻️ 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
📒 Files selected for processing (2)
sdk/typescript/src/cli/harness-wrapper.tssdk/typescript/tests/local-agents-handshake.test.ts
| 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. | ||
| } | ||
| } |
There was a problem hiding this comment.
🔒 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 -SRepository: 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.tsRepository: 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.
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.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
mergeLocalAgentEntry(add / self-replace / keep-others / prune-stale-and-undated): 4 passing.tinyplace codexwrites a valid fresh entry to~/.openhuman/local-agents.json.Notes
This branches off
main, which has a single contact-request path (ensureContactNow). Thefeat/tui-session-historybranch adds a second path (InboundMessageReceiver.ensureOwnerContact); a follow-up will add the same announce there once that lands.Summary by CodeRabbit
New Features
Bug Fixes