This repository was archived by the owner on Aug 10, 2026. It is now read-only.
feat: permission relay + launchd plist for channel daemon - #3
Merged
Conversation
Permission relay: bridge receives permission_request notifications from Claude Code, forwards them to the daemon's new POST /api/permission endpoint, which sends the prompt to all allowlisted Telegram users. Users reply "yes xxxxx" or "no xxxxx"; the daemon intercepts the verdict pattern before normal message handling, broadcasts a permission_verdict SSE event, and the bridge forwards it back to Claude Code as a notifications/claude/channel/permission notification. Launchd: wrapper script at ~/.parachute/channel-start.sh and plist at ~/Library/LaunchAgents/computer.parachute.channel.plist mirror the vault daemon's pattern — source shell profile for PATH, load .env, exec bun. KeepAlive ensures restart on crash. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This was referenced Apr 10, 2026
This was referenced Jun 19, 2026
unforced
added a commit
that referenced
this pull request
Jun 19, 2026
…aths (PR #3) Six post-audit point-fixes in the load-bearing reply-delivery, claim, and def-lifecycle paths. The audit flagged silent reply-loss as the scariest failure mode for an agent module; each fix has a regression test. FIX 1 (registry.ts) - vault 5xx/network during the outbound write no longer silently loses the reply. The outbound write now gets a BOUNDED retry (2 retries, linear backoff) on a TRANSIENT error (5xx or no-status network), but NOT on a 4xx (a real rejection). On a persistent failure the live view resolves to error (not done) AND the #agent/thread note is RE-RECORDED as status:error carrying the undelivered reply text - so the durable record never falsely claims a clean "ok". The claude -p turn is never re-run (no fork/quota burn); only the idempotent outbound WRITE retries. Backoff base is injectable for fast tests. FIX 2 (programmatic.ts) - mid-turn vault-token expiry: ASSESSED + DEFERRED (documented, no code). The token is minted FRESH per turn at the hub default ~90d TTL, so it cannot expire during a minutes-long turn; and the vault writes happen inside the opaque claude -p subprocess via the token baked into its .mcp.json, so the backend has no in-process seam to observe a 401 and re-mint. A re-mint-on-401 is infeasible at this layer + unnecessary; the real fix (if a long/short-TTL turn ever makes it real) is MCP-client refresh-on-401, flagged as a follow-up. FIX 3 (channel-queue.ts, vault.ts) - claimNext double-claim race. The claim PATCH is now a COMPARE-AND-SWAP: it carries if_updated_at (the note's last-seen updated_at); the vault returns 409 (stale precondition) / 428 (precondition required) when the race is lost, surfaced as a typed InboundClaimConflictError, on which claimNext re-lists and tries the next pending message instead of double-claiming. updated_at is threaded through InboundQueueNote. release/handled/sweep keep last-write-wins (force). FIX 4 (agent-defs.ts) - deleteDef now deletes the vault note FIRST, then deregisters the in-memory agent (mirrors the agent-vaults removal ordering): a vault-delete 502 throws before any teardown, leaving the def REGISTERED (it re-converges on the next poll) instead of orphaned in the confusing gone-from-memory-but-still-in-vault half-state. FIX 5 (agent-defs.ts, daemon.ts) - a grant-reconcile failure on def-delete is no longer silently swallowed. It is still best-effort (does not block the delete), but now warns loudly AND returns a grantsReconciled:false partial- success signal so the delete path/route reports that approved hub grants may be orphaned rather than claiming a clean full success. FIX 6 (vault.ts) - listInboundQueue no longer lets accumulated handled notes crowd pending out of the query cap. handled notes are excluded client-side (only pending + in-flight are the actionable queue) and the vault query is requested newest-first so a hard cap drops the oldest handled notes, never a recent pending. status is not indexable per-vault, so a server-side filter is a future scale optimization. Gates: typecheck 0 errors; bun test 970 pass / 0 fail; vitest 87 pass / 0 fail. Closes #101 Closes #96 Closes #103 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
unforced
added a commit
that referenced
this pull request
Jun 19, 2026
…ount) PR #3 reviewer (af47de6b, LGTM-with-nits) caught the one structural nit in FIX 1: the outbound-failure path called recordThread a SECOND time for the same turn, so single-threaded re-read turn_count=N and wrote N+1 (the turn counted twice), and multi-threaded minted a SECOND per-fire note (fresh uuid) for one turn. Fix: thread a stable per-TURN id (turnThreadId) through every recordThread for the turn, and a `sameTurn` flag on the failure re-record. writeThread then reuses that leaf for multi-threaded (one note, not a duplicate) and, for single-threaded with sameTurn, keeps the existing turn_count instead of incrementing. The note still ends on status:error with the undelivered reply — just counted once. Tests: single-threaded same-turn re-record keeps turn_count==1 + flips to error; multi-threaded re-record reuses the threadId leaf (both writes hit one path). Declined the 3 cosmetic nits (dead camelCase fallback, two comment-only items). Gate: bun run test:all -> typecheck 0, bun 972/0, vitest 87/0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
unforced
added a commit
that referenced
this pull request
Jun 19, 2026
…ering, grant-GC, queue cap (#116) * fix(robustness): harden turn-delivery + claim + def-lifecycle error paths (PR #3) Six post-audit point-fixes in the load-bearing reply-delivery, claim, and def-lifecycle paths. The audit flagged silent reply-loss as the scariest failure mode for an agent module; each fix has a regression test. FIX 1 (registry.ts) - vault 5xx/network during the outbound write no longer silently loses the reply. The outbound write now gets a BOUNDED retry (2 retries, linear backoff) on a TRANSIENT error (5xx or no-status network), but NOT on a 4xx (a real rejection). On a persistent failure the live view resolves to error (not done) AND the #agent/thread note is RE-RECORDED as status:error carrying the undelivered reply text - so the durable record never falsely claims a clean "ok". The claude -p turn is never re-run (no fork/quota burn); only the idempotent outbound WRITE retries. Backoff base is injectable for fast tests. FIX 2 (programmatic.ts) - mid-turn vault-token expiry: ASSESSED + DEFERRED (documented, no code). The token is minted FRESH per turn at the hub default ~90d TTL, so it cannot expire during a minutes-long turn; and the vault writes happen inside the opaque claude -p subprocess via the token baked into its .mcp.json, so the backend has no in-process seam to observe a 401 and re-mint. A re-mint-on-401 is infeasible at this layer + unnecessary; the real fix (if a long/short-TTL turn ever makes it real) is MCP-client refresh-on-401, flagged as a follow-up. FIX 3 (channel-queue.ts, vault.ts) - claimNext double-claim race. The claim PATCH is now a COMPARE-AND-SWAP: it carries if_updated_at (the note's last-seen updated_at); the vault returns 409 (stale precondition) / 428 (precondition required) when the race is lost, surfaced as a typed InboundClaimConflictError, on which claimNext re-lists and tries the next pending message instead of double-claiming. updated_at is threaded through InboundQueueNote. release/handled/sweep keep last-write-wins (force). FIX 4 (agent-defs.ts) - deleteDef now deletes the vault note FIRST, then deregisters the in-memory agent (mirrors the agent-vaults removal ordering): a vault-delete 502 throws before any teardown, leaving the def REGISTERED (it re-converges on the next poll) instead of orphaned in the confusing gone-from-memory-but-still-in-vault half-state. FIX 5 (agent-defs.ts, daemon.ts) - a grant-reconcile failure on def-delete is no longer silently swallowed. It is still best-effort (does not block the delete), but now warns loudly AND returns a grantsReconciled:false partial- success signal so the delete path/route reports that approved hub grants may be orphaned rather than claiming a clean full success. FIX 6 (vault.ts) - listInboundQueue no longer lets accumulated handled notes crowd pending out of the query cap. handled notes are excluded client-side (only pending + in-flight are the actionable queue) and the vault query is requested newest-first so a hard cap drops the oldest handled notes, never a recent pending. status is not indexable per-vault, so a server-side filter is a future scale optimization. Gates: typecheck 0 errors; bun test 970 pass / 0 fail; vitest 87 pass / 0 fail. Closes #101 Closes #96 Closes #103 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(thread): re-record the SAME turn on outbound failure (no double-count) PR #3 reviewer (af47de6b, LGTM-with-nits) caught the one structural nit in FIX 1: the outbound-failure path called recordThread a SECOND time for the same turn, so single-threaded re-read turn_count=N and wrote N+1 (the turn counted twice), and multi-threaded minted a SECOND per-fire note (fresh uuid) for one turn. Fix: thread a stable per-TURN id (turnThreadId) through every recordThread for the turn, and a `sameTurn` flag on the failure re-record. writeThread then reuses that leaf for multi-threaded (one note, not a duplicate) and, for single-threaded with sameTurn, keeps the existing turn_count instead of incrementing. The note still ends on status:error with the undelivered reply — just counted once. Tests: single-threaded same-turn re-record keeps turn_count==1 + flips to error; multi-threaded re-record reuses the threadId leaf (both writes hit one path). Declined the 3 cosmetic nits (dead camelCase fallback, two comment-only items). Gate: bun run test:all -> typecheck 0, bun 972/0, vitest 87/0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
permission_requestnotifications from Claude Code, forwards to daemon's newPOST /api/permissionendpoint, which sends the prompt to all allowlisted Telegram users. Users reply "yes xxxxx" / "no xxxxx"; daemon intercepts the verdict pattern, broadcastspermission_verdictSSE event, bridge forwards back asnotifications/claude/channel/permission. Matches the official telegram plugin's protocol.computer.parachute.channelmirrors the vault daemon's pattern — wrapper script sources PATH + .env, exec's bun.KeepAlive: truefor crash restart.Test plan
POST /api/permissionsends formatted prompt to Aaron's Telegram🤖 Generated with Claude Code