Skip to content

Commit bb5e686

Browse files
grypezclaude
andcommitted
feat(session): thread ParsedInvocation through authorization pipeline
Carries parsed command invocations from the hook call-site through the session registry and channel to TUI history entries, enabling the provision editor to show per-arg pattern controls. - Move ParsedInvocation type to session/types.ts to avoid circular deps - Add invocations? to SectionNotification and SessionHistoryEntry - Change authorizeRequest signature to an options bag { reason?, timeoutMs?, invocations? } for extensibility - channel.listAll() includes invocations via ifDefined in both the pending and decided entry paths - rpc-socket-server session.authorize handler extracts invocations from JSON-RPC params and forwards to session.authorizeRequest Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent d0ae972 commit bb5e686

8 files changed

Lines changed: 62 additions & 27 deletions

File tree

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -376,8 +376,7 @@ describe('startRpcSocketServer — session.* methods', () => {
376376
expect(response.result).toStrictEqual(decision);
377377
expect(existing.authorizeRequest).toHaveBeenCalledWith(
378378
'Allow read access',
379-
'Needed for operation',
380-
undefined,
379+
{ reason: 'Needed for operation' },
381380
);
382381
});
383382
});

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

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { RpcService } from '@metamask/kernel-rpc-methods';
22
import type { KernelDatabase } from '@metamask/kernel-store';
33
import { ifDefined } from '@metamask/kernel-utils';
4-
import type { Provision } from '@metamask/kernel-utils/session';
4+
import type {
5+
ParsedInvocation,
6+
Provision,
7+
} from '@metamask/kernel-utils/session';
58
import type { Kernel } from '@metamask/ocap-kernel';
69
import { rpcHandlers } from '@metamask/ocap-kernel/rpc';
710
import { unlink } from 'node:fs/promises';
@@ -302,11 +305,14 @@ async function handleSessionRequest(
302305
typeof args.reason === 'string' ? args.reason : undefined;
303306
const timeoutMs =
304307
typeof args.timeoutMs === 'number' ? args.timeoutMs : undefined;
305-
const decision = await session.authorizeRequest(
306-
description,
307-
reason,
308-
timeoutMs,
309-
);
308+
const invocations = Array.isArray(args.invocations)
309+
? (args.invocations as ParsedInvocation[])
310+
: undefined;
311+
const decision = await session.authorizeRequest(description, {
312+
...ifDefined({ reason }),
313+
...ifDefined({ timeoutMs }),
314+
...ifDefined({ invocations }),
315+
});
310316
return ok(decision);
311317
}
312318

packages/kernel-utils/src/session/channel.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { makePromiseKit } from '@endo/promise-kit';
22
import type { PromiseKit } from '@endo/promise-kit';
33

4+
import { ifDefined } from '../misc.ts';
45
import type {
56
Decision,
67
SectionNotification,
@@ -181,6 +182,7 @@ export function makeChannel(): Channel {
181182
queuedAt: hist.queuedAt,
182183
status: hist.verdict,
183184
decidedAt: hist.decidedAt,
185+
...ifDefined({ invocations: hist.notification.invocations }),
184186
}));
185187
const stillPending: SessionHistoryEntry[] = Array.from(
186188
pending.values(),
@@ -191,6 +193,7 @@ export function makeChannel(): Channel {
191193
guard: pend.notification.guard,
192194
queuedAt: pend.queuedAt,
193195
status: 'pending' as const,
196+
...ifDefined({ invocations: pend.notification.invocations }),
194197
}));
195198
return [...decided, ...stillPending].sort((lhs, rhs) => {
196199
if (lhs.queuedAt < rhs.queuedAt) {

packages/kernel-utils/src/session/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export type {
22
ArgPattern,
33
InvocationPattern,
4+
ParsedInvocation,
45
Provision,
56
SectionRequest,
67
SectionNotification,
@@ -14,7 +15,7 @@ export { makeChannel } from './channel.ts';
1415
export type { Channel, ModalStream } from './channel.ts';
1516
export { makeSessionRegistry } from './session-registry.ts';
1617
export type { Session, SessionRegistry } from './session-registry.ts';
17-
export type { ParsedInvocation, PatternOrder } from './provision.ts';
18+
export type { PatternOrder } from './provision.ts';
1819
export {
1920
isPathArg,
2021
pathInterval,

packages/kernel-utils/src/session/provision.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
import type { ArgPattern, InvocationPattern, Provision } from './types.ts';
2-
3-
export type ParsedInvocation = { name: string; argv: string[] };
1+
import type {
2+
ArgPattern,
3+
InvocationPattern,
4+
ParsedInvocation,
5+
Provision,
6+
} from './types.ts';
47

58
/**
69
* Returns true if the string looks like a file-system path (absolute or relative).

packages/kernel-utils/src/session/session-registry.test.ts

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -139,10 +139,9 @@ describe('makeSessionRegistry', () => {
139139
const registry = makeSessionRegistry(makeChannelBundle());
140140
const session = await registry.createSession();
141141

142-
const authPromise = session.authorizeRequest(
143-
'Write /tmp/out',
144-
'needs temp',
145-
);
142+
const authPromise = session.authorizeRequest('Write /tmp/out', {
143+
reason: 'needs temp',
144+
});
146145

147146
// Retrieve the token from the pending list so we can decide it
148147
const pending = session.listPending();
@@ -162,11 +161,10 @@ describe('makeSessionRegistry', () => {
162161
const registry = makeSessionRegistry(makeChannelBundle());
163162
const session = await registry.createSession();
164163

165-
const authPromise = session.authorizeRequest(
166-
'Execute script',
167-
'needs shell',
168-
500,
169-
);
164+
const authPromise = session.authorizeRequest('Execute script', {
165+
reason: 'needs shell',
166+
timeoutMs: 500,
167+
});
170168

171169
// Advance past the timeout — no subscriber decides, so the race rejects
172170
vi.advanceTimersByTime(600);

packages/kernel-utils/src/session/session-registry.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { ifDefined } from '../misc.ts';
22
import type { Channel, ModalStream } from './channel.ts';
33
import type {
44
Decision,
5+
ParsedInvocation,
56
SectionNotification,
67
SessionHistoryEntry,
78
} from './types.ts';
@@ -28,8 +29,11 @@ export type Session = {
2829
queueRequest(description: string, reason?: string): string;
2930
authorizeRequest(
3031
description: string,
31-
reason?: string,
32-
timeoutMs?: number,
32+
options?: {
33+
reason?: string;
34+
timeoutMs?: number;
35+
invocations?: ParsedInvocation[];
36+
},
3337
): Promise<Decision>;
3438
subscribe(stream: ModalStream): void;
3539
};
@@ -69,10 +73,17 @@ function makeSession(
6973
const makeNotification = (
7074
description: string,
7175
reason: string,
76+
invocations?: ParsedInvocation[],
7277
): SectionNotification => {
7378
const token = `req-${requestCount}`;
7479
requestCount += 1;
75-
return { token, description, reason, guard: { body: '#{}', slots: [] } };
80+
return {
81+
token,
82+
description,
83+
reason,
84+
guard: { body: '#{}', slots: [] },
85+
...ifDefined({ invocations }),
86+
};
7687
};
7788

7889
return harden({
@@ -101,10 +112,14 @@ function makeSession(
101112

102113
async authorizeRequest(
103114
description: string,
104-
reason = 'Queued from CLI',
105-
timeoutMs?: number,
115+
options: {
116+
reason?: string;
117+
timeoutMs?: number;
118+
invocations?: ParsedInvocation[];
119+
} = {},
106120
): Promise<Decision> {
107-
const notification = makeNotification(description, reason);
121+
const { reason = 'Queued from CLI', timeoutMs, invocations } = options;
122+
const notification = makeNotification(description, reason, invocations);
108123
const decision = channel.broadcast(notification);
109124
if (timeoutMs === undefined) {
110125
return decision;

packages/kernel-utils/src/session/types.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
/**
2+
* A single parsed command-or-tool invocation: the name and its positional args.
3+
* Used to describe what exactly was called before being converted to a Provision.
4+
*/
5+
export type ParsedInvocation = { name: string; argv: string[] };
6+
17
/**
28
* Pattern for one positional argument in a provision.
39
*
@@ -61,6 +67,8 @@ export type SectionNotification = {
6167
reason: string;
6268
schema?: unknown;
6369
guard: { body: string; slots: string[] };
70+
/** Parsed invocations for the request — present when routed through the PreToolUse hook. */
71+
invocations?: ParsedInvocation[];
6472
};
6573

6674
/**
@@ -103,6 +111,8 @@ export type SessionHistoryEntry = {
103111
queuedAt: string;
104112
status: 'pending' | 'accepted' | 'rejected';
105113
decidedAt?: string;
114+
/** Parsed invocations — present when routed through the PreToolUse hook. */
115+
invocations?: ParsedInvocation[];
106116
};
107117

108118
/**

0 commit comments

Comments
 (0)