refactor(pi-permission-inner-cmd): narrow bash recovery uniqueness to one message

- walk entries in reverse and stop at the latest assistant message containing the id
- require the id to match exactly one block within that message rather than across the whole session
- resolve a cross-message id reuse to the latest call being authorized
- update ADR 0001 wording for the narrowed scope
- add a regression test for cross-message id reuse
This commit is contained in:
2026-08-11 21:53:46 +08:00
parent f21cf54ff1
commit 06b21a28a6
3 changed files with 58 additions and 33 deletions
@@ -33,17 +33,21 @@ function extractBashCommand(block: ToolCallBlock): string | undefined {
/**
* Recover the complete native Bash command for one tool call.
*
* Walks session entries, finds the assistant `toolCall` block whose `id` equals
* `toolCallId`, and reads its structured `arguments.command`. Proceeds only
* when, per ADR 0001:
* The tool call being authorized is always the most recent one, so entries are
* walked in reverse and the search stops at the first (latest) assistant
* message that contains a `toolCall` block whose `id` equals `toolCallId`.
*
* - exactly one block matches the id (no duplicate),
* - that block names the native Bash tool,
* - `arguments.command` is a string.
* Per ADR 0001, the id must match exactly one block *within that single
* message*. An earlier message reusing the same id is a stale, already-resolved
* call and is irrelevant to the current authorization; but two matching blocks
* inside one message cannot be disambiguated (we cannot tell which one
* `details.toolCallId` refers to), so that case stays fail-closed. The matched
* block must then name the native Bash tool and carry a string
* `arguments.command`.
*
* Any other outcome — no match, duplicate id, a non-Bash tool call, a
* non-string command, or malformed entries — returns `undefined` so the caller
* defers fail-closed.
* Any other outcome — no match, a within-message duplicate id, a non-Bash tool
* call, a non-string command, or malformed entries — returns `undefined` so the
* caller defers fail-closed.
*
* @returns the complete Bash command, or `undefined`.
*/
@@ -51,33 +55,34 @@ export function recoverNativeBashCommand(
entries: ReadonlyArray<SessionEntry>,
toolCallId: string,
): string | undefined {
let matches = 0;
let command: string | undefined;
for (const entry of entries) {
for (let i = entries.length - 1; i >= 0; i--) {
const entry = entries[i];
if (entry.type !== "message") {
continue;
}
const message = entry.message as { role?: unknown; content?: unknown };
const message = entry.message;
if (message.role !== "assistant") {
continue;
}
const content = message.content;
if (!Array.isArray(content)) {
const matches: ToolCallBlock[] = [];
for (const block of message.content) {
if (isToolCallBlock(block) && block.id === toolCallId) {
matches.push(block);
}
}
if (matches.length === 0) {
continue;
}
for (const block of content) {
if (!isToolCallBlock(block) || block.id !== toolCallId) {
continue;
}
matches += 1;
// Keep walking the whole session so a duplicate id (two matching
// blocks) is detected even when the first match was unusable.
if (matches === 1) {
command = extractBashCommand(block);
}
}
// Latest message containing the id. Uniqueness only has to hold within
// this one message (see above); a cross-message reuse resolves to the
// latest, which is the call currently being authorized.
return matches.length === 1
? extractBashCommand(matches[0])
: undefined;
}
return matches === 1 ? command : undefined;
return undefined;
}