Skip to content

feat(plugin): one tiny.place plugin for any harness (runtime harness detection)#215

Merged
sanil-23 merged 35 commits into
tinyhumansai:mainfrom
oxoxDev:feat/tinyplace-plugin
Jul 3, 2026
Merged

feat(plugin): one tiny.place plugin for any harness (runtime harness detection)#215
sanil-23 merged 35 commits into
tinyhumansai:mainfrom
oxoxDev:feat/tinyplace-plugin

Conversation

@oxoxDev

@oxoxDev oxoxDev commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Summary

One installable plugin — @tinyhumansai/tinyplace-plugin (sdk/plugin-tinyplace/) — that a user installs once and it detects its harness at runtime (Codex CLI / Claude Code / any future one) and self-wires: MCP server, hooks, launcher, and inbound strategy. No per-harness package, no manual wiring.

It unifies the two near-identical plugins (plugin-claude ~1280 loc, plugin-codex ~1333 loc, ~95% shared) into a single package where the ~5% that differs is isolated into runtime-selected adapters. Same 20 MCP tools, same wallet store, same Signal E2E drain/send, same daemon — one codebase, two harnesses proven live.

How it works

  • mcp/harness.mjsdetectHarness(env)activeAdapter() (memoized). Order: TINYPLACE_HARNESS override → Codex env signals (CODEX_HOME/CODEX_SESSION_ID/CODEX_THREAD_ID) → Claude signals (CLAUDE_PLUGIN_ROOT/CLAUDE_CODE_SESSION_ID) → safe default.
  • adapters/{claude,codex}.mjs — the ONLY per-harness surface: data dir, session/label prefix, serverInstructions, inbound (Claude channel push vs Codex pull), responder (claude -p vs codex exec), and launch.prepare() (install recipe). The shared core reads only this descriptor — no harness name appears anywhere in the 20 tools, the daemon, or the hooks.
  • bin/tinyplace.mjs — one harness-agnostic launcher: wallet store + menu + import/register, then delegates to activeAdapter().launch.prepare(). Claude gets claude --plugin-dir; Codex gets an isolated CODEX_HOME (config.toml + auto-discovered hooks.json + symlinked auth.json). --harness <name> forces it; otherwise auto-detected.
  • foreground-inject slot — a fail-open core no-op today, so sanil-23's feat(sdk/plugin-claude): foreground-resolve inbound DMs via tmux TTY injection #212 tmux wake-an-idle-pane logic drops in later with no server/daemon/hook rework.

Contributor convention (add a harness safely)

Requested guardrail so new contributors can't break the plugin while adding a harness:

  • adapters/README.md — the authoring guide: the full field-contract table, launch.prepare rules, a wiring checklist, and the do-nots (no harness names in core, no shared data dir, keep the UNTRUSTED framing, always leave an inbound delivery path).
  • adapter-contract-test.mjs (wired into npm test) — structurally enforces that contract for every adapter in the ADAPTERS map: a missing/wrong-shaped field, a shared data dir, a dropped UNTRUSTED guard, an inbound with no delivery path, or a launch.prepare that returns an invalid plan fails CI. Adding a harness = copy an adapter, fill the fields, add a detection signal, make the contract green.

Security

  • Codex auto-responder runs in --sandbox read-only (never --dangerously-bypass-approvals-and-sandbox). The responder pipes attacker-controlled DM text into codex exec; read-only sandbox keeps a prompt injection off the shell/filesystem — the only side-effecting path is the auto_reply MCP tool. (Parity with the plugin-codex feat(sdk): Codex CLI plugin for tiny.place (parity port of plugin-claude) #214 critical fix.)
  • serverInstructions mark inbound DMs as UNTRUSTED data, never instructions — enforced by the contract test.

Testing

Self-contained offline suite + a live-boot smoke, all green:

  • harness 28 · adapter-contract 72 (2 adapters) · branch 16 (same core flips codex↔claude purely from TINYPLACE_HARNESS) · envelope 34 · registry 25 · routing 22 · lock 15 · hooks
  • test:smoke 15 — boots the ONE unified MCP server under both harnesses over real JSON-RPC: identical 20-tool set, push-capability gate flips (Claude advertises claude/channel, Codex doesn't), instructions differ.

plugin-tinyplace is a standalone package (own node_modules/lockfile), excluded from the pnpm workspace exactly like plugin-claude — so root pnpm -r lint/build and the website format check don't cover it; it runs its own npm test.

Scope / follow-ups

Summary by CodeRabbit

  • New Features
    • Introduced the @tinyhumansai/tinyplace-plugin experience with a Tinyplace CLI/TUI for wallet setup/import and automatic harness selection (Codex/Claude).
    • Added a unified messaging flow with session-aware envelopes, inbox routing with redelivery, and automated reply/dispatch via the Tinyplace MCP server plus a per-agent background daemon.
  • Bug Fixes
    • Improved reliability and safety for distributed agent locking, outbox claiming/recovery, and inbox draining/routing.
  • Chores
    • Updated workspace/install handling for the Tinyplace plugin and added adapter design/spec documentation and validation tests.

oxoxDev added 20 commits July 3, 2026 20:04
The auto-responder pipes attacker-controlled DM text into `codex exec`; running
it with --dangerously-bypass-approvals-and-sandbox let a prompt injection reach
the shell/filesystem. Switch to --sandbox read-only so the only side-effecting
path is the auto_reply MCP tool. Parity with the plugin-codex tinyhumansai#214 critical fix.
Each adapter now owns its launcher install recipe via launch.prepare(ctx) →
{command,args,env}: Claude points `claude --plugin-dir` at the package; Codex
writes an isolated CODEX_HOME (config.toml + auto-discovered hooks.json +
symlinked auth.json). The shared launcher stays harness-agnostic.
One launcher for any harness: owns wallet store + arrow-key menu + import/
register flows, then delegates to activeAdapter().launch.prepare() for the boot
plan. --harness <name> (or TINYPLACE_HARNESS) forces the adapter; otherwise
auto-detected. Replaces the two near-identical plugin-claude/plugin-codex
launchers.
…rness

adapter-contract-test.mjs iterates the ADAPTERS map and structurally asserts the
full per-harness contract (data-dir isolation, UNTRUSTED framing, an inbound
delivery path, responder arg threading, a valid launch.prepare plan, reachable
via TINYPLACE_HARNESS). A missing/wrong-shaped field fails CI. Wired into `test`.
adapters/README.md: the field-contract table, launch.prepare rules, a wiring
checklist, and the do-nots (no harness names in core, no shared data dir, keep
UNTRUSTED framing). DESIGN.md: launcher + convention sections + the deferred
shim-conversion note. New contributors add a harness by copying an adapter and
making the contract test green.
@vercel

vercel Bot commented Jul 3, 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 3, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1b1569dc-3450-4854-b86b-6e47cf3a9a18

📥 Commits

Reviewing files that changed from the base of the PR and between e1f9025 and 68fcb81.

📒 Files selected for processing (3)
  • sdk/plugin-tinyplace/lock-test.mjs
  • sdk/plugin-tinyplace/mcp/outbox.mjs
  • sdk/plugin-tinyplace/register.mjs
🚧 Files skipped from review as they are similar to previous changes (2)
  • sdk/plugin-tinyplace/register.mjs
  • sdk/plugin-tinyplace/mcp/outbox.mjs

📝 Walkthrough

Walkthrough

This PR adds a standalone sdk/plugin-tinyplace package with harness-aware adapters, filesystem-backed session and daemon state, message routing and hook scripts, an MCP server, a CLI launcher, and offline validation scripts.

Changes

Tinyplace Plugin Package

Layer / File(s) Summary
Workspace and package scaffolding
pnpm-workspace.yaml, sdk/plugin-tinyplace/.gitignore, sdk/plugin-tinyplace/DESIGN.md, sdk/plugin-tinyplace/package.json, sdk/plugin-tinyplace/.claude-plugin/plugin.json, sdk/plugin-tinyplace/.mcp.json, sdk/plugin-tinyplace/adapters/README.md
Excludes the package from pnpm workspace operations, adds package metadata, and documents the standalone design and adapter contract.
Harness detection and adapters
sdk/plugin-tinyplace/mcp/harness.mjs, sdk/plugin-tinyplace/mcp/address.mjs, sdk/plugin-tinyplace/adapters/claude.mjs, sdk/plugin-tinyplace/adapters/codex.mjs, sdk/plugin-tinyplace/adapter-contract-test.mjs, sdk/plugin-tinyplace/harness-test.mjs, sdk/plugin-tinyplace/branch-test.mjs
Detects the active harness, resolves adapter descriptors, and validates adapter wiring and harness-specific behavior.
Session registry and daemon locking
sdk/plugin-tinyplace/mcp/registry.mjs, sdk/plugin-tinyplace/mcp/daemon-lock.mjs, sdk/plugin-tinyplace/registry-test.mjs, sdk/plugin-tinyplace/lock-test.mjs
Implements filesystem-backed session presence and daemon lock handling, plus offline tests for allocation, liveness, stealing, and release behavior.
Message format, routing, and outbox
sdk/plugin-tinyplace/mcp/format.mjs, sdk/plugin-tinyplace/mcp/routing.mjs, sdk/plugin-tinyplace/mcp/outbox.mjs, sdk/plugin-tinyplace/mcp/foreground-inject.mjs, sdk/plugin-tinyplace/envelope-test.mjs, sdk/plugin-tinyplace/routing-test.mjs
Encodes and decodes envelope bodies, routes inbound messages into inbox queues, manages outbox claims, and validates routing and envelope behavior.
Daemon and hook scripts
sdk/plugin-tinyplace/hooks/agent-daemon.mjs, sdk/plugin-tinyplace/hooks/dispatch.mjs, sdk/plugin-tinyplace/hooks/respond-batch.mjs, sdk/plugin-tinyplace/hooks/surface-inbound.mjs, sdk/plugin-tinyplace/hooks/hooks.json, sdk/plugin-tinyplace/hooks-test.mjs
Implements the per-agent daemon, stop-hook dispatch path, batch responder, inbound surfacing hook, hook registration, and hook-level tests.
MCP server and smoke test
sdk/plugin-tinyplace/mcp/server.mjs, sdk/plugin-tinyplace/mcp-smoke.mjs
Implements the stdio MCP server and validates its tool surface across Codex and Claude harnesses.
CLI launcher and wallet TUI
sdk/plugin-tinyplace/bin/tinyplace.mjs, sdk/plugin-tinyplace/register.mjs
Implements wallet creation/import, TUI selection, registration flow, and harness launch via the selected adapter.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • tinyhumansai/tiny.place#5: Introduces the wallet/signing primitives used by the new Tinyplace CLI registration and wallet flows.
  • tinyhumansai/tiny.place#209: Adds the session-aware messaging and daemon plumbing that this plugin now generalizes for a unified Codex/Claude package.

Poem

A rabbit hops through Codex and Claude,
One tiny plugin, no shim is flawed.
Queues are routed, locks stand tight,
Hooks hum gently through the night,
And envelopes land just right 🐰

🚥 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 summarizes the main change: a single TinyPlace plugin with runtime harness detection for any harness.
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: 3020383bfa

ℹ️ 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".

Comment thread sdk/plugin-tinyplace/adapters/claude.mjs
Comment thread sdk/plugin-tinyplace/hooks/hooks.json
Comment thread sdk/plugin-tinyplace/bin/tinyplace.mjs

@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: 9

🧹 Nitpick comments (5)
sdk/plugin-tinyplace/mcp-smoke.mjs (1)

51-57: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

rpc() has no timeout — a hung/crashed server makes the smoke test block forever.

If the child never emits a response line (boot failure, transport crash), the awaited promise never settles and the test hangs with no diagnostic instead of failing. Add a timeout and reject on child exit.

♻️ Proposed hardening
   const rpc = (method, params) => {
     const id = nextId++;
-    return new Promise((resolve) => {
-      pending.set(id, resolve);
-      child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n");
-    });
+    return new Promise((resolve, reject) => {
+      const timer = setTimeout(() => { pending.delete(id); reject(new Error(`rpc ${method} timed out`)); }, 15000);
+      pending.set(id, (msg) => { clearTimeout(timer); resolve(msg); });
+      child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n");
+    });
   };
🤖 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/mcp-smoke.mjs` around lines 51 - 57, The rpc() helper
currently leaves requests pending forever if the child process stops responding,
so harden it by adding a timeout and rejection path. Update rpc() to create a
timer for each request that rejects and clears the pending entry if no response
arrives in time, and also reject any still-pending requests when the child
process exits or errors. Use the existing rpc(), pending map, nextId counter,
and child.stdin/child process wiring to locate the fix.
sdk/plugin-tinyplace/bin/tinyplace.mjs (1)

53-61: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Consider an atomic write for the secret-key store.

saveWallets writes wallets.json in place via writeFileSync. A crash or full disk mid-write can truncate/corrupt the file, and this file holds irreplaceable Ed25519 seeds (secretKey) — corruption means permanent loss of the identity. Prefer write-to-temp + renameSync (rename is atomic on the same filesystem). Since this logic mirrors mcp/server.mjs "byte-for-byte", apply the same change there to keep them in sync.

🛡️ Proposed atomic write
 function saveWallets(wallets) {
   mkdirSync(DATA_DIR, { recursive: true });
-  writeFileSync(WALLETS_FILE, JSON.stringify({ wallets }, null, 2) + "\n", { mode: 0o600 });
-  try {
-    chmodSync(WALLETS_FILE, 0o600);
-  } catch {
-    /* best-effort */
-  }
+  const tmp = `${WALLETS_FILE}.tmp`;
+  writeFileSync(tmp, JSON.stringify({ wallets }, null, 2) + "\n", { mode: 0o600 });
+  try {
+    chmodSync(tmp, 0o600);
+  } catch {
+    /* best-effort */
+  }
+  renameSync(tmp, WALLETS_FILE);
 }

(renameSync would need adding to the node:fs import.)

🤖 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/bin/tinyplace.mjs` around lines 53 - 61, The saveWallets
secret-key persistence is writing wallets.json directly, which can leave the
file truncated if the process crashes or disk fills mid-write. Update
saveWallets to use a write-to-temp-then-renameSync flow so the final file is
replaced atomically, and make sure renameSync is imported from node:fs. Apply
the same atomic-write change in the mirrored wallet persistence logic in
mcp/server.mjs so both implementations stay byte-for-byte aligned.
sdk/plugin-tinyplace/mcp/routing.mjs (1)

59-62: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Avoid the duplicate session scan on the inbound hot path.

liveSessions(agentAddress) and primarySession(agentAddress) each run a full readSessions (directory scan + JSON parse of every presence file), and primarySession internally re-invokes liveSessions. Since primarySession is just liveSessions()[0] (already label-sorted), derive both from a single call.

♻️ Proposed refactor
-  const live = liveSessions(agentAddress).map((s) => s.label);
-  const primary = primarySession(agentAddress)?.label ?? null;
+  const liveList = liveSessions(agentAddress);
+  const live = liveList.map((s) => s.label);
+  const primary = liveList[0]?.label ?? null;
🤖 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/mcp/routing.mjs` around lines 59 - 62, The inbound hot
path in enqueueRouted is scanning sessions twice because
liveSessions(agentAddress) and primarySession(agentAddress) each trigger
readSessions, with primarySession reusing liveSessions again. Refactor
enqueueRouted to derive both live and primary from a single
liveSessions(agentAddress) call, using the first sorted session label as the
primary value, and keep routeTarget unchanged except for consuming those shared
results.
sdk/plugin-tinyplace/mcp/outbox.mjs (2)

1-90: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing dedicated offline test for outbox.mjs.

registry.mjs and daemon-lock.mjs each have a test file (registry-test.mjs, lock-test.mjs) that specifically stresses concurrent claim/reclaim races. outbox.mjs implements comparable atomic-claim/stale-recovery logic (and per the concern above, its own subtle race) but has no equivalent test in this cohort.

🤖 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/mcp/outbox.mjs` around lines 1 - 90, Add a dedicated
offline race test for outbox.mjs to cover the atomic claim and stale-recovery
behavior in claimOutboxJobs and recoverStaleClaims. Mirror the style of
registry-test.mjs and lock-test.mjs by exercising writeOutboxJob, concurrent
claims, and reclaiming a .sending-* file after it becomes stale. Verify the job
is not double-claimed, that a crashed claim is requeued, and that done()/fail()
leave the outbox in the expected state.

58-90: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

No backoff/attempt-limit on failed jobs.

fail() renames a claim straight back to pending on any error, including the 403 contact-gate path in drainOutbound. With no backoff or retry-count, a persistently-failing job (blocked/unreachable recipient) becomes immediately reclaimable on the next poll, which can turn into a tight retry loop hammering the send 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/mcp/outbox.mjs` around lines 58 - 90, The retry path for
claimed outbox jobs has no delay or attempt cap, so a failing job is immediately
requeued and can loop forever. Update the outbox flow around claimOutboxJobs()
and the claimed.fail() behavior so drainOutbound() can track retry state for
each job, apply backoff or a max retry limit before making it reclaimable again,
and avoid instantly renaming the claim back to the pending file on repeated
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/adapters/claude.mjs`:
- Around line 41-48: The headless responder in claude.mjs is launching `claude
-p` with `--dangerously-skip-permissions`, which bypasses safety checks for
untrusted DM input. Update the `responder.buildArgs` logic under the `responder`
object to remove that flag and use the same restricted/sandboxed permission
model as the Codex responder, keeping the unattended `command: "claude"` flow
otherwise intact.

In `@sdk/plugin-tinyplace/adapters/codex.mjs`:
- Around line 136-140: The hooks.json templating in codex.mjs is inserting
pluginDir as a raw string, which can break JSON when the path contains
backslashes. Update the hooks.json generation in the codex adapter to use a
JSON-safe escaping approach before replacing ${TINYPLACE_PLUGIN_ROOT}, and keep
the change localized around the hooksTemplate/readFileSync/writeFileSync flow so
Windows paths remain valid.

In `@sdk/plugin-tinyplace/adapters/README.md`:
- Around line 37-40: The adapter contract for the inbound push channel is
inconsistent between this table and the design docs; align the README’s
`inbound`/`push` description with the canonical schema used by `DESIGN.md`.
Update the `inbound` row so it references the same field names as the rest of
the adapter contract, and make sure the `responder`, `install`, and `launch`
rows remain unchanged while the `push` shape is documented consistently in one
place.

In `@sdk/plugin-tinyplace/bin/tinyplace.mjs`:
- Around line 184-192: The raw-mode menu input handler in onData currently
ignores Ctrl+C, so add explicit cancel handling for the control bytes that
arrive in raw mode. Update the onData logic in tinyplace.mjs to treat "\x03"
(Ctrl+C) and "\x04" as a cancel path that calls finish(-1), alongside the
existing q handling, before falling through to render().

In `@sdk/plugin-tinyplace/DESIGN.md`:
- Around line 14-29: The diagram block in DESIGN.md is using an untagged fenced
code block, which triggers markdownlint MD040. Update the fence around the
architecture diagram to include a language tag such as text so the block remains
rendered correctly and passes lint; this applies to the diagram section near
detectHarness() and adapters[harness].

In `@sdk/plugin-tinyplace/hooks/respond-batch.mjs`:
- Around line 52-69: The prompt builder in buildPrompt interpolates untrusted
msg.id and msg.from directly into the auto_reply tool-call string, so sanitize
or escape both fields before insertion. Add validation/quoting for these values
in respond-batch.mjs (alongside the existing SAFE_SESSION_RE handling) so the
generated in_reply_to and to arguments cannot break the prompt or tool syntax.

In `@sdk/plugin-tinyplace/hooks/surface-inbound.mjs`:
- Around line 129-142: The direct-message context built in surface-inbound.mjs
needs the same untrusted-data framing used elsewhere. Update the `context`
construction around `fresh`, `shown`, and `emit(hookEventName, context)` so the
surfaced preview is explicitly labeled as untrusted and not to be followed,
before listing the attacker-controlled message snippets. Keep the existing inbox
guidance, but prepend/inline the safety warning so any model reading this
`additionalContext` treats the DM preview as data only, not instructions.

In `@sdk/plugin-tinyplace/mcp/outbox.mjs`:
- Around line 18-39: `recoverStaleClaims()` is requeueing `.sending-*` jobs
purely by age, which can duplicate deliveries when the owning daemon is still
running. Update the stale-claim recovery logic in `recoverStaleClaims` to parse
the owner pid from the `.sending-` claim name and check that pid is no longer
alive before calling `renameSync`; only restore the original job file when the
claim is both old enough and orphaned.

In `@sdk/plugin-tinyplace/package.json`:
- Around line 13-16: The smoke test command in package.json is defined but not
part of the main validation path, so live-boot coverage can be skipped. Update
the test workflow around the existing scripts block in package.json so
test:smoke is invoked by the CI/release gate, either by adding it to the main
test chain or wiring it into the automated pipeline used for plugin-tinyplace
validation. Keep the existing mcp-smoke.mjs entrypoint and ensure the gating
change is applied consistently wherever the package’s tests are orchestrated.

---

Nitpick comments:
In `@sdk/plugin-tinyplace/bin/tinyplace.mjs`:
- Around line 53-61: The saveWallets secret-key persistence is writing
wallets.json directly, which can leave the file truncated if the process crashes
or disk fills mid-write. Update saveWallets to use a
write-to-temp-then-renameSync flow so the final file is replaced atomically, and
make sure renameSync is imported from node:fs. Apply the same atomic-write
change in the mirrored wallet persistence logic in mcp/server.mjs so both
implementations stay byte-for-byte aligned.

In `@sdk/plugin-tinyplace/mcp-smoke.mjs`:
- Around line 51-57: The rpc() helper currently leaves requests pending forever
if the child process stops responding, so harden it by adding a timeout and
rejection path. Update rpc() to create a timer for each request that rejects and
clears the pending entry if no response arrives in time, and also reject any
still-pending requests when the child process exits or errors. Use the existing
rpc(), pending map, nextId counter, and child.stdin/child process wiring to
locate the fix.

In `@sdk/plugin-tinyplace/mcp/outbox.mjs`:
- Around line 1-90: Add a dedicated offline race test for outbox.mjs to cover
the atomic claim and stale-recovery behavior in claimOutboxJobs and
recoverStaleClaims. Mirror the style of registry-test.mjs and lock-test.mjs by
exercising writeOutboxJob, concurrent claims, and reclaiming a .sending-* file
after it becomes stale. Verify the job is not double-claimed, that a crashed
claim is requeued, and that done()/fail() leave the outbox in the expected
state.
- Around line 58-90: The retry path for claimed outbox jobs has no delay or
attempt cap, so a failing job is immediately requeued and can loop forever.
Update the outbox flow around claimOutboxJobs() and the claimed.fail() behavior
so drainOutbound() can track retry state for each job, apply backoff or a max
retry limit before making it reclaimable again, and avoid instantly renaming the
claim back to the pending file on repeated failures.

In `@sdk/plugin-tinyplace/mcp/routing.mjs`:
- Around line 59-62: The inbound hot path in enqueueRouted is scanning sessions
twice because liveSessions(agentAddress) and primarySession(agentAddress) each
trigger readSessions, with primarySession reusing liveSessions again. Refactor
enqueueRouted to derive both live and primary from a single
liveSessions(agentAddress) call, using the first sorted session label as the
primary value, and keep routeTarget unchanged except for consuming those shared
results.
🪄 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: c9cb7e81-531b-496a-bc43-739acc8f52cf

📥 Commits

Reviewing files that changed from the base of the PR and between 3f9218a and 3020383.

📒 Files selected for processing (31)
  • pnpm-workspace.yaml
  • sdk/plugin-tinyplace/.gitignore
  • sdk/plugin-tinyplace/DESIGN.md
  • sdk/plugin-tinyplace/adapter-contract-test.mjs
  • sdk/plugin-tinyplace/adapters/README.md
  • sdk/plugin-tinyplace/adapters/claude.mjs
  • sdk/plugin-tinyplace/adapters/codex.mjs
  • sdk/plugin-tinyplace/bin/tinyplace.mjs
  • sdk/plugin-tinyplace/branch-test.mjs
  • sdk/plugin-tinyplace/envelope-test.mjs
  • sdk/plugin-tinyplace/harness-test.mjs
  • sdk/plugin-tinyplace/hooks-test.mjs
  • sdk/plugin-tinyplace/hooks/agent-daemon.mjs
  • sdk/plugin-tinyplace/hooks/dispatch.mjs
  • sdk/plugin-tinyplace/hooks/hooks.json
  • sdk/plugin-tinyplace/hooks/respond-batch.mjs
  • sdk/plugin-tinyplace/hooks/surface-inbound.mjs
  • sdk/plugin-tinyplace/lock-test.mjs
  • sdk/plugin-tinyplace/mcp-smoke.mjs
  • sdk/plugin-tinyplace/mcp/address.mjs
  • sdk/plugin-tinyplace/mcp/daemon-lock.mjs
  • sdk/plugin-tinyplace/mcp/foreground-inject.mjs
  • sdk/plugin-tinyplace/mcp/format.mjs
  • sdk/plugin-tinyplace/mcp/harness.mjs
  • sdk/plugin-tinyplace/mcp/outbox.mjs
  • sdk/plugin-tinyplace/mcp/registry.mjs
  • sdk/plugin-tinyplace/mcp/routing.mjs
  • sdk/plugin-tinyplace/mcp/server.mjs
  • sdk/plugin-tinyplace/package.json
  • sdk/plugin-tinyplace/registry-test.mjs
  • sdk/plugin-tinyplace/routing-test.mjs

Comment thread sdk/plugin-tinyplace/adapters/claude.mjs
Comment thread sdk/plugin-tinyplace/adapters/codex.mjs
Comment thread sdk/plugin-tinyplace/adapters/README.md
Comment thread sdk/plugin-tinyplace/bin/tinyplace.mjs
Comment thread sdk/plugin-tinyplace/DESIGN.md Outdated
Comment thread sdk/plugin-tinyplace/hooks/respond-batch.mjs
Comment thread sdk/plugin-tinyplace/hooks/surface-inbound.mjs
Comment thread sdk/plugin-tinyplace/mcp/outbox.mjs
Comment thread sdk/plugin-tinyplace/package.json
oxoxDev added 6 commits July 3, 2026 20:46
…ns (tinyhumansai#215)

The Claude autoresponder fed attacker-controlled DM text into `claude -p`
with --dangerously-skip-permissions, granting unrestricted shell/fs to a
prompt-injected message. Mirror the codex `--sandbox read-only` intent:
--permission-mode dontAsk (auto-deny unlisted tools), --tools "" (strip all
built-in tools), --allowedTools mcp__tinyplace__auto_reply (single MCP tool as
the only side-effecting path).
…ols (tinyhumansai#215)

The Claude launcher points `claude --plugin-dir` at this package, but the
package shipped no .claude-plugin/plugin.json or .mcp.json (they lived only
under plugin-claude), so Claude opened with NO tinyplace MCP tools. Port both,
declaring the MCP server as `tinyplace` (matching `server:tinyplace` in the
launch args) and pointing it at mcp/server.mjs via ${CLAUDE_PLUGIN_ROOT}.
…tinyhumansai#215)

hooks/hooks.json commands reference ${TINYPLACE_PLUGIN_ROOT}. Codex substitutes
it at install; Claude consumes the shared hooks.json directly, and the launch
env only set TINYPLACE_ACTIVE_WALLET — so hook commands expanded to
node "/hooks/surface-inbound.mjs" and inbound surfacing + the Stop
autoresponder broke on Claude. Set the env var to ctx.pluginDir; keep the
shared token in hooks.json since codex depends on it.
…inyhumansai#215)

bin/tinyplace.mjs's registerFlow spawns <plugin>/register.mjs, which was absent
from this package (only in plugin-claude) — the Register @handle menu item hit a
missing script. Port it harness-agnostic: the wallet store resolves via
harnessDataDir() (the active adapter's data dir) instead of a hardcoded
TINYPLACE_CLAUDE_HOME, so it serves both Codex and Claude. Signature
(<walletName> <baseHandle>) matches the registerFlow spawn.
…nyhumansai#215)

ensureIsolatedHome spliced pluginDir into hooks.json via a raw string replace.
On Windows the backslashes in the path (and any embedded quote) land inside JSON
string values and produce invalid JSON, breaking every hook. Escape the path
with JSON.stringify(...).slice(1,-1) before splicing.
oxoxDev added 7 commits July 3, 2026 21:24
…humansai#215)

recoverStaleClaims requeued .sending-* claims by mtime age alone, so a send
still in flight past STALE_CLAIM_MS was reclaimed and delivered twice. The claim
name carries the owner daemon's pid — check liveness (process.kill(pid, 0)) and
only reclaim a dead (or unknown, age-gated) owner's claim. Extend lock-test to
cover the live-pid skip vs dead-pid reclaim.
…inyhumansai#215)

respond-batch dropped attacker-controlled msg.from and msg.id straight into
quoted tool-call arguments (to="…", in_reply_to="…") in the responder prompt,
so a crafted id/from could close the quote and inject extra args or
instructions. Strip both to a safe charset ([\w:.-]) — well-formed base58 ids
and slug-like relay ids are unchanged. Also refresh the stale responder-flag
comment to match the sandboxed codex/claude arg sets.
…ansai#215)

The attacker-controlled DM previews were injected verbatim as additionalContext
with no untrusted-data framing, unlike server.mjs pushToChannel and respond-batch.
Add the UNTRUSTED framing line so an embedded prompt-injection in a preview is
treated as data, not instructions.
…humansai#215)

DESIGN.md described inbound.push as {capability, pushMessage, pushContactRequest},
but the actual adapter/code shape (claude.mjs, server.mjs consuming
ADAPTER.inbound.push.capability/.method) and adapters/README.md both use
{capability, method}. Align DESIGN.md with the real schema.
…ansai#215)

Under setRawMode(true), Ctrl+C is byte 0x03 (not SIGINT) and Ctrl+D is 0x04, and
onData ignored both — so the menu swallowed Ctrl+C and couldn't be exited that
way. Treat 0x03/0x04 as cancel (restore terminal, exit cleanly) like q/ESC.
…i#215)

The ASCII architecture diagram used a bare code fence, tripping markdownlint
MD040 (fenced-code-language). Tag it `text`.
…yhumansai#215)

mcp-smoke.mjs boots the real unified MCP server over stdio for both harnesses
and asserts the 20-tool set + push-capability gate. Its header documents it as
offline (no tool touches the relay) and it passes with no network, so wire it
into the automated `test` script rather than leaving it standalone. `test:smoke`
is kept as a standalone alias for running it in isolation.
@oxoxDev

oxoxDev commented Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

All CodeRabbit + codex review comments addressed in the 12 commits pushed (8bb2062…e1f90251), each replied inline with the fixing SHA. Highlights: the headless Claude responder is now sandboxed (dontAsk + tools stripped + single auto_reply MCP tool), Claude plugin metadata (.claude-plugin/plugin.json + .mcp.json) and TINYPLACE_PLUGIN_ROOT wiring restored, register.mjs ported, outbox double-send closed via pid-liveness, respond-batch args sanitized, and the offline MCP smoke is now CI-gated. Full suite (9 suites) green.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

@oxoxDev Thanks for the detailed summary of the fixes. I'll take a fresh look at the full set of changes now.

(=^・ω・^=)

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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

🤖 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/mcp/outbox.mjs`:
- Around line 49-58: The fallback path in outbox.mjs is leaving unknown-owner
claim names with the generic `.sending-` prefix, so the age-gated requeue never
restores the original filename. Update the claim-name handling around the `orig`
extraction logic in the outbox scan so unparseable names like
`.sending-job.json` are converted back to `job.json` before the rename/requeue
step, while preserving the existing owner-pid branch behavior in the
`pidAlive`/`STALE_CLAIM_MS` flow.

In `@sdk/plugin-tinyplace/register.mjs`:
- Around line 19-20: Validate the CLI inputs in register.mjs before any wallet
registry or handle derivation work begins: the argument parsing around
process.argv.slice(2) should explicitly require both walletName (name) and
baseHandle, and abort early with a clear error if either is missing or empty.
Update the register flow that consumes baseHandle so it never calls the registry
with undefined and never builds fallback handles from an undefined value; keep
the check near the existing argument destructuring and h2b helper so the
entrypoint fails fast.
- Around line 30-35: The fallback handle logic in register.mjs needs to avoid
generating invalid suffixes and must only return a handle that has been checked
for availability. Update the handle selection inside the loop around
client.registry.get(handle) so the reduction over w.address uses a numeric
accumulator from the start, preventing NaN when the address contains non-numeric
characters, and make sure the final handle assignment does not create a new
unchecked candidate after the last failed probe. Keep the fix localized to the
registration loop and the handle generation using baseHandle,
client.registry.get, and w.address.
🪄 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: 40ef172c-5f11-46e4-914a-262aedb67ba2

📥 Commits

Reviewing files that changed from the base of the PR and between 07d4f63 and e1f9025.

📒 Files selected for processing (12)
  • sdk/plugin-tinyplace/.claude-plugin/plugin.json
  • sdk/plugin-tinyplace/.mcp.json
  • sdk/plugin-tinyplace/DESIGN.md
  • sdk/plugin-tinyplace/adapters/claude.mjs
  • sdk/plugin-tinyplace/adapters/codex.mjs
  • sdk/plugin-tinyplace/bin/tinyplace.mjs
  • sdk/plugin-tinyplace/hooks/respond-batch.mjs
  • sdk/plugin-tinyplace/hooks/surface-inbound.mjs
  • sdk/plugin-tinyplace/lock-test.mjs
  • sdk/plugin-tinyplace/mcp/outbox.mjs
  • sdk/plugin-tinyplace/package.json
  • sdk/plugin-tinyplace/register.mjs
✅ Files skipped from review due to trivial changes (2)
  • sdk/plugin-tinyplace/.mcp.json
  • sdk/plugin-tinyplace/.claude-plugin/plugin.json
🚧 Files skipped from review as they are similar to previous changes (7)
  • sdk/plugin-tinyplace/package.json
  • sdk/plugin-tinyplace/adapters/claude.mjs
  • sdk/plugin-tinyplace/adapters/codex.mjs
  • sdk/plugin-tinyplace/hooks/surface-inbound.mjs
  • sdk/plugin-tinyplace/DESIGN.md
  • sdk/plugin-tinyplace/hooks/respond-batch.mjs
  • sdk/plugin-tinyplace/bin/tinyplace.mjs

Comment thread sdk/plugin-tinyplace/mcp/outbox.mjs
Comment thread sdk/plugin-tinyplace/register.mjs
Comment thread sdk/plugin-tinyplace/register.mjs Outdated
sanil-23 and others added 2 commits July 4, 2026 00:00
…humansai#215)

recoverStaleClaims stripped a `\d+-` prefix for unparseable claim names, so an
owner-less claim like `.sending-job.json` kept its full name in `orig` and
renameSync(p, orig) was a rename-to-self — the aged job was never requeued (and
the leftover dot-file is skipped by the pending scan, so it's silently dropped).
Strip the generic `.sending-` prefix instead. Extend lock-test with the aged
unknown-owner requeue (restored to its real name) and the fresh-claim age-gate
skip. Addresses CodeRabbit review on mcp/outbox.mjs.

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

Two correctness bugs in the ported register flow:
- No CLI-arg validation: a missing baseHandle called the registry with
  `undefined` and derived `undefined…` fallback handles. Fail fast with usage.
- The availability loop assigned a fresh handle on its last failed iteration and
  then registered it WITHOUT checking availability. Rework to test each candidate
  (base, then deterministic variants) and only register one confirmed available;
  exit non-zero if none of the 6 tries is free.

Addresses CodeRabbit review on register.mjs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sanil-23 sanil-23 merged commit e3f3194 into tinyhumansai:main Jul 3, 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