20 KiB
issue, issue_title
| issue | issue_title |
|---|---|
| 531 | pi-permission-system: remove the deprecated permissions:rpc:check / permissions:rpc:prompt event-bus channel |
Remove the deprecated permissions:rpc:check / permissions:rpc:prompt event-bus channel
Release Recommendation
Release: ship independently
This is Phase 8 Step 7, tagged Release: independent in the roadmap and explicitly called out as "ships as its own major-bump release, separate from the other Phase 8 batches."
It is a breaking change for event-bus RPC consumers, so it cuts its own feat(pi-permission-system)!: major release rather than batching with the yolo-recorded-authority batch (Steps 2–3, already shipped) or the auto-batching refactors (Steps 5, 6, 8).
Problem Statement
The package exposes two cross-extension mechanisms for the same jobs: a Symbol.for()-backed PermissionsService (the modern, synchronous, type-safe path) and an event-bus RPC channel pair (permissions:rpc:check for policy queries, permissions:rpc:prompt for prompt forwarding).
The RPC check channel and its request/reply types are already marked @deprecated in favor of the service accessor.
The RPC prompt handler is a third, parallel elicitation path — with its own hasUI check, its own review-log entry, and its own UI-prompt event — alongside the gate prompt and the file-based forwarded inbox.
The Phase 9 authority spine would otherwise have to adapt all three elicitation paths.
Removing the RPC channel now collapses the cross-extension surface to a single service accessor and narrows the spine's scope from three elicitation paths to two.
Goals
- Delete the entire event-bus RPC subsystem: both the
permissions:rpc:checkandpermissions:rpc:prompthandlers. - Remove the RPC request/reply payload types, channel constants, the shared
PermissionsRpcReplyenvelope, and the RPC-onlyPERMISSIONS_PROTOCOL_VERSIONfrom the public event contract. - Remove the now-dead
rpc_promptUI-prompt source and itsbuildRpcUiPromptbuilder. - Unwire RPC registration from
index.tsand drop the two RPC unsubscribe handles from thePermissionServiceLifecyclesubscription list. - Repoint the cross-extension docs exclusively at the
Symbol.for()service accessor. - Mark Phase 8 Step 7 complete in
docs/architecture/architecture.md. - Comment on #309 to record that this narrows its scope to the service bash path only.
- Breaking for any external event-bus RPC consumer — carried as
feat(pi-permission-system)!:with aBREAKING CHANGE:footer.
Non-Goals
- The Phase 9 authority spine itself (the
Authorizerinterface and its implementations). This step only removes friction in its way. - The file-based forwarded inbox (
src/authority/escalation/serving machinery). It is a distinct elicitation path and stays; the RPC prompt channel is a separate, parallel path. - The
Symbol.for()service accessor (PermissionsService) and its broadcast channels (permissions:ready,permissions:ui_prompt,permissions:decision). These are the surviving cross-extension surface. - Editing the frozen phase-history docs (
docs/architecture/history/phase-4-constructibility.md,phase-7-accesspath-universal-representation.md). They record past-phase state accurately and are intentionally left unchanged; their RPC references describe what was true then. - Resolving #309 itself — this plan only leaves a scope-narrowing comment on it.
Background
Relevant modules (all under packages/pi-permission-system/):
src/permission-event-rpc.ts— the whole RPC subsystem:registerPermissionRpcHandlersregisters bothpermissions:rpc:check(routes policy queries through the sharedPermissionResolveras anaccess-pathintent, #503) andpermissions:rpc:prompt(forwards a prompt to the parent UI). Both handlers are removed with the file.src/permission-events.ts— the public event contract: channel-name constants,PERMISSIONS_PROTOCOL_VERSION, thePermissionsRpcReplyenvelope, the RPC request/reply types, plus the surviving broadcast types (PermissionsReadyEvent,PermissionUiPromptEvent,PermissionDecisionEvent) and emit helpers.src/service.ts— thepackage.jsonexportsentry point; re-exports a subset ofpermission-events.tssymbols, including the RPC prompt channel, the RPC prompt types,PermissionsRpcReply, andPERMISSIONS_PROTOCOL_VERSION.src/permission-ui-prompt.ts— centralizedpermissions:ui_promptpayload construction;buildRpcUiPrompt/RpcPromptInputbuild therpc_prompt-source event, called only by the RPC prompt handler.src/authority/forwarding-io.ts—UI_PROMPT_SOURCESwhitelists valid persisted prompt sources for tolerant forwarded-request reads; includes"rpc_prompt".src/index.ts— the composition root; constructs the resolver, callsregisterPermissionRpcHandlers, and threadsrpcHandles.unsubCheck/rpcHandles.unsubPromptintoPermissionServiceLifecycle.src/service-lifecycle.ts—PermissionServiceLifecyclereceives the subscription list as an opaquereadonly (() => void)[]; it has no direct RPC reference, so "unwire fromPermissionServiceLifecycle" means dropping the two handles at theindex.tsconstruction site, not editing this file.
Tests: test/permission-event-rpc.test.ts (deleted whole), test/permission-events.test.ts (RPC type/constant blocks), test/permission-ui-prompt.test.ts (buildRpcUiPrompt block), test/composition-root.test.ts (the RPC-check arm of the single-source-of-truth test).
Docs: docs/cross-extension-api.md (RPC sections + event tables), docs/architecture/architecture.md (cross-extension paragraph, directory tree, Phase 8 Step 7 marker + Mermaid node + metric row).
Constraints from AGENTS.md and the package skill:
- Removing a public export breaks every importer at the type level in the same commit — fold the export removal,
service.tsre-export drop, and all consumer-test updates into one atomic step. - The
package.jsonexportsfield points atsrc/service.ts; whatever it re-exports is the public cross-extension API. - Mark the roadmap step ✅ in the implementation doc-update commit (
✅on both the step heading and its Mermaid node, plus any stale metric row), not a deferred/ship-issuecommit. - Use
feat(pi-permission-system)!:—!after the scope — with aBREAKING CHANGE:footer separated from the body by a blank line. - Do not put
Closes #531in the commit; reference asRefs #531.
Design Overview
This is a pure subtractive change: no new collaborator, no new parameter, no widened interface. A design-review pass finds nothing to fix — removing the RPC subsystem strictly narrows the cross-extension surface and removes one elicitation path. The only judgment calls are how far the dead-code removal cascades, resolved below.
What is removed vs. what survives
The surviving cross-extension surface after this change:
PermissionsServiceviagetPermissionsService()(theSymbol.for()accessor) — policy queries and prompt-relevant registration.- Broadcast channels:
permissions:ready,permissions:ui_prompt,permissions:decision(fire-and-forget observation). - The file-based forwarded inbox (subagent → parent prompt forwarding) — unchanged.
Everything RPC-specific is removed:
- Channel constants
PERMISSIONS_RPC_CHECK_CHANNEL,PERMISSIONS_RPC_PROMPT_CHANNEL. - Request/reply types
PermissionsCheckRequest,PermissionsCheckReplyData,PermissionsPromptRequest,PermissionsPromptReplyData. - The RPC envelope
PermissionsRpcReply<T>. PERMISSIONS_PROTOCOL_VERSION— an RPC-only concept. The surviving broadcasts explicitly carry noprotocolVersion; their contract is "the published types plus package semver" (documented inpermission-events.tsandcross-extension-api.md). With no RPC envelope, the version constant has no remaining reader.
Dead-code cascade (removed, dictated by code-design)
Once both RPC handlers are gone, three symbols become dead and are removed in the same change rather than left as orphans:
buildRpcUiPrompt/RpcPromptInput(permission-ui-prompt.ts) — sole caller was the RPC prompt handler."rpc_prompt"member of thePermissionUiPromptSourceunion (permission-events.ts) — only the RPC prompt handler ever emitted apermissions:ui_promptevent with this source. The file-based forwarded inbox never persisted"rpc_prompt"(forwarded requests are written byApprovalEscalator, which never used the RPC path), so narrowing the union cannot orphan a persisted request."rpc_prompt"entry inUI_PROMPT_SOURCES(forwarding-io.ts) — the tolerant-read whitelist for persisted forwarded sources; safe to drop for the same reason.
Narrowing the public PermissionUiPromptSource union is itself a breaking type change, but it is subsumed by the major bump this change already requires, and it is correct: no runtime permissions:ui_prompt event will carry "rpc_prompt" after the handlers are gone.
permission-events.ts doc touch-up
The PermissionsReadyEvent doc comment currently reads "Version negotiation lives in the RPC envelope (PermissionsRpcReply)."
With the envelope removed, reword it to state the broadcast contract is defined by the published types plus package semver (matching the existing ui_prompt/decision comments), with no reference to a removed symbol.
Migration note (verified against the real surface)
The RPC check consumer migrates to the service accessor.
Verified against the real PermissionsService interface in src/service.ts:
const { getPermissionsService } = await import("@gotgenes/pi-permission-system");
const service = getPermissionsService();
if (service) {
const result = service.checkPermission("bash", "git push");
// result: PermissionCheckResult { state, matchedPattern, origin, ... }
}
checkPermission(surface, value?, agentName?) is the replacement for permissions:rpc:check.
There is no service-accessor replacement for permissions:rpc:prompt — prompt forwarding is an internal subagent→parent mechanism (the file-based inbox), not a public cross-extension operation.
The migration note must say the prompt-forwarding RPC is removed with no public replacement, rather than inventing one.
Module-Level Changes
Deleted:
src/permission-event-rpc.ts— the whole RPC subsystem.test/permission-event-rpc.test.ts— its test.
src/permission-events.ts:
- Remove
PERMISSIONS_PROTOCOL_VERSION. - Remove
PERMISSIONS_RPC_CHECK_CHANNEL,PERMISSIONS_RPC_PROMPT_CHANNEL. - Remove
PermissionsRpcReply,PermissionsCheckRequest,PermissionsCheckReplyData,PermissionsPromptRequest,PermissionsPromptReplyData. - Remove the
"rpc_prompt"member from thePermissionUiPromptSourceunion. - Reword the
PermissionsReadyEventdoc comment to drop thePermissionsRpcReplyreference.
src/service.ts:
- Drop
PermissionsPromptReplyData,PermissionsPromptRequest,PermissionsRpcReplyfrom theexport type { … }block. - Drop
PERMISSIONS_PROTOCOL_VERSION,PERMISSIONS_RPC_PROMPT_CHANNELfrom theexport { … }block.
src/permission-ui-prompt.ts:
- Remove
RpcPromptInputandbuildRpcUiPrompt.
src/authority/forwarding-io.ts:
- Remove
"rpc_prompt"from theUI_PROMPT_SOURCESarray (and confirm thesatisfies readonly PermissionUiPromptSource[]assertion still holds against the narrowed union).
src/index.ts:
- Remove the
import { registerPermissionRpcHandlers } from "./permission-event-rpc". - Remove the
const rpcHandles = registerPermissionRpcHandlers(...)call and its explanatory comment (the resolver is still constructed — only its RPC consumer is removed; adjust the comment that says "the RPC and service route their policy queries through it" to name only the service). - Change the
PermissionServiceLifecyclesubscription list from[rpcHandles.unsubCheck, rpcHandles.unsubPrompt, unsubSubagentLifecycle]to[unsubSubagentLifecycle].
Tests:
test/permission-events.test.ts— remove thePERMISSIONS_PROTOCOL_VERSION is 1test; remove the two RPC channel-name assertions from thechannel namestest (keep the surviving broadcast channel assertions); remove thetype shapes (PermissionsRpcReply),PermissionsCheckRequest,PermissionsCheckReplyData,PermissionsPromptRequest,PermissionsPromptReplyDatadescribe blocks and their imports.test/permission-ui-prompt.test.ts— remove thebuildRpcUiPromptdescribe block and its import.test/composition-root.test.ts— remove the RPC-check arm of the single-source-of-truth test (thePERMISSIONS_RPC_CHECK_CHANNELemit/reply assertions and the import) while keeping thegetPermissionsService()!.checkPermission("demo")assertion that the same test makes; that service-accessor assertion still proves session-approval visibility, so the test's purpose survives.
Docs:
docs/cross-extension-api.md— remove the "Policy Query RPC (deprecated)" and "Prompt Forwarding RPC" sections; remove the four RPC rows from the event-bus channel table; remove thePERMISSIONS_PROTOCOL_VERSIONparagraph and the ping-style-RPC-readiness paragraph; drop"rpc_prompt"from thesourcefield description; reword the opening "Event bus — broadcasts and RPC" framing to "broadcasts only" and point the "how to query policy" guidance exclusively at the service accessor.docs/architecture/architecture.md— update the cross-extension paragraph (drop the "event-bus RPC remains as a zero-dependency fallback" and "permissions:rpc:promptremain on the event bus" sentences); remove thepermission-event-rpc.tsline from the directory-tree listing and adjust thepermission-events.tsline if it enumerates RPC types; mark Phase 8 Step 7 ✅ on its heading and on theS7Mermaid node; flip the "Elicitation paths the spine must adapt" metric row (3 → 2) to done.
Not changed (verified — no stale references remain):
docs/architecture/history/*.md— frozen phase history, intentionally unchanged (see Non-Goals).README.md,docs/configuration.md— grep confirms no RPC references.schemas/permissions.schema.json,config/config.example.json— RPC is not a config surface.- No other package in the monorepo imports any RPC symbol (grep of
packages/confirms zero external consumers).
Test Impact Analysis
This is a removal, not an extraction, so the analysis is inverted — what tests go away, and what must be preserved:
- New tests enabled: none. Removal enables no new lower-level test surface.
- Tests that become redundant / removed:
test/permission-event-rpc.test.ts(whole file — it exercises only the deleted handlers); the RPC type-shape and constant blocks intest/permission-events.test.ts; thebuildRpcUiPromptblock intest/permission-ui-prompt.test.ts; the RPC-check arm of the composition-root single-source-of-truth test. - Tests that must stay as-is: the surviving broadcast tests in
test/permission-events.test.ts(emitReadyEvent,emitUiPromptEvent,emitDecisionEvent, the ready-event wiring test); thebuildDirectUiPrompt/buildForwardedUiPromptblocks intest/permission-ui-prompt.test.ts; the session-approval assertion viagetPermissionsService()in the composition-root test (it must keep proving session rules reach the surviving service path).
No regression test is added for "the RPC channel no longer replies" — asserting the absence of a deleted handler tests nothing meaningful; the deletion is proven by the removed handler tests and a green suite.
Invariants at risk
- Single-source-of-truth for tool policy (#296 / composition-root test).
The composition-root test currently proves session approvals reach both the RPC channel and the service accessor.
After removing the RPC arm, the service-accessor assertion must remain and continue to prove the invariant.
Pinned by the surviving
getPermissionsService()!.checkPermission("demo")assertion in that test — no new test needed. permissions:ui_promptbroadcast contract. NarrowingPermissionUiPromptSourcemust not break the survivingbuildDirectUiPrompt/buildForwardedUiPromptbuilders orforwarding-io.ts's tolerant read. Pinned by the retained builder tests and bytsc(thesatisfies readonly PermissionUiPromptSource[]assertion onUI_PROMPT_SOURCESfails to compile if the array and the narrowed union disagree).
TDD Order
This is a subtractive change with no new behavior to drive red→green; each step's "green" is the existing suite passing after the removal, gated by pnpm run check + pnpm run lint + pnpm run test.
Because removing public exports from permission-events.ts breaks service.ts, the deleted handler file, and every consumer test at the type level simultaneously, the code+test removal is one atomic step.
-
Remove the RPC subsystem and all consumers (atomic). Delete
src/permission-event-rpc.tsandtest/permission-event-rpc.test.ts; remove the RPC constants, request/reply types,PermissionsRpcReply,PERMISSIONS_PROTOCOL_VERSION, and the"rpc_prompt"union member fromsrc/permission-events.ts(and reword thePermissionsReadyEventcomment); removebuildRpcUiPrompt/RpcPromptInputfromsrc/permission-ui-prompt.ts; remove"rpc_prompt"fromsrc/authority/forwarding-io.ts; drop the RPC re-exports fromsrc/service.ts; unwire registration and the two unsub handles fromsrc/index.ts; updatetest/permission-events.test.ts,test/permission-ui-prompt.test.ts, and the RPC arm oftest/composition-root.test.ts. Verify:pnpm --filter @gotgenes/pi-permission-system run check(tsc catches any missed importer),pnpm --filter @gotgenes/pi-permission-system run lint,pnpm --filter @gotgenes/pi-permission-system run test, andpnpm fallow dead-code(confirms no orphaned RPC symbol survives). Commit:feat(pi-permission-system)!: remove deprecated event-bus RPC channel (#531)with aBREAKING CHANGE:footer stating thepermissions:rpc:check/permissions:rpc:promptchannels are removed, thatpermissions:rpc:checkconsumers migrate togetPermissionsService().checkPermission(surface, value?, agentName?), and thatpermissions:rpc:promptis removed with no public replacement (prompt forwarding is internal). -
Repoint the docs and mark the roadmap step complete. Rewrite the RPC sections of
docs/cross-extension-api.mdto point exclusively at the service accessor (remove the two RPC sections, the four channel-table rows, the protocol-version and readiness-ping paragraphs, and therpc_promptsource mention); updatedocs/architecture/architecture.md(cross-extension paragraph, directory tree, Phase 8 Step 7 ✅ +S7Mermaid node ✅, "Elicitation paths" metric row → done). Verify:pnpm run lint(rumdl) passes; no dangling[#N]:link definitions; grep confirms no surviving RPC reference outside the frozen history docs. Commit:docs(pi-permission-system): repoint cross-extension docs off the removed RPC channel (#531). -
Comment on #309. Post a comment via
gh issue comment 309noting that removing the event-bus RPC narrows #309's scope to the advisory service bash path only — the RPC bash path referenced in its proposed-change item 3 no longer exists. Not a commit; an issue action taken during implementation.
Risks and Mitigations
- Missed importer of a removed symbol.
tsc(viapnpm run check) fails the build on any dangling import, andpnpm fallow dead-codecatches orphaned symbols. The atomic step 1 keeps the tree compiling at every commit boundary. Mitigated. - External event-bus RPC consumers break silently on upgrade.
This is the intended breaking change.
Mitigated by the
feat!:major bump, theBREAKING CHANGE:footer, the migration note, and the repointed cross-extension docs. - Narrowing the public
PermissionUiPromptSourceunion surprises a broadcast consumer. A consumer with an exhaustive switch over the old union still compiles against their own copy; no runtime event will carry"rpc_prompt"after removal, so the narrowing is behavior-correct. Documented in the migration note. Mitigated. - Stale RPC mention left in a doc.
A repo-wide grep after step 2 confirms the only remaining references live in the frozen
docs/architecture/history/*.mdfiles, which are intentionally preserved. Mitigated.
Open Questions
None.
The scope is fully determined by the issue, the Phase 8 roadmap (Step 7), and the code grep; the two judgment calls (removing the RPC-only PERMISSIONS_PROTOCOL_VERSION / PermissionsRpcReply, and the dead rpc_prompt source cascade) are resolved in Design Overview and dictated by code-design's remove-dead-code rule.