Repository navigation
refactor(herdr): type the herdr RPC seam and route callers through wrappers - #970
Conversation
…wrappers
HerdrMethods (server/herdr/methods.ts) types pane.process_info, agent.get,
pane.send_text and pane.send_keys from the generated schema types; herdrRpc
takes that map first and keeps an untyped fallback that refuses a typed
method name, so { target } to pane.process_info is a compile error.
paneProcessInfo and agentGet wrap the two methods callers asked directly.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e wrappers pi, omp, grok-store, agy-store and codex call paneProcessInfo / agentGet instead of herdrRpc with an inline result type. Same params on the wire, same default timeout, same catch/try at each call site. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Files changed by open PRs stay in HERDR_RPC_ALLOWED with the PR numbers; a stale entry fails too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📝 Summary
Merge Risk: ⚪ Minimal · up to
|
…ing holes
The typed herdrRpc overload now takes one method only (SingleHerdrMethod), so a
union like "pane.process_info" | "agent.get" no longer lets { target } through
as the union of both params. seam.test.ts pins that with ts-expect-error and
lists the two holes that still compile (an explicit result type, a plain-string
method) for the follow-up that splits the untyped fallback out.
conventions.test.ts gains HERDR_RPC_PENDING for server/gjc-status.ts (#928)
and server/index.ts (#859), whose open PRs add a pane.process_info call. The
docs, changelog fragment and comments no longer claim a target call cannot
compile, and "PR B" becomes the follow-up PR that drains the allowlist.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem
server/herdr/client.tsexported an untypedherdrRpc(method: string, params: Record<string, unknown>). Eleven server files called it directly for four methods (pane.process_infox10,agent.getx6,pane.send_textx2,pane.send_keysx1), each with its own inline result type. Nothing stopped{ target }reachingpane.process_info, which herdr then silently answers for the focused pane (the AGENTS.md rule was prose only).Change
server/herdr/methods.ts:HerdrMethods, a{ [method]: { params; result } }map for those four methods. Params and result bodies come from the generated schema types;PaneProcessInfoParams,AgentTargetandPaneProcessInfoare now generator roots (scripts/generate-protocol-types.ts, regeneratedshared/herdr-api.generated.ts). The map itself is hand-written because herdr's schema links a method to its params (request.oneOf) but not to its answer: answers are oneResponseResult.oneOftold apart only by atypetag (pane_process_info,agent_info,ok).herdrRpc<M extends keyof HerdrMethods>(method, params: HerdrMethods[M]["params"], …): Promise<HerdrMethods[M]["result"]>is the first overload. An untyped fallback overload stays for methods the map does not list yet. It refuses a typed method name (UntypedHerdrMethod<M>isneverfor one), soherdrRpc("pane.process_info", { target })is a compile error.server/herdr/seam.test.tspins that with@ts-expect-errorand checks the wire params of the new wrappers against a stand-in socket.paneProcessInfo(paneId)returns one sharedProcessInfo(= generatedPaneProcessInfo: shell_pid, foreground_processes with pid/name/argv0/argv) andagentGet(paneId)returnsAgentInfo. The existingpaneSendText/paneSendKeysnow go through the typed overload. The wrappers call the exportedherdrRpcbinding, so tests thatspyOn(herdr, "herdrRpc")keep working.server/pi.ts,server/omp.ts,server/grok-store.ts,server/agy-store.ts,server/codex.ts(x3). Behaviour is unchanged: the same params go on the wire, with the same default timeout and the same.catch/tryat each call site.codex.tskeeps its oldargv?: string[]reading through a commented cast, because the schema allowsargv: nulland that path has never handled null. Changing that would change behaviour, so it is left for a separate fix.server/conventions.test.ts("herdr socket") now fails when server code outsideserver/herdr/usesherdrRpc. The message names the fix ("add a typed wrapper in server/herdr/client.ts; for pane status use the server/collector.ts listeners") and the allowlistHERDR_RPC_ALLOWED. A stale allowlist entry also fails. Theevents.subscribe/subscribeEventsrules from test: enforce AGENTS.md conventions with repo-wide guard tests #949 were already there; their message now also says whereevents.subscribelives.pane.process_infoline points atpaneProcessInfo/HerdrMethodsand names the enforcing test. Only that one line changes; feat(shortcuts): record and preview custom bindings #812, feat(i18n): add Russian as a UI language #888 and fix(chat): type a message sent during a turn into the agent at once #894 also edit AGENTS.md.Allowlist (
HERDR_RPC_ALLOWED, migrated in PR B)server/conversation.tsserver/gjc-runtime.tsserver/hermes.tsserver/omo.tsserver/shell-agent.tsBuilt from
gh pr list --state open --limit 100 --json number,fileson 2026-10-14.Known limits (closed in PR B)
herdrRpc. Renaming it toherdrRpcUnsafewould touch about 33 test files and 30scripts/harnesses, several of them in open PRs (api.contract, gjc.contract, omo.contract, ui-regression, pending-input…). Tests andscripts/QA harnesses are outside this guard's scope: they drive herdr (pane.report_agent,workspace.create, …) and are not part of the bridge seam.herdrRpc<Inline>("pane.process_info", …), still reaches the untyped fallback. That is the form the allowlisted files use, and they may not change here. In server code the source guard closes this gap. The fallback goes away once the allowlist is empty.Validation
bun run check fast: green. A first run had one failure inserver/portal.test.ts("does not treat an arbitrary interpreter as a Portal script": the process argv read raced and returned undefined). This change does not touch that file, it passed alone, and the rerun was fully green.bun run test:integration: 455 pass, 0 fail, 14 skip in 38 files (includes the omp, codex, codex-binding, pi, gjc, omo and hermes contract tests).herdr.herdrRpcuse inserver/pi.ts.Follow-up: PR B
Once #869, #885, #910, #912 and #923 land: migrate the five allowlisted files to
paneProcessInfo/agentGet/paneSendText/paneSendKeys, emptyHERDR_RPC_ALLOWED, drop the untyped fallback overload (or rename itherdrRpcUnsafefor tests and scripts), and consider handlingargv: nullincodex.ts.Review
Fixed in 290d5f1:
SingleHerdrMethod<M>, soherdrRpc(cond ? "pane.process_info" as const : "agent.get" as const, { target })no longer compiles.seam.test.tspins this with a@ts-expect-errorline. Checked with tsc: the inferredherdrRpc("pane.process_info", { pane_id })still returns the typed result.conventions.test.tsaddsHERDR_RPC_PENDINGwithserver/gjc-status.ts(fix(gjc): derive status from the native parent turn #928) andserver/index.ts(feat(hermes): add safe Chat interaction #859). Those PRs add apane.process_infocall, but neither file calls herdrRpc on main yet, so the "no longer calls" check skips them. It would be better for both PRs to switch topaneProcessInfo(paneId), which is a one-line change in each. After that, these entries come out.client.tsand theUntypedHerdrMethodcomment inmethods.tslist the holes.Deferred to the follow-up PR that drains
HERDR_RPC_ALLOWED:herdrRpc<T>("pane.process_info", { target })andherdrRpc(m as string, { target })still compile. With an explicit T, the fallback'sMstays atstring, and that cannot be closed while the fallback shares the name. Closing it means splitting the fallback out asherdrRpcUntyped(orherdrRpcUnsafe) and moving about 400 test and script call sites onto it. It also means converting theherdrRpc<T>("pane.process_info" | "agent.get", …)calls in the five allowlisted files, which conflicts with their open PRs.seam.test.tskeeps both forms as compiling lines with a comment, so the follow-up can turn them into@ts-expect-error.{ target }to pane.process_info asnot assignable to parameter of type 'never'instead of naming the wrong param. That message only gets better with the split.HERDR_RPC_PENDINGentry on its own. If fix(gjc): derive status from the native parent turn #928 or feat(hermes): add safe Chat interaction #859 switches topaneProcessInfoor closes, its entry stays until someone takes it out by hand.herdrRpc<T>("pane.process_info", …). They should hear about the switch topaneProcessInfobefore the split lands. I have not commented on those PRs.Checks:
bun run check fastpassed (typecheck, build, 3452 unit tests).bun run test:integrationpassed (455 pass, 0 fail).🤖 Generated with Claude Code