Skip to content

Commit bbfb371

Browse files
grypezclaude
andcommitted
feat(session): identify matched provision and add active-provisions panel
Tweak 1 — show which provision auto-accepted a request: - Add findMatch(tool, invocations) and listProvisions() to permission-tracker vat - Hook calls vatFindMatch after vatRoute returns 'allow' and passes the result to recordProvisioned, threading it through rpc.ts → session.record RPC → channel.record() → HistoryEntry.provision - TUI: provisioned entries show '→ by provision: git log * | head *' (compact one-liner) instead of '→ standing provision'; detailed pattern block reserved for user-accepted-with-provision (◆) entries Tweak 2 — active-provisions panel (P keybind): - ProvisionsPanel component derives unique active provisions from session history - Press P in session detail view to open; Esc to close - Lists each provision as '◆ tool name arg1 arg2 | name2 arg1' - Status bar hint updated Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 27571db commit bbfb371

8 files changed

Lines changed: 315 additions & 118 deletions

File tree

packages/caprock/bin/hook.ts

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,31 @@ async function vatAddSection(
263263
});
264264
}
265265

266+
/**
267+
* Return the first provision that matches the given tool and invocations,
268+
* or null if none match.
269+
*
270+
* @param rootKref - The vat's root kref.
271+
* @param tool - The tool name.
272+
* @param invocations - The parsed command components.
273+
* @returns The matching provision, or null.
274+
*/
275+
async function vatFindMatch(
276+
rootKref: string,
277+
tool: string,
278+
invocations: ParsedInvocation[],
279+
): Promise<Provision | null> {
280+
const response = await sendCommand({
281+
socketPath: SOCKET_PATH,
282+
method: 'queueMessage',
283+
params: [rootKref, 'findMatch', [tool, invocations]],
284+
});
285+
if (isJsonRpcFailure(response)) {
286+
return null;
287+
}
288+
return decodeCapData(response.result as CapData) as Provision | null;
289+
}
290+
266291
/**
267292
* Return the number of entries in the permission vat's allow set.
268293
*
@@ -518,14 +543,21 @@ async function onPreToolUse(payload: PreToolUsePayload): Promise<void> {
518543
});
519544

520545
if (vatResponse === 'allow') {
521-
if (state.kernelSessionId) {
546+
if (state.kernelSessionId && invocations !== null) {
522547
const autoDescription = `Allow ${tool_name}(${JSON.stringify(tool_input)})`;
523-
recordProvisioned(
524-
SOCKET_PATH,
525-
state.kernelSessionId,
526-
autoDescription,
527-
invocations === null ? undefined : { invocations },
528-
).catch(() => undefined);
548+
vatFindMatch(state.rootKref, tool_name, invocations)
549+
.then(async (matched) =>
550+
recordProvisioned(
551+
SOCKET_PATH,
552+
state.kernelSessionId,
553+
autoDescription,
554+
{
555+
invocations,
556+
...(matched === null ? {} : { provision: matched }),
557+
},
558+
),
559+
)
560+
.catch(() => undefined);
529561
}
530562
process.stdout.write(JSON.stringify({ continue: true }));
531563
return;
@@ -556,7 +588,9 @@ async function onPreToolUse(payload: PreToolUsePayload): Promise<void> {
556588
if (!isNoSubscriber && errorStr.includes('Session not found')) {
557589
try {
558590
const ks = await createKernelSession(SOCKET_PATH, session_id);
591+
// eslint-disable-next-line require-atomic-updates
559592
state.kernelSessionId = ks.sessionId;
593+
// eslint-disable-next-line require-atomic-updates
560594
state.ocapUrl = ks.ocapUrl;
561595
await saveSessionState(session_id, state);
562596
connectId = ks.sessionId;

packages/caprock/src/rpc.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import type { ParsedInvocation } from '@metamask/kernel-utils/session/provision';
1+
import type {
2+
ParsedInvocation,
3+
Provision,
4+
} from '@metamask/kernel-utils/session/provision';
25
import type { JsonRpcResponse } from '@metamask/utils';
36
import { assertIsJsonRpcResponse, isJsonRpcFailure } from '@metamask/utils';
47
import { randomUUID } from 'node:crypto';
@@ -273,17 +276,21 @@ export async function authorizeRequest(
273276
* @param description - Human-readable description of the auto-accepted operation.
274277
* @param options - Optional parameters.
275278
* @param options.invocations - Parsed invocations to forward to the TUI.
279+
* @param options.provision - The standing provision that approved the request.
276280
*/
277281
export async function recordProvisioned(
278282
socketPath: string,
279283
sessionId: string,
280284
description: string,
281-
options?: { invocations?: ParsedInvocation[] },
285+
options?: { invocations?: ParsedInvocation[]; provision?: Provision },
282286
): Promise<void> {
283287
const params: Record<string, unknown> = { sessionId, description };
284288
if (options?.invocations !== undefined) {
285289
params.invocations = options.invocations;
286290
}
291+
if (options?.provision !== undefined) {
292+
params.provision = options.provision;
293+
}
287294
await sendCommand({ socketPath, method: 'session.record', params });
288295
}
289296

packages/caprock/vat/permission-tracker.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@ import type {
2222
Provision,
2323
ParsedInvocation,
2424
} from '@metamask/kernel-utils/session';
25-
import { computeAuthority, matchPattern } from '@metamask/kernel-utils/session';
25+
import {
26+
computeAuthority,
27+
matchPattern,
28+
matchProvision,
29+
} from '@metamask/kernel-utils/session';
2630
import {
2731
constant,
2832
leastAuthority,
@@ -162,6 +166,32 @@ export function buildRootObject(): ReturnType<typeof makeDefaultExo> {
162166
rebuildSection();
163167
},
164168

169+
/**
170+
* Return the first provision that matches the given tool and invocations,
171+
* or null if none match.
172+
*
173+
* @param tool - The tool name.
174+
* @param invocations - The parsed command components.
175+
* @returns The matching provision, or null.
176+
*/
177+
findMatch(tool: string, invocations: ParsedInvocation[]): Provision | null {
178+
for (const { provision } of sectionRecords) {
179+
if (matchProvision(provision, tool, invocations)) {
180+
return provision;
181+
}
182+
}
183+
return null;
184+
},
185+
186+
/**
187+
* Return all provisions currently in the sheaf.
188+
*
189+
* @returns Array of provisions, oldest first.
190+
*/
191+
listProvisions(): Provision[] {
192+
return sectionRecords.map(({ provision }) => provision);
193+
},
194+
165195
/**
166196
* Return the current section count (for session_end stats).
167197
*

packages/kernel-node-runtime/src/daemon/rpc-socket-server.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -325,10 +325,14 @@ async function handleSessionRequest(
325325
const invocations = Array.isArray(args.invocations)
326326
? (args.invocations as ParsedInvocation[])
327327
: undefined;
328-
session.recordProvisioned(
329-
description,
330-
invocations === undefined ? undefined : { invocations },
331-
);
328+
const provision =
329+
typeof args.provision === 'object' && args.provision !== null
330+
? (args.provision as Provision)
331+
: undefined;
332+
session.recordProvisioned(description, {
333+
...ifDefined({ invocations }),
334+
...ifDefined({ provision }),
335+
});
332336
return ok(null);
333337
}
334338

0 commit comments

Comments
 (0)