Files
my-pi/pi-permission-system/docs/plans/0752-mint-permission-request-id.md

351 lines
35 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
issue: 752
issue_title: "pi-permission-system: mint a permission request id at creation and carry it on every decision"
---
# Mint a permission request id at creation and carry it on every decision
## Release Recommendation
**Release:** ship independently
Phase 13 Step 9 carries `Release: independent` in the roadmap's Release batches subsection, and it is a member of no batch.
The work lands as `feat:` commits, so it cuts a release on its own.
It must land **before** Step 3 ([#745]), which edits a different interface in the same `src/permission-events.ts` file.
## Problem Statement
This package has no permission request id.
It has three conventions, and none of them covers a request that never prompts.
| Path | Id today | Site |
| ----------------- | ---------------------------------------------------------------- | ---------------------------------------------------- |
| tool-call gates | **borrowed** `tcc.toolCallId` — the SDK's id, never minted | `src/handlers/gates/runner.ts:162` |
| skill-input gate | minted `skill-input-<ts>-<rand>-<pid>` | `src/handlers/gates/skill-input-gate-pipeline.ts:86` |
| forwarded request | a **fresh** `<ts>-<rand>-<pid>`, discarding the id it was handed | `src/authority/approval-escalator.ts:253` |
The id also attaches at the *ask*, not at request creation.
`GateRunner.runDescriptor` sets `requestId` only inside `promptForApproval`, so every non-prompting resolution — session-approved, yolo auto-approve, infrastructure auto-allow, policy block — carries no request id at all.
And `PermissionDecisionEvent` has seven fields, none of them an id, so the terminal event of every permission request is uncorrelatable.
The issue's measurement, from a 7.3 MB review log (9 417 entries) restricted to the last 14 days (766 entries): 452 entries — `session_approved` ×422, `infrastructure_auto_allowed` ×24, `blocked` ×6 — carry `toolCallId` but never `requestId`.
## Goals
- Mint one id per permission request at request creation, in `GateRunner.run`, shared by the bypass and descriptor branches.
- Carry it on every review-log write the runner performs, whatever the resolution.
- Add it to `PermissionDecisionEvent`, so the terminal broadcast of a permission request is correlatable.
- Collapse the three id conventions to one: the forwarding edge adopts the id it is handed rather than minting a third, and `createSkillInputRequestId` is deleted.
- Give the gate-error boundary entry a minted id too, so no `permission_request.*` write is id-less.
- Keep `toolCallId` flowing untouched as the join back to the Pi transcript, and close the one review-log write that omits it (`skill-read.ts`).
Not breaking.
`PermissionDecisionEvent` gains a field, which its documented stability guarantee permits.
See [Is this breaking?](#is-this-breaking) for the one observable change that needs a changelog note but not a major bump.
## Non-Goals
- **The missing `permissions:decision` on the gate-error path.**
A thrown gate blocks the tool call and writes a review entry, but emits no terminal event — the only blocking path that does not.
Filed as [#753] during this planning session and deferred there; this plan only mints the id that issue will carry.
- **`decidedBy` provenance** — Phase 13 Step 6 ([#726]).
*Which request* and *what decided it* are the same audit question and both enrich the review-log write path, so they land in sequence, not concurrently.
- **Cross-session prompt/decision correlation** — Phase 13 Step 10 ([#610]).
This plan is the local foundation it needs; the parent-side terminal emit stays there.
- **The payload/`message` swap on the forwarded wire and the `ui_prompt` broadcast** — Phase 13 Step 3 ([#745]).
This plan touches `src/permission-events.ts` and `src/authority/approval-escalator.ts`, which that step also rewrites; the two must not run concurrently.
- **Adding `toolCallId` to `PermissionDecisionEvent` or `PermissionUiPromptEvent`.**
Decided against: the decision event correlates to the review log by `requestId`, and the review log is where the transcript join lives.
- **Rewriting the review-log entries' other fields.**
`logContext` composition, redaction, and the `resolution` vocabulary are unchanged.
## Background
### The runner is the one request-creation point
`GateRunner.run(gate, agentName, toolCallId)` (`src/handlers/gates/runner.ts`) is called from exactly two sites: `ToolCallGatePipeline.evaluate` (once per gate producer, six per tool call) and `SkillInputGatePipeline.evaluate`.
Every permission request this package raises passes through it.
`toolCallId` has exactly **one** reader inside the runner: `requestId: toolCallId` at line 162.
Every descriptor already carries `toolCallId` in its own `promptDetails` and `logContext``bash-path.ts`, `bash-external-directory.ts`, `external-directory.ts`, `path.ts`, `tool.ts`, and `skill-read.ts`'s `promptDetails`.
So once the runner mints its own id, **the third parameter has no reader left**: it is deleted, not narrowed to `string | null` as the issue proposes.
The one gap that parameter would otherwise cover is `skill-read.ts`'s `logContext`, which omits `toolCallId` while its `promptDetails` carries it.
That is a one-line fix in the gate module, not a reason to keep a relay parameter.
### The write paths are four, not three
| Site | Event | Reached by |
| --------------------------------------- | --------------------------------------------------------- | --------------------------------------------------------- |
| `runner.ts:60` | `gate.log.event` (bypass) | infrastructure auto-allow, all-session-covered bash paths |
| `runner.ts:99` | `permission_request.session_approved` | descriptor session-hit fast path |
| `runner.ts:123` | `permission_request.auto_approved` | yolo grant |
| `runner.ts:173``applyPermissionGate` | `permission_request.blocked`, `resolution: policy_denied` | config-level deny |
The fourth is written by `applyPermissionGate` (`src/permission-gate.ts:52`) from the `logContext` object the runner hands it, so injecting the id into that one object covers it without touching the gate function.
### `GateBypass.decision` has one construction site, not three
The issue and the roadmap both say "the three descriptor-built event literals (`bash-external-directory.ts`, `bash-path.ts`, `external-directory.ts`)".
Only **one** `GateBypass` carries a `decision`: the Pi infrastructure-read bypass at `src/handlers/gates/external-directory.ts:59`.
The other two bypasses carry a `log` and no `decision`, and `decision: { surface, value }` on a `GateDescriptor` is a different, two-field shape that this change does not touch.
### The forwarding edge already discards the id it is handed
`ParentAuthorizer.authorize` receives `PromptPermissionDetails` and never reads `requestId` — the tests name their fixture value `"unused-by-parent-authorizer"`.
`buildForwardedRequest` mints a fresh `<ts>-<rand>-<pid>` for the wire `id`, which names the request and response files and which the serving node then adopts as its own ask's `requestId` (`forwarded-request-server.ts:100`).
### Constraints from `AGENTS.md` and the package skill
- A module no code imports yet is dead code; `pnpm fallow dead-code` gates CI.
So the new mint module cannot land as a standalone pure-addition commit — it lands with its first caller.
- Adding a **required** field to a shared interface breaks inline literal constructors at compile time, so those call sites fold into the same commit.
- The roadmap's step-mark (`✅` on the heading and the Mermaid node), the `Landed:` note, and the stale health-metric rows land in the implementation doc-update commit, not at ship time.
## Design Overview
### One mint, one home
```typescript
// src/permission-request-id.ts
import { randomUUID } from "node:crypto";
/**
* Mint the id that identifies one permission request from creation to its
* terminal decision.
*
* Distinct from the host's `toolCallId`, which stays alongside it as the join
* back to the Pi transcript: one tool call raises up to six requests.
*/
export function createPermissionRequestId(): string {
return `perm-${randomUUID()}`;
}
```
The `perm-` prefix makes the id self-identifying in a review log that also carries SDK tool-call ids (`toolu_…`) and, until this change lands, two other mint formats.
UUIDv7 was considered and declined: Node has no v7.
`crypto.randomUUID({ version: 7 })` does not throw — it silently ignores the option and returns a v4, verified on Node v26.7.0 (`0d29880d-a1ec-4e44-a356-6da665db3cef`, version nibble `4`).
The package's `engines` is `>=22`, so v7 would mean either a new runtime dependency on a security-sensitive extension or a hand-rolled layout, and what it buys is external time-ordering that the append-only, per-entry-timestamped review log does not need.
### The runner mints once and injects at two points
```typescript
async run(gate: GateResult, agentName: string | null): Promise<GateOutcome> {
if (!gate) return { action: "allow" };
const requestId = createPermissionRequestId();
if (isGateBypass(gate)) {
if (gate.log) {
this.reporter.writeReviewLog(gate.log.event, { ...gate.log.details, requestId });
}
if (gate.decision) this.emitDecision(requestId, gate.decision);
return { action: "allow" };
}
return this.runDescriptor(gate, agentName, requestId);
}
```
`runDescriptor` builds its log context once — `const logContext = { ...descriptor.logContext, agentName, requestId }` — and uses it for the `session_approved` write, the `auto_approved` write, and the object handed to `applyPermissionGate`, which covers the `policy_denied` write.
It passes `requestId` to `prompter.escalate` where `toolCallId` is passed today.
A single private helper is the sole injection point for the event:
```typescript
private emitDecision(requestId: string, facts: DecisionEventFacts): void {
this.reporter.emitDecision({ requestId, ...facts });
}
```
`buildDecisionEvent` keeps its five parameters and narrows its return type to `DecisionEventFacts`; `GateBypass.decision` narrows to the same type.
Both the bypass branch and the three descriptor emits then flow through one stamping site, so a future emit cannot forget the id.
```typescript
// src/handlers/gates/descriptor.ts
/** A decision event's facts, before the runner stamps the request id it minted. */
export type DecisionEventFacts = Omit<PermissionDecisionEvent, "requestId">;
```
It lives in `descriptor.ts` rather than `permission-events.ts` because it is a gate-layer projection — the shape a gate can produce without knowing the id — not part of the published event contract.
### The forwarding edge adopts the id it is handed
`ForwardedRequestFacts` gains `requestId: string`; `authorize` passes `details.requestId`; `buildForwardedRequest` uses it as the wire `id` and deletes its own mint.
One id then runs end to end: the child's request → the request and response filenames → the serving node's `PromptPermissionDetails.requestId` → the serving node's own prompt and decision.
Consumer sketch at the edge:
```typescript
// ParentAuthorizer.authorize
return this.waitForForwardedApproval(this.ctx, {
requestId: details.requestId, // adopted, not re-minted
message: details.message,
display: { source: uiPrompt.source, surface: uiPrompt.surface, value: uiPrompt.value },
sessionApproval: details.sessionApproval,
accessIntent: details.accessIntent,
});
```
One guard is warranted by the change itself.
Today the wire `id` is always locally minted, so an inbound id never becomes an **outbound** filename.
After adoption it can: at a relay hop, `details.requestId` is `request.id`, read from a JSON file on disk that `forwarding-io.ts` validates only as `typeof parsed.id === "string"`.
`buildForwardedRequest` therefore falls back to a fresh `createPermissionRequestId()` when the adopted id does not match `/^[A-Za-z0-9._-]+$/`.
This makes [#745]'s planned `requesterRequestId` wire field redundant — the wire `id` **is** the child's request id — so that committed plan gets an amending note in this change's doc commit.
### The gate-error boundary
`createFailClosedToolCall`'s `catch` mints an id and names it on the `permission_request.blocked` / `gate_error` entry.
That introduces a call into the fail-closed catch body, which is the package's headline invariant (#452): the SDK's `emitToolCall` does not catch a throwing handler, so anything that throws out of this `catch` means the command runs ungated.
`randomUUID()` throws only when there is no entropy source, but the invariant should not rest on that.
The recording work in the `catch``audit.recordError()`, the mint, `writeReviewLog` — is wrapped in a nested `try`/`catch` that swallows, leaving `return { block: true, reason: … }` unconditional.
That strengthens the guarantee beyond today's, where the same block is unprotected.
### Is this breaking?
No, and the reasoning is worth recording because one observable value does change.
`permissions:ui_prompt.requestId` today equals the SDK `toolCallId` for a tool-call ask; after this it is a minted `perm-…`.
The event carries no `toolCallId` field, so a consumer that had been joining that broadcast to the Pi transcript through `requestId` loses the join.
But the documented contract is "Unique ID for the permission request being prompted" — the equality was coincidental and never documented, and one tool call legitimately raises up to six distinct requests, so the old value was not even unique per request.
`PermissionDecisionEvent` gains a field, which its stability guarantee explicitly permits.
The forwarded-request `id` changes format, but it is an internal wire artifact with no consumer contract and no format validation on either side, so version skew is safe in both directions.
Ships as `feat:` with a changelog-visible note on the `ui_prompt` value change, not `feat!:`.
## Module-Level Changes
### Source
| File | Change |
| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `src/permission-request-id.ts` | **New.** `createPermissionRequestId()``perm-<randomUUID()>` |
| `src/handlers/gates/runner.ts` | `run(gate, agentName)` — third parameter deleted; mint once at the top; inject into the bypass log write and the descriptor's single `logContext`; escalate with the minted id; new private `emitDecision(requestId, facts)` as the sole stamping site |
| `src/handlers/gates/descriptor.ts` | New `DecisionEventFacts` type alias; `GateBypass.decision?: DecisionEventFacts` |
| `src/handlers/gates/helpers.ts` | `buildDecisionEvent` return type → `DecisionEventFacts` (signature otherwise unchanged) |
| `src/handlers/gates/skill-input-gate-pipeline.ts` | Delete `createSkillInputRequestId`; `evaluate` calls `runner.run(descriptor, agentName)` |
| `src/handlers/gates/tool-call-gate-pipeline.ts` | `runner.run(await produce(), tcc.agentName)` — drops the `tcc.toolCallId` argument |
| `src/handlers/gates/skill-read.ts` | Add `toolCallId: tcc.toolCallId` to `logContext`, matching every other tool-call gate |
| `src/permission-events.ts` | `PermissionDecisionEvent` gains required `requestId: string` |
| `src/authority/approval-escalator.ts` | `ForwardedRequestFacts` gains `requestId: string`; `authorize` relays `details.requestId`; `buildForwardedRequest` adopts it as the wire `id` behind a filename-safety guard and deletes its `Math.random` mint |
| `src/handlers/tool-call-boundary.ts` | Mint a request id in the `catch` and name it on the `gate_error` entry; wrap the catch's recording work so nothing in it can defeat the block |
### Tests
| File | Change |
| ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `test/permission-request-id.test.ts` | **New.** Prefix and uniqueness, relocated from the `createSkillInputRequestId` cases |
| `test/handlers/gates/runner.test.ts` | Rewrite `"passes requestId from toolCallId to prompt"`; new cases pinning one id across all four write paths, the escalate call, and the decision event; two runs mint different ids |
| `test/handlers/gates/skill-input-gate-pipeline.test.ts` | Drop the `createSkillInputRequestId` import and its two cases; `runner.run` asserted with two arguments |
| `test/handlers/gates/external-directory.test.ts` | The infrastructure bypass's `decision` no longer carries a `requestId` (the runner stamps it) |
| `test/decision-reporter.test.ts` | `makeDecisionEvent` literal gains `requestId` (required field — compile error otherwise) |
| `test/permission-events.test.ts` | Same for its local decision-event factory; a case asserting the emitted payload carries `requestId` |
| `test/authority/approval-escalator.test.ts` | Fixture `requestId` renamed from `"unused-by-parent-authorizer"`; the written request's `id` equals `details.requestId`; an unsafe id falls back to a fresh mint |
| `test/handlers/tool-call-boundary.test.ts` | The `gate_error` entry carries a `perm-` `requestId`; a throwing reporter still yields `{ block: true }` |
| `test/handlers/gates/helpers.test.ts` | `buildDecisionEvent` returns facts without a `requestId` |
Everything else asserting decision events uses `toMatchObject` / `objectContaining` (verified across `test/handlers/tool-call-events.test.ts`, `test/handlers/input-events.test.ts`, `test/handlers/external-directory-integration.test.ts`), so an added field does not break them.
### Documentation
| File | Change |
| ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `docs/cross-extension-api.md` | Add the `requestId` row to the `permissions:decision` payload-fields table; note on the `ui_prompt` `requestId` row that it is a minted request id, not the SDK tool-call id |
| `docs/architecture/architecture.md` | `✅` on the Step 9 heading and its Mermaid node, plus a `Landed:` note; module-tree entries for `runner.ts` (signature), `skill-input-gate-pipeline.ts` (mint removed), `approval-escalator.ts` (adopts the handed id), `tool-call-boundary.ts` (minted id on the `gate_error` entry), and a new `permission-request-id.ts` entry; correct the Step 9 `Target:` text (one bypass decision literal, not three; parameter deleted, not narrowed); fix the mint-site recompute command and the metric row |
| `docs/plans/0745-cross-boundary-payload-swap.md` | Amending note: the wire `id` now **is** the child's request id, so the planned `requesterRequestId` field and its TDD step are redundant |
### Health metrics
Both baselines measured at planning time.
| Metric | Baseline | Predicted |
| ----------------------------------------------- | -------- | --------- |
| Request-id mint sites in `src/` | 2 | 1 |
| `requestId` lines in `src/permission-events.ts` | 1 | 2 |
The roadmap's recompute command for the first is `grep -rn "Math.random().toString(36)" … | wc -l`, which under this design goes 2 → **0**, not 2 → 1: both ad-hoc mints are deleted and the one replacement uses `randomUUID`.
The row's intent — one mint site — is unchanged, so the command is corrected to `grep -rnE "Math\.random\(\)\.toString\(36\)|randomUUID" packages/pi-permission-system/src --include="*.ts" | wc -l`, which reads 2 today and 1 after.
The second is a line count, so the new field's doc comment must not repeat the token `requestId`, and `DecisionEventFacts` must not live in `permission-events.ts` — the layering argument above already places it in `descriptor.ts`, and this is the check that confirms it.
## Test Impact Analysis
1. **Newly possible.**
`createPermissionRequestId` is directly testable for format and uniqueness — the same two properties `createSkillInputRequestId` had, now covering every surface rather than skill-input alone.
The correlation property is newly assertable at all: within one `runner.run`, the id on the review-log write and the id on the decision event can be read off the two mocks and compared, which no test could do before because the non-prompting paths carried no id.
2. **Newly redundant.**
`test/handlers/gates/skill-input-gate-pipeline.test.ts`'s two `createSkillInputRequestId` cases are removed with the function; their coverage moves to `test/permission-request-id.test.ts`.
`runner.test.ts:320` (`"passes requestId from toolCallId to prompt"`) pins the behavior being removed and is rewritten rather than kept.
3. **Must stay as-is.**
`test/authority/approval-escalator.test.ts`'s round-trip cases read `request.id` dynamically from the written file, so they keep exercising the wire path unchanged; only the new assertion about *where the id came from* is added.
Every `toMatchObject` decision-event assertion across the handler tests stays: they pin the surface/value/resolution contract this change must not disturb.
## Invariants at risk
| Invariant | Source | Pinned by |
| --------------------------------------------------------------- | ------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `message` is byte-identical to the pre-[#744] assemblers | Step 1 `Landed:` | `test/presentation/legacy-message.test.ts` — untouched; this plan changes no presentation module |
| A 200-line here-string ask renders within the 24-row default | Step 2 `Landed:` | `test/presentation/dialog-renderer.test.ts` — untouched, same reason |
| Fail-closed: a thrown gate blocks, never allows | #452, `tool-call-boundary.ts` | `test/handlers/tool-call-boundary.test.ts` "blocks fail-closed when the gate throws" — **strengthened** here by a new case pinning that a throwing reporter still yields `{ block: true }`, since this change adds work to that `catch` |
| A forwarded request's `id` names its request and response files | `approval-escalator.ts` / `forwarded-request-server.ts` | `test/authority/approval-escalator.test.ts` round-trips — plus the new filename-safety guard case |
| One prompt per gate, not per tool call | implicit in `ToolCallGatePipeline`'s six producers | The minted-id change makes this observable for the first time: a tool call now yields up to six distinct `requestId`s where it yielded one repeated `toolCallId` |
No quantitative invariant (byte count, row budget, token budget, latency) is touched.
## TDD Order
1. **Mint at request creation and carry it on every review-log write.**
Red: `test/handlers/gates/runner.test.ts` — the id escalated to the prompter matches `/^perm-/` and is not the tool call id; the same id appears on the bypass log write, `session_approved`, `auto_approved`, and the `policy_denied` write; two `run` calls mint different ids.
`test/permission-request-id.test.ts` — prefix and uniqueness.
Green: add `src/permission-request-id.ts`; `GateRunner.run(gate, agentName)` mints once and injects; delete `createSkillInputRequestId`; update both pipeline call sites; add `toolCallId` to `skill-read.ts`'s `logContext`.
The deletion, the signature change, and both call sites are one commit because removing the export and the parameter breaks the type check otherwise.
`feat(pi-permission-system): mint a permission request id at request creation`
2. **`requestId` on `PermissionDecisionEvent`.**
Red: `test/permission-events.test.ts` — the emitted payload carries `requestId`; `test/handlers/gates/runner.test.ts` — the decision event's id equals the review entry's on the session-hit, yolo, gate-result, and bypass paths.
Green: add the required field; introduce `DecisionEventFacts` in `descriptor.ts`; narrow `GateBypass.decision` and `buildDecisionEvent`'s return type; add the runner's private `emitDecision`.
The two full-literal test factories (`test/decision-reporter.test.ts:31`, `test/permission-events.test.ts:120`) and `test/handlers/gates/external-directory.test.ts`'s bypass literal fold in — a new required field is a compile error at each.
`feat(pi-permission-system): carry the request id on permissions:decision`
3. **The forwarding edge adopts the id it is handed.**
Red: `test/authority/approval-escalator.test.ts` — the written request's `id` equals `details.requestId`; a `requestId` containing a path separator falls back to a freshly minted id.
Green: `ForwardedRequestFacts.requestId`, the relay in `authorize`, the adoption plus safety guard in `buildForwardedRequest`, and the deletion of its `Math.random` mint.
`feat(pi-permission-system): adopt the requester's request id as the forwarded request id`
4. **The gate-error boundary.**
Red: `test/handlers/tool-call-boundary.test.ts` — the `gate_error` entry carries a `perm-` `requestId`; two errored calls carry different ids; a reporter that throws still yields `{ block: true }`.
Green: mint in the `catch`, name it on the entry, and wrap the recording work.
`feat(pi-permission-system): give the gate-error review entry a request id`
5. **Documentation.**
The `docs/cross-extension-api.md` table rows, the architecture step-mark and `Landed:` note, the module-tree entries, the corrected Step 9 `Target:` text and mint-site recompute command, and the amending note on [#745]'s plan.
`docs(pi-permission-system): record the minted request id in the API and architecture docs`
## Risks and Mitigations
| Risk | Mitigation |
| -------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| A `catch`-body throw defeats fail-closed and lets a command run ungated | The mint and the review write are wrapped in a nested swallowing `try`; the `{ block: true }` return is unconditional; a new test pins it with a throwing reporter |
| An inbound, unvalidated request id becomes an outbound filename at a relay hop | `buildForwardedRequest` falls back to a fresh mint unless the adopted id matches `/^[A-Za-z0-9._-]+$/`; tested |
| A consumer joining `permissions:ui_prompt.requestId` to the Pi transcript breaks | Documented in the changelog note and in `docs/cross-extension-api.md`; the equality was never a documented contract, and the old value was not unique per request |
| A later emit site forgets to stamp the id | `GateBypass.decision` and `buildDecisionEvent` both produce `DecisionEventFacts`, which is not assignable to `PermissionDecisionEvent`, so the compiler routes every emit through the runner's single stamping helper |
| The mint module lands with no importer and trips `pnpm fallow dead-code` in CI | It lands in step 1 together with its first caller, never as a standalone pure-addition commit |
| Concurrent work on [#745] or [#721] collides | Step 9 lands before Step 3 (`src/permission-events.ts`) and is sequenced against Step 5 (`src/authority/approval-escalator.ts`); the roadmap's Track E note already records this |
| The corrected metric-row grep drifts from what actually landed | Both the baseline and the predicted value were measured at planning time and are recomputed in the step 5 doc commit |
## Open Questions
- **Does [#745] still need `requesterRequestId`?**
Under step 3 the wire `id` *is* the child's request id, so the field is redundant.
Step 5 records that in [#745]'s plan rather than silently leaving a superseded TDD step in a committed document.
If [#745] later finds a case where the two must differ — a relay that re-identifies the request — the field returns with a recorded reason.
- **Should the `gate_error` path emit a terminal decision event?**
Deferred to [#753], filed during this planning session.
Until it lands, the minted boundary id appears on exactly one review entry and joins to nothing else.
- **Does a per-gate id change how [#610] correlates?**
A tool call now raises up to six ids where it raised one repeated `toolCallId`.
That is the correct granularity — one id per permission request — but [#610]'s parent-side emit should be planned against it explicitly rather than assuming one id per tool call.
[#610]: https://github.com/gotgenes/pi-packages/issues/610
[#721]: https://github.com/gotgenes/pi-packages/issues/721
[#726]: https://github.com/gotgenes/pi-packages/issues/726
[#744]: https://github.com/gotgenes/pi-packages/issues/744
[#745]: https://github.com/gotgenes/pi-packages/issues/745
[#753]: https://github.com/gotgenes/pi-packages/issues/753