Skip to content

feat(plugin): mock harness + end-to-end orchestration test (mocked LLM) + Signal key-routing fix#223

Merged
senamakel merged 7 commits into
tinyhumansai:mainfrom
senamakel:feat/plugin-mock-orchestration-e2e
Jul 5, 2026
Merged

feat(plugin): mock harness + end-to-end orchestration test (mocked LLM) + Signal key-routing fix#223
senamakel merged 7 commits into
tinyhumansai:mainfrom
senamakel:feat/plugin-mock-orchestration-e2e

Conversation

@senamakel

Copy link
Copy Markdown
Member

Verifies the whole tiny.place coding-agent orchestration pipe end to end with a mocked LLM and no terminal, and fixes three real bugs that were blocking it.

What's here

A mock coding-agent harness (sdk/plugin-tinyplace/adapters/mock.mjs) — a deterministic stand-in for the Claude Code / Codex TUI. It's registered but never auto-detected (only via TINYPLACE_HARNESS=mock), so the real harness paths are untouched (passes adapter-contract-test). The LLM is mocked at the one seam the core exposes — adapter.responder — which spawns test/mock-responder.mjs (a canned reply written to the outbox) instead of claude -p / codex exec.

An end-to-end orchestration test (sdk/plugin-tinyplace/orchestration-live-e2e.mjs) against a real backend, no model:

human peer (SDK) → contact connection → Signal DM → real per-agent daemon drains + decrypts (owns the ratchet) → routes headless → autoresponder → mock LLM → outbox → daemon encrypts + sends reply → human decrypts.

Asserts the full round trip, that the mock LLM saw the message, and that the relay only ever held ciphertext. test/ORCHESTRATION.md documents the pieces + the human connect/send flow (CLI + scripted).

Bugs found & fixed (surfaced by the e2e)

  1. fix(sdk): relay key routes by base58 cryptoId, not the base64 key. EncryptionContext.publishKeyBundle/getBundle addressed /keys/:cryptoId/* with the base64 Ed25519 key, whose / breaks the single-segment route → 404 for ~half of all identities. Now uses deriveCryptoId(pubkey) (== signer.agentId); identity key still travels in the body. Full SDK vitest suite green (354 passed); 8/8 slash-containing keys now publish.
  2. fix(plugin): don't strip base64 chars from the auto-reply target. respond-batch's safeArg stripped +/= from the reply address (assumed base58), mangling base64 targets so the headless autoresponder's auto_reply could never be delivered to a base64-addressed peer. Now preserves address chars while keeping the injection guard.

Verification

  • Plugin offline suite green (9/9; mcp-smoke needs @modelcontextprotocol/sdk installed — unrelated).
  • Full SDK vitest run green (354 passed, 0 failed).
  • orchestration-live-e2e.mjs green + stable (4/4 runs) against the docker-compose backend.

Note: the respond-batch safeArg bug means the real headless auto-responder was broken for base64-addressed peers, independent of the mock.

https://claude.ai/code/session_01EJDqbVAWGNdmx9njh4hME8

senamakel added 4 commits July 5, 2026 00:18
…orchestration tests

Adds a `mock` harness adapter (adapters/mock.mjs) — a deterministic stand-in for
a Claude Code / Codex TUI so the full daemon → route → autoresponder → outbox →
send pipeline can be exercised end to end without a real model or terminal.

- Registered in mcp/harness.mjs but NEVER auto-detected: detectHarness only
  selects it via the explicit TINYPLACE_HARNESS=mock override, so the real
  Claude/Codex paths are untouched. Passes adapter-contract-test.
- The LLM is mocked at the one seam the core exposes: adapter.responder. Instead
  of `claude -p` / `codex exec`, it spawns test/mock-responder.mjs, which parses
  the injected prompt and writes a deterministic canned reply to the agent outbox
  — the same side-effecting path the real `auto_reply` MCP tool takes — so the
  per-agent daemon then encrypts + sends it over Signal. No network, no model.
- test/mock-tui.mjs is a trivial launch stand-in for the launcher recipe.

Full offline plugin suite green (mcp-smoke needs @modelcontextprotocol/sdk
installed; unrelated).

Claude-Session: https://claude.ai/code/session_01EJDqbVAWGNdmx9njh4hME8
… key

Both sides of the Signal key exchange addressed the relay key routes
(/keys/:cryptoId/{signed-prekey,prekeys,bundle}) with the base64 Ed25519 key.
A base64 key contains `/`, which breaks the single-segment `:cryptoId` route
match and 404s for ~half of all identities (any key whose base64 contains a
slash):

- publishKeyBundle (enableEncryption/publishKeys) intermittently failed to
  publish, so peers could never open an X3DH session with the agent;
- encryptEnvelope's getBundle intermittently failed to fetch a peer's bundle.

Address both by the base58 cryptoId (deriveCryptoId(pubkey) == signer.agentId);
the Signal identity key still travels in the body as `identityKey`. Mirrors the
manual publish/fetch flow that already used agentId.

Verified: 8/8 fresh identities publish + fetch a bundle including 6 whose base64
key contains `/` (previously 404); full SDK vitest suite green (354 passed).

Claude-Session: https://claude.ai/code/session_01EJDqbVAWGNdmx9njh4hME8
respond-batch's safeArg sanitized msg.from (the reply target) with
/[^\w:.-]+/g, on the assumption it was always a base58 agent id. But a messaging
address is the base64 Ed25519 key, which contains `+ / =` — safeArg stripped
those, mangling the address into an unresolvable string, so the headless
autoresponder's auto_reply could never be delivered back to a base64-addressed
sender.

Allow `+ / = @` in the charset (base64 keys, cryptoIds, and @Handles). None can
terminate a double-quoted tool-call argument (only `"`/newline can, and both stay
excluded), so the prompt-injection guard is unchanged.

Surfaced by the new orchestration-live-e2e: the mock agent's reply sat
undeliverable in the outbox until the target address survived intact.

Claude-Session: https://claude.ai/code/session_01EJDqbVAWGNdmx9njh4hME8
orchestration-live-e2e.mjs drives the WHOLE coding-agent pipe against a real
backend with no model and no terminal: a "human" peer (SDK) sets up a contact
connection and sends a Signal DM; bob's real per-agent daemon under the `mock`
harness drains + decrypts it, routes it headless to the autoresponder, which
spawns the mock responder (canned reply) in place of claude -p / codex exec; the
daemon encrypts + sends the reply back and the human decrypts it. Asserts the
full round trip, that the mock LLM saw the message, and that the relay only ever
held ciphertext.

- test/ORCHESTRATION.md documents the pieces, the data flow, and the human
  connect+send flow (CLI + scripted).
- mock-responder.mjs gains a debug breadcrumb log (respond-batch runs it with
  stdio ignored) for headless troubleshooting.

Green against the docker-compose backend. Run: API_URL=... node
orchestration-live-e2e.mjs (self-links the local SDK).

Claude-Session: https://claude.ai/code/session_01EJDqbVAWGNdmx9njh4hME8
@vercel

vercel Bot commented Jul 5, 2026

Copy link
Copy Markdown

@senamakel 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 5, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 9 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ff8007e7-c291-4e1b-bf27-e3e352d21510

📥 Commits

Reviewing files that changed from the base of the PR and between 3e2f715 and 278d194.

📒 Files selected for processing (11)
  • sdk/plugin-tinyplace/adapters/mock.mjs
  • sdk/plugin-tinyplace/hooks/respond-batch.mjs
  • sdk/plugin-tinyplace/mcp/harness.mjs
  • sdk/plugin-tinyplace/orchestration-live-e2e.mjs
  • sdk/plugin-tinyplace/test/ORCHESTRATION.md
  • sdk/plugin-tinyplace/test/mock-responder.mjs
  • sdk/plugin-tinyplace/test/mock-tui.mjs
  • sdk/typescript/src/messaging/encryption.ts
  • sdk/typescript/src/version.ts
  • website/src/common/signal-messaging.test.ts
  • website/src/common/signal-messaging.ts

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: 59719ac2bd

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

// the `/` breaks the single-segment `:cryptoId` route match (→ 404) for roughly
// half of all identities. The Signal identity key (base64) still travels in the
// body as `identityKey`. Mirrors the recipient path and the manual publish flow.
const keyPathId = this.signer.agentId;

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 Derive the cryptoId for web bundle verification

This publishes the bundle under signer.agentId, but the web enable flow still calls verifyKeyBundlePublished(encryptionClient, address) (website/src/hooks/use-signal-identity.ts:119), and that helper fetches /keys/${address}/bundle with the base64 messaging key. In the real relay described by this change, /keys/:cryptoId is keyed by the base58 cryptoId, so after a successful publish the probe can 404 and clearPublishProgress() leaves isReady false; browser users cannot enable encrypted messaging. Derive the same cryptoId for the verification fetch.

Useful? React with 👍 / 👎.

let replyText = null;
let replyEnvelope = null;
for (let i = 0; i < 60 && replyText === null; i++) {
const { messages } = await aliceClient.messages.list(alice).catch(() => ({ messages: [] }));

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 Poll Alice's messaging-key inbox

The reply in this flow is addressed to Alice's base64 messaging key: Bob's daemon queues decoded.from from Alice's envelope, and sendMessage preserves raw base64 recipients. Polling messages.list(alice) here asks the relay for Alice's base58 cryptoId mailbox instead, so the new live e2e never observes the reply and times out even when the daemon sent it. Poll aliceSigner.publicKeyBase64 (or listRaw) before manual decryption.

Useful? React with 👍 / 👎.

…ish path

Follow-on to the SDK key-routing fix: verifyKeyBundlePublished probed the relay
with the base64 messaging key (`/keys/<base64>/bundle`), but the bundle is now
published under the base58 cryptoId. The probe 404s after a successful publish,
so clearPublishProgress leaves isReady false and browser users can't enable
encrypted messaging. Derive the cryptoId (deriveCryptoId(fromBase64(address)))
for the verification fetch. (Codex P1 on tinyhumansai#223.)

Claude-Session: https://claude.ai/code/session_01EJDqbVAWGNdmx9njh4hME8
@senamakel

Copy link
Copy Markdown
Member Author

Thanks for the review. Addressed:

P1 (key-bundle verification 404) — fixed in adb9ca0. verifyKeyBundlePublished now derives the base58 cryptoId (deriveCryptoId(fromBase64(address))) for the probe, matching the publish path — so the web enable flow no longer 404s after a successful publish. Website tsc --noEmit green.

P2 (e2e polls the base58 mailbox) — not an issue for this relay. The backend canonicalizes the recipient on both sides: putMessage stores envelope.To = canonicalCryptoID(...) (base58), and listMessages looks up by canonicalCryptoID(owner). So a reply addressed to Alice's base64 messaging key is stored under her base58 cryptoId, and messages.list(alice.agentId) finds it — the live e2e passes 4/4 runs against the docker-compose backend for exactly this reason. Happy to switch the poll to publicKeyBase64 for symmetry if preferred, but it isn't required for correctness here.

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

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

// it, but the contract requires a valid prepare().
launch: {
displayHarness: "Mock",
binary: "node",

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 Hide the mock harness from launcher discovery

Because bin/tinyplace.mjs discovers installed harnesses by iterating every adapter and running each adapter's launch.binary --version, declaring the mock binary as node makes it look installed on every machine. In an unforced launch, the chooser will offer “Mock” (or auto-select it when no Claude/Codex binary is present), so users can end up in the inert mock TUI even though this adapter is meant to be explicit-only; mark it non-discoverable or skip it unless TINYPLACE_HARNESS=mock/--harness mock was requested.

Useful? React with 👍 / 👎.

…aw address

Follow-up to adb9ca0: verifyKeyBundlePublished now derives the base58 cryptoId
for the getBundle probe, so the unit test must expect that derived id
(deriveCryptoId(fromBase64(address))) rather than the raw base64 address. Fixes
the website "Test" CI job.

Claude-Session: https://claude.ai/code/session_01EJDqbVAWGNdmx9njh4hME8

@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: 278d194d80

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

// the `/` breaks the single-segment `:cryptoId` route match (→ 404) for roughly
// half of all identities. The Signal identity key (base64) still travels in the
// body as `identityKey`. Mirrors the recipient path and the manual publish flow.
const keyPathId = this.signer.agentId;

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 Update all key-route callers to use cryptoId

After publishing under signer.agentId, the rest of the SDK must use that same cryptoId for /keys/:cryptoId routes. I checked the CLI flows and statusFlow still probes ctx.client.keys.health(publicKey) in sdk/typescript/src/cli/workflows.ts:223-226, while raw key-health/prekeys/signed-prekey default to selfPub in sdk/typescript/src/cli/raw.ts:308-320; with the relay semantics described here those calls look in the old base64 bucket, so tinyplace status reports key-health errors and manual pre-key refills rotate/upload keys that peers will not fetch.

Useful? React with 👍 / 👎.

@senamakel senamakel merged commit 0fce176 into tinyhumansai:main Jul 5, 2026
13 of 18 checks passed
senamakel added a commit that referenced this pull request Jul 5, 2026
* feat(plugin): mock harness adapter + mock-LLM responder for headless orchestration tests

Adds a `mock` harness adapter (adapters/mock.mjs) — a deterministic stand-in for
a Claude Code / Codex TUI so the full daemon → route → autoresponder → outbox →
send pipeline can be exercised end to end without a real model or terminal.

- Registered in mcp/harness.mjs but NEVER auto-detected: detectHarness only
  selects it via the explicit TINYPLACE_HARNESS=mock override, so the real
  Claude/Codex paths are untouched. Passes adapter-contract-test.
- The LLM is mocked at the one seam the core exposes: adapter.responder. Instead
  of `claude -p` / `codex exec`, it spawns test/mock-responder.mjs, which parses
  the injected prompt and writes a deterministic canned reply to the agent outbox
  — the same side-effecting path the real `auto_reply` MCP tool takes — so the
  per-agent daemon then encrypts + sends it over Signal. No network, no model.
- test/mock-tui.mjs is a trivial launch stand-in for the launcher recipe.

Full offline plugin suite green (mcp-smoke needs @modelcontextprotocol/sdk
installed; unrelated).

Claude-Session: https://claude.ai/code/session_01EJDqbVAWGNdmx9njh4hME8

* fix(sdk): address relay key routes by base58 cryptoId, not the base64 key

Both sides of the Signal key exchange addressed the relay key routes
(/keys/:cryptoId/{signed-prekey,prekeys,bundle}) with the base64 Ed25519 key.
A base64 key contains `/`, which breaks the single-segment `:cryptoId` route
match and 404s for ~half of all identities (any key whose base64 contains a
slash):

- publishKeyBundle (enableEncryption/publishKeys) intermittently failed to
  publish, so peers could never open an X3DH session with the agent;
- encryptEnvelope's getBundle intermittently failed to fetch a peer's bundle.

Address both by the base58 cryptoId (deriveCryptoId(pubkey) == signer.agentId);
the Signal identity key still travels in the body as `identityKey`. Mirrors the
manual publish/fetch flow that already used agentId.

Verified: 8/8 fresh identities publish + fetch a bundle including 6 whose base64
key contains `/` (previously 404); full SDK vitest suite green (354 passed).

Claude-Session: https://claude.ai/code/session_01EJDqbVAWGNdmx9njh4hME8

* fix(plugin): don't strip base64 address chars from the auto-reply target

respond-batch's safeArg sanitized msg.from (the reply target) with
/[^\w:.-]+/g, on the assumption it was always a base58 agent id. But a messaging
address is the base64 Ed25519 key, which contains `+ / =` — safeArg stripped
those, mangling the address into an unresolvable string, so the headless
autoresponder's auto_reply could never be delivered back to a base64-addressed
sender.

Allow `+ / = @` in the charset (base64 keys, cryptoIds, and @Handles). None can
terminate a double-quoted tool-call argument (only `"`/newline can, and both stay
excluded), so the prompt-injection guard is unchanged.

Surfaced by the new orchestration-live-e2e: the mock agent's reply sat
undeliverable in the outbox until the target address survived intact.

Claude-Session: https://claude.ai/code/session_01EJDqbVAWGNdmx9njh4hME8

* test(plugin): end-to-end orchestration e2e with a mocked LLM

orchestration-live-e2e.mjs drives the WHOLE coding-agent pipe against a real
backend with no model and no terminal: a "human" peer (SDK) sets up a contact
connection and sends a Signal DM; bob's real per-agent daemon under the `mock`
harness drains + decrypts it, routes it headless to the autoresponder, which
spawns the mock responder (canned reply) in place of claude -p / codex exec; the
daemon encrypts + sends the reply back and the human decrypts it. Asserts the
full round trip, that the mock LLM saw the message, and that the relay only ever
held ciphertext.

- test/ORCHESTRATION.md documents the pieces, the data flow, and the human
  connect+send flow (CLI + scripted).
- mock-responder.mjs gains a debug breadcrumb log (respond-batch runs it with
  stdio ignored) for headless troubleshooting.

Green against the docker-compose backend. Run: API_URL=... node
orchestration-live-e2e.mjs (self-links the local SDK).

Claude-Session: https://claude.ai/code/session_01EJDqbVAWGNdmx9njh4hME8

* fix(website): verify key bundle by base58 cryptoId, matching the publish path

Follow-on to the SDK key-routing fix: verifyKeyBundlePublished probed the relay
with the base64 messaging key (`/keys/<base64>/bundle`), but the bundle is now
published under the base58 cryptoId. The probe 404s after a successful publish,
so clearPublishProgress leaves isReady false and browser users can't enable
encrypted messaging. Derive the cryptoId (deriveCryptoId(fromBase64(address)))
for the verification fetch. (Codex P1 on #223.)

Claude-Session: https://claude.ai/code/session_01EJDqbVAWGNdmx9njh4hME8

* update version number

* feat(rust-sdk): add onboard handoff API

* feat(rust-sdk): add OpenHuman compatibility APIs
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