Repository navigation
feat(tapd): long-lived daemon + web UI, with CLI/Hermes/OpenClaw as thin clients - #80
Conversation
Designs a long-lived tapd daemon that owns transport across all hosts and a Next.js static-export web UI that renders agent-to-agent conversations as a Slack-style chat with inline action cards. Refactors OpenClaw plugin and Hermes daemon to be thin clients of tapd while preserving all current user behavior. Documents v2 storage substrate work (SQLite + event log unification + channel primitive) as the next spec to write. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
20-task TDD plan for building packages/tapd: workspace scaffold, event bus with ring buffer, runtime wrapper bridging emitEvent to typed events, HTTP server with unix socket + localhost TCP dual bind, bearer token auth, all REST routes (identity/contacts/ conversations/pending/notifications/daemon-control), SSE event stream with Last-Event-ID replay, daemon lifecycle and bin entrypoint, plus end-to-end and SSE replay integration tests. No user-facing changes; the daemon binary stands alone and nothing else in the repo references it yet. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
13-task plan for packages/ui: Next.js 15 static export, Tailwind + hand-written shadcn primitives, typed REST/SSE clients, sidebar and chat thread components matching the chat-metaphor mockup, SWR + EventSource wiring, tapd-side static asset serving, and Playwright golden-path end-to-end test against an in-process tapd seeded with one contact and one conversation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Native browser EventSource cannot set custom headers, so the bearer token must travel via query string for any client that uses it. The auth check still uses constant-time comparison against the same expected token, so there is no security regression — the query string is just an alternative carrier for the same secret. The Last-Event-ID fallback mirrors the same constraint for resume-on-reconnect.
…s (PORT_FILE, PID_FILE, TOKEN_FILE) across 5 files into 3 canonical exports from tapd/config.ts, extracted shared response base in connect.ts to deduplicate 2 identical success() calls, and extracted requireTimestamp() helper in sqlite-logger.ts to collapse 3 repetitive timestamp validation blocks (-21 net lines).
…emitActionCompleted, emitActionFailed) in TapMessagingService, replacing 6 remaining inline event-construction blocks with named methods that centralize each event type's schema in one place — completing the emit helper suite started in iteration 9.
…rings with TAPD_PORT_FILE/TAPD_PID_FILE/TAPD_TOKEN_FILE constants in daemon-start.ts, daemon-restart.ts, and the in-process-tapd test helper, and extracted getQueryParam() helper in auth.ts to deduplicate the 6-line query-string parsing pattern between auth.ts and sse.ts (-7 lines in sse.ts).
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b0536bbb20
ℹ️ 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 CLI imports types from `trusted-agents-tapd` (TAPD_PID_FILE, TAPD_PORT_FILE, etc.), but the root typecheck script only ran `packages/tapd typecheck` (tsc --noEmit) without building. Without tapd's `dist/*.d.ts`, CLI typecheck failed with TS2307 "Cannot find module 'trusted-agents-tapd'". Added `packages/tapd build` after tapd typecheck, matching the same pattern already used for core and sdk. Fixed 5 spell-check errors (crate-ci/typos): `unparseable` → `unparsable`, `mis-shaped` → `malformed`, `mis-ordered` → `out-of-order`, `mis-render` → `render... incorrectly`, and renamed test IDs `conv-noo`/`conn-noo` → `conv-no-offset`/`conn-no-offset` for clarity. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6149b11ac3
ℹ️ 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".
CI blocker: semantic merge conflict with `main`After the typecheck ordering fix in 6149b11, CI gets past the previous `trusted-agents-tapd` module error and now surfaces a deeper issue: ``` Why this is happening GitHub tests `refs/pull/80/merge` (a virtual merge of this branch into `main`). That virtual merge contains:
Neither side touches the same file, so Git doesn't flag a text conflict — but the combined tree won't typecheck. Proposed fix (mechanical) When `main` is merged into this branch, update `status.ts` to import the underlying function directly from `hermes/config.ts`, which is still exported on the branch: ```ts // After On `main`, `readHermesTapDaemonState` is a one-line wrapper around exactly that function, so this is a no-op behavior change that removes a dead indirection. The other export from `hermes/client.ts` (`sendHermesTapDaemonRequest`) is obsolete in the new tapd architecture — the CLI now talks to the tapd HTTP client via `lib/tapd-client.ts` instead of the old Hermes Unix-socket IPC. No consumer of `sendHermesTapDaemonRequest` remains once `status.ts` is updated. I'm skipping this fix in this babysit iteration because:
Leaving this for the maintainer to decide — merge with the fix above, or rebase onto `main` and include the `status.ts` update. |
PR #81 added tap status on main importing readHermesTapDaemonState from ../hermes/client.js, but this branch removed hermes/client.ts when folding Hermes's standalone daemon into tapd. That wrapper was one line over loadTapHermesDaemonState, which is still exported from hermes/config.ts, so swap the import and call site. Pure rename, no behavioral change to tap status. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ermes identity scoping - openclaw-plugin: start the escalation watcher unconditionally in the plugin service `start`. The watcher already has a built-in reconnect loop (escalation-watcher.ts handleEnd); gating it on a one-shot tapd health check meant Gateway booting before tapd left the agent blind to escalations until the plugin service was restarted. - cli hermes aliases: `tap hermes status/sync/restart` were parsing `--identity` and `--hermes-home` and throwing them away, so multi-identity Hermes setups always hit the default data dir. Resolve the identity via loadTapHermesPluginConfig and scope the forwarded GlobalOptions.dataDir to the selected identity before dispatching to the underlying daemon/sync command. Architecturally: identity lookup is a CLI-side concern (fat CLI, thin plugin — CLAUDE.md), and the alias commands stay thin wrappers over daemon-status / message-sync / daemon-restart. Addresses codex bot review comments #3114838436 and #3114838438. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
spawnTapdDetached removed the pidfile on any catch, including the waitForPortFileAndLiveness timeout path where the child process is still alive. That left a detached tapd running with no pidfile pointing at it: tap daemon stop/restart could no longer control it, and the next tap daemon start could unlink its port file mid-flight while it was still serving. SIGKILL the child first when it's still alive, then clean up. Addresses codex bot review comment #3114838432. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 834ab46bb9
ℹ️ 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".
… of silent outage Both the OpenClaw plugin (tap_gateway) and the Hermes Python client had `action="restart"` wired to `/daemon/shutdown` with no respawn path. In those contexts — thin transport-active adapters per CLAUDE.md §16 — neither process can spawn tapd back up, so what looks like a recovery action silently leaves tapd down: OpenClaw Gateway has no supervisor for tapd, and Hermes notification drain runs with auto_start=False so drain stops until the next user action. Rather than pretend to restart, both surfaces now refuse the action and point the operator at `tap daemon restart` / `tap hermes restart` on the CLI, which actually does stop+start. Keeps the thin-plugin rule intact (no new spawn paradigm in either adapter) and turns a silent outage into a visible "wrong tool" error. Addresses codex bot review comments #3117241110 and #3117241114. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3727c77911
ℹ️ 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".
…rt behavior 3727c77 made the restart action throw with operator guidance instead of dispatching to client.shutdown(). The dispatch-table test was still asserting the old behavior, so it failed in CI. Drop the old assertion and add a focused test that restart rejects with the `tap daemon restart` hint while never touching client.shutdown. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a0455e29c6
ℹ️ 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".
- tool.test.ts: biome wanted the single-arg rejects.toThrow call on one line; collapse it. This fixes the CI lint failure from a0455e2. - ui.ts: `tap ui` was echoing a URL containing the bearer token to stdout via info() AND including it in the structured success() payload. The hash-based token placement only protects against browser history / referrer leaks — terminal logs, --json capture, CI collectors are separate attack surfaces, and that token authorizes the daemon control and write APIs. Now hand the token-bearing URL directly to openInBrowser(), echo only the base URL on the happy path, and only surface the full URL when auto-open fails (the user has no other way to reach it). - escalation-watcher.ts: `stop()` set `stopped=true` permanently, and `start()` bailed if that flag was true, so one stop disabled the watcher for the life of the instance. The plugin service lifecycle can call start() after stop() on a Gateway reload without process replacement, which would silently leave escalations dark. `stopped` is now a signal to the reconnect loop, not terminal state: start() resets it and reconnects. - notification-queue.ts: the queue had no upper bound. If /api/notifications/drain wasn't being called (consumer offline), memory grew without limit. Added a maxSize cap (default 1000, matching the event bus ring-buffer shape) with FIFO eviction on overflow. Addresses codex bot review comments #3117308353, #3117308361, #3117308367 (and the CI lint regression from a0455e2). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…, 4xx for client errors
- client.py resolve_pending: the verb was derived via
`bool(params.get("approve"))`, so a missing approve silently coerced
to `/deny` and could reject a pending action the caller never meant
to reject. Require `approve` to be an explicit boolean and return a
validation error otherwise.
- ui.ts openInBrowser: `spawn()` command-lookup failures are emitted on
the child as an async `"error"` event, not caught by the surrounding
try/catch. On systems without `open`/`xdg-open` the process would
crash from an unhandled error after `tap ui` had already returned.
Attach a no-op `error` handler so we stay crash-safe; return value
stays optimistic since the caller's fallback messaging already tells
the user how to reach the URL manually.
- server.ts handle(): the catch-all mapped every async rejection to
HTTP 500, so malformed JSON bodies and payload-too-large came back
as internal_error and triggered client retry behavior that would
never succeed. Introduced HttpError (status + code) and plumbed it
through readJsonBody (400 invalid_json, 413 payload_too_large);
unknown exceptions still fall through to 500 because those are
genuine server bugs.
Addresses codex bot review comments #3117358151, #3117358156,
#3117358161.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ec7c53a090
ℹ️ 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".
…mapping Per review comment 3117464873: ec7c53a surfaced 4xx for readJsonBody errors but left requireParam/requireBody throwing plain Error, which fell through to the catch-all 500 path and reported client input mistakes as internal_error. Throw HttpError(400, ...) from the two validation helpers so the server's top-level catch maps them to their intended 4xx status and caller retry logic stays correct. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e771f97684
ℹ️ 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".
Completes the 4xx-mapping sweep from e771f97: - invites route: parseInviteBody throws HttpError(400, invalid_body / invalid_param) for non-object bodies and bad expiresInSeconds values (review comment 3119936389). - contacts-write revoke: unknown connectionId throws HttpError(404, contact_not_found) so stale UI/CLI state surfaces as a 4xx instead of an internal_error 500 (review comment 3119936394). - router: decodeURIComponent on route params now throws HttpError(400, malformed_param) on URIError, so malformed percent-encoding in a path no longer leaks as an internal server fault (review comment 3119936397). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0764116d6a
ℹ️ 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".
…y, spawn race hardening
Ships three review-driven changes that had been queued in the worktree:
1. Unix-socket migration + per-dataDir auth token
- tapd drops the TCP-bypass shortcut; the Unix socket requires the same
bearer token as the TCP listener. Token is persisted to `.tapd-token`
next to the socket so CLI/Hermes/OpenClaw clients can read it once at
startup instead of fishing it out of the pidfile.
- Default TCP port switches to 0 (OS-assigned ephemeral) so multi-
identity Hermes setups stop colliding on :6810. The bundled web UI
reads the bound port from `.tapd.port` the same way.
- `TapdConnectionInfo` is now `{ socketPath, token }`; `discoverTapd` no
longer returns a baseUrl. A dedicated `discoverTapdUiUrl` returns the
TCP URL for `tap ui` and `tap daemon status`.
2. Meetings route — single flat body shape (comments #3119988721, #3119988723)
- Collapses the dual-shape (full SchedulingProposal vs flat kwargs) POST
body into a single flat shape. All clients send the same thing now.
- `respond` throws HttpError(404) for unknown schedulingId so stale CLI
state surfaces as a 4xx rather than the catch-all 500.
3. tapd-spawn race hardening (comments #3081753465, #3081753473)
- Atomic `wx` pidfile write closes the check-then-act window between
racing `tap daemon start` calls.
- Pidfile now carries `{ pid, binPath, ownerToken }`; a UUID owner token
is embedded in the child's argv so `inspectTapdProcess` can confirm
the live pid still belongs to our tapd (via `/proc/<pid>/cmdline` on
Linux, `ps -o command=` on macOS).
- `daemonStartCommand` blocks only when the owner is actually alive;
stale port/pid files are cleaned up and the start proceeds.
Comment #3081753468 (Windows package-dir) is addressed by switching the
resolver to `dirname` from `node:path`, which handles the platform the
host runs on. Explicit Windows support (PowerShell command-line lookup,
win32 path normalization) is intentionally out of scope — tap targets
Linux + macOS and keeping the Windows branches would add surface area for
no supported use case.
Reactive mode for Claude Code (new SKILL.md section, `--filter`/`--quiet`
on `tap message listen`, auto-reconnect cursor) is deferred to a follow-up
PR; it is a net-new feature rather than a review fix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8dc474a1c8
ℹ️ 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".
… only Per review comment 3120267621: `authorizeRequest` accepted `?token=...` for every route/method, which widened the blast radius of any token leak (shell history, referrer, log capture). A cross-site form POST to `/daemon/shutdown?token=...` would bypass CORS preflight because the request would carry no custom `Authorization` header. Restrict the query-token fallback to `GET /api/events/stream` (the only route whose client — the browser's native `EventSource` — literally cannot set a header). Everything else now requires header-based auth. Also: - `authorizeRequest` takes `method` and `requestPath` from the caller rather than re-parsing the URL; `TapdHttpServer.handle` already has both. - Test coverage updates to reflect the narrower contract: the SSE route still accepts the query token, `/api/identity` and `/daemon/shutdown` reject it with 401 even when the value is correct. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6ab94eeb35
ℹ️ 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".
…ng success Per review comment 3117464878: openInBrowser returned true synchronously after child_process.spawn, but command-lookup failures on POSIX arrive asynchronously as an "error" event on the child. On systems without open/xdg-open the command optimistically reported success and the token-bearing URL (intentionally suppressed from stdout/--json on the happy path, see 3117308353) was lost to the user. Make openInBrowser async and wait BROWSER_OPEN_ERROR_WINDOW_MS (250ms) for an error event before resolving true. Fast enough to feel instant, long enough to catch ENOENT in practice; anything slower is out of scope (we only care about "did the opener process launch"). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
trusted-agents/packages/openclaw-plugin/src/tool.ts
Lines 176 to 177 in d7ff06e
tap_gateway forwards request_funds with chain/toAddress left undefined when the model omits them, but tapd validates both fields as required in /api/funds-requests (isFundsRequestBody requires non-empty chain and toAddress). In practice this turns an advertised optional-input flow into a 400 failure path for OpenClaw users unless they always supply both fields manually; this should be defaulted in the tool layer (as the CLI does) before calling tapd.
trusted-agents/packages/cli/src/commands/status.ts
Lines 299 to 305 in d7ff06e
tap status
tap status still reads message/conversation summaries from <dataDir>/conversations/*.json, but this change moved runtime logging to conversations.db via SqliteConversationLogger (and migration renames legacy JSON to conversations.bak*). On migrated installs, this path returns ENOENT and the command reports zero inbound/outbound counts and no last-message data even when conversations exist, so status output becomes misleading for active agents.
ℹ️ 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".
Fixes #32
Adds
tapd, a long-lived TAP daemon that owns the XMTP transport, runtime state, and conversation storage behind an HTTP API over a Unix socket. The CLI, Hermes plugin, and OpenClaw plugin are now all thin clients that forward commands to tapd, so only one process holds the transport-ownership lock at a time and the three surfaces stay in sync by construction instead of manual feature parity.The daemon also serves a Next.js web UI from
/for browsing conversations, approving pending actions, and watching a live SSE event feed (bearer token passed via URL hash). Conversation logs move from per-file JSON into a single SQLite store with a one-shot migration on first startup and canonical timestamps so lexicalORDER BYis safe.The standalone Hermes sidecar daemon is deleted — the Hermes hook now starts tapd and the plugin speaks to it over the socket. The OpenClaw plugin loses its own notification queue, event translator, and main-session state machine, and replaces them with an SSE escalation watcher against tapd. Core gains a typed
TapEventdiscriminated union emitted at every state transition so tapd can classify notifications centrally.Seventeen simplification passes on top (
gnhf #2–#18) extract helpers, collapse duplicate validators and well-known file-name constants, centralize event construction inTapMessagingService, and trim verbose patterns. Those commits were independently reviewed and all kept.Test plan
bun run lintbun run typecheckbun run testtap daemon start→tap connect→tap message sendround-trip against a real peertap ui, verify SSE event feed and approve a pending action from the web UItap_gateway send_messageround-trip[TAP Notifications]injected on next agent turnNote
Medium Risk
Introduces new workspace packages (
trusted-agents-tapd,trusted-agents-ui) and a native SQLite dependency (better-sqlite3), which can impact runtime behavior and installation/build reliability across platforms.Overview
Adds new workspaces for a daemon (
trusted-agents-tapd) and web UI (trusted-agents-ui), and bumps the repo package versions to0.2.0-beta.6with updated dependency graph.Updates conversation log persistence to use a single SQLite database (
conversations.db) viabetter-sqlite3, including a migration flow that moves legacy per-peer JSON logs into SQLite and leavesconversations.bak/backups on success. Also expands.gitignoreto exclude additional local/dev artifacts (e.g..superpowers/,.superset/,__pycache__/).Reviewed by Cursor Bugbot for commit d7ff06e. Bugbot is set up for automated code reviews on this repo. Configure here.