agent-relay-runner 0.11.5 → 0.11.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -11,6 +11,18 @@
|
|
|
11
11
|
]
|
|
12
12
|
}
|
|
13
13
|
],
|
|
14
|
+
"PreToolUse": [
|
|
15
|
+
{
|
|
16
|
+
"matcher": "AskUserQuestion",
|
|
17
|
+
"hooks": [
|
|
18
|
+
{
|
|
19
|
+
"type": "command",
|
|
20
|
+
"command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/permission-request.sh\"",
|
|
21
|
+
"timeout": 900
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
],
|
|
14
26
|
"PermissionRequest": [
|
|
15
27
|
{
|
|
16
28
|
"hooks": [
|
package/src/adapter.ts
CHANGED
|
@@ -89,12 +89,15 @@ export interface TerminalAttachSpec {
|
|
|
89
89
|
ttlMs?: number;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
export type ProviderPermissionDecision = "approve" | "approve-session" | "deny" | "abort";
|
|
92
|
+
export type ProviderPermissionDecision = "approve" | "approve-session" | "deny" | "abort" | "answer";
|
|
93
93
|
|
|
94
94
|
export interface ProviderPermissionDecisionInput {
|
|
95
95
|
approvalId: string;
|
|
96
96
|
decision: ProviderPermissionDecision;
|
|
97
97
|
reason?: string;
|
|
98
|
+
// For "answer" decisions (Claude AskUserQuestion): maps each question's text
|
|
99
|
+
// to the chosen option label(s). Multi-select labels are comma-joined.
|
|
100
|
+
answers?: Record<string, string>;
|
|
98
101
|
}
|
|
99
102
|
|
|
100
103
|
export interface ProviderAdapter {
|
package/src/control-server.ts
CHANGED
|
@@ -197,9 +197,25 @@ async function handlePermissionRequest(
|
|
|
197
197
|
return Response.json(claudePermissionHookResponse(decision, body));
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
-
function claudePermissionApprovalView(id: string, body: Record<string, unknown>): Record<string, unknown> {
|
|
200
|
+
export function claudePermissionApprovalView(id: string, body: Record<string, unknown>): Record<string, unknown> {
|
|
201
201
|
const toolName = typeof body.tool_name === "string" ? body.tool_name : "Tool";
|
|
202
202
|
const toolInput = isRecord(body.tool_input) ? body.tool_input : {};
|
|
203
|
+
// AskUserQuestion is not a yes/no gate — it asks the user to pick answers.
|
|
204
|
+
// Surface the structured questions so the dashboard can render a form and
|
|
205
|
+
// return the selections via an "answer" decision (handled in the hook
|
|
206
|
+
// response below as a PreToolUse allow + updatedInput).
|
|
207
|
+
if (toolName === "AskUserQuestion" && Array.isArray(toolInput.questions) && toolInput.questions.length) {
|
|
208
|
+
const count = toolInput.questions.length;
|
|
209
|
+
return {
|
|
210
|
+
id,
|
|
211
|
+
provider: "claude",
|
|
212
|
+
kind: "questions",
|
|
213
|
+
title: count > 1 ? `Claude is asking ${count} questions` : "Claude is asking a question",
|
|
214
|
+
body: "",
|
|
215
|
+
questions: toolInput.questions,
|
|
216
|
+
choices: [],
|
|
217
|
+
};
|
|
218
|
+
}
|
|
203
219
|
const command = typeof toolInput.command === "string" ? toolInput.command : "";
|
|
204
220
|
const description = typeof toolInput.description === "string" ? toolInput.description : "";
|
|
205
221
|
const bodyText = [
|
|
@@ -222,7 +238,30 @@ function claudePermissionApprovalView(id: string, body: Record<string, unknown>)
|
|
|
222
238
|
};
|
|
223
239
|
}
|
|
224
240
|
|
|
225
|
-
function claudePermissionHookResponse(decision: ProviderPermissionDecisionInput, body: Record<string, unknown>): Record<string, unknown> {
|
|
241
|
+
export function claudePermissionHookResponse(decision: ProviderPermissionDecisionInput, body: Record<string, unknown>): Record<string, unknown> {
|
|
242
|
+
// AskUserQuestion comes through a PreToolUse hook. The only way to satisfy it
|
|
243
|
+
// headlessly is permissionDecision "allow" + updatedInput carrying the answers
|
|
244
|
+
// (echoing back the original questions). A bare "allow" is not sufficient, so
|
|
245
|
+
// anything that is not a populated answer is treated as a deny.
|
|
246
|
+
if (body.hook_event_name === "PreToolUse") {
|
|
247
|
+
const toolInput = isRecord(body.tool_input) ? body.tool_input : {};
|
|
248
|
+
if (decision.decision === "answer" && decision.answers && Object.keys(decision.answers).length) {
|
|
249
|
+
return {
|
|
250
|
+
hookSpecificOutput: {
|
|
251
|
+
hookEventName: "PreToolUse",
|
|
252
|
+
permissionDecision: "allow",
|
|
253
|
+
updatedInput: { ...toolInput, answers: decision.answers },
|
|
254
|
+
},
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
return {
|
|
258
|
+
hookSpecificOutput: {
|
|
259
|
+
hookEventName: "PreToolUse",
|
|
260
|
+
permissionDecision: "deny",
|
|
261
|
+
permissionDecisionReason: decision.reason || "Dismissed from Agent Relay dashboard",
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
}
|
|
226
265
|
const hookEventName = "PermissionRequest";
|
|
227
266
|
if (decision.decision === "approve" || decision.decision === "approve-session") {
|
|
228
267
|
const suggestions = Array.isArray(body.permission_suggestions) ? body.permission_suggestions : [];
|
package/src/runner.ts
CHANGED
|
@@ -500,13 +500,23 @@ export class AgentRunner {
|
|
|
500
500
|
const approvalId = typeof params.approvalId === "string" ? params.approvalId : "";
|
|
501
501
|
const decision = typeof params.decision === "string" ? params.decision : "";
|
|
502
502
|
if (!approvalId) throw new Error("approvalId required");
|
|
503
|
-
if (decision !== "approve" && decision !== "approve-session" && decision !== "deny" && decision !== "abort") {
|
|
504
|
-
throw new Error("decision must be approve, approve-session, deny, or
|
|
503
|
+
if (decision !== "approve" && decision !== "approve-session" && decision !== "deny" && decision !== "abort" && decision !== "answer") {
|
|
504
|
+
throw new Error("decision must be approve, approve-session, deny, abort, or answer");
|
|
505
|
+
}
|
|
506
|
+
const answersRaw = params.answers;
|
|
507
|
+
const answers = answersRaw && typeof answersRaw === "object" && !Array.isArray(answersRaw)
|
|
508
|
+
? Object.fromEntries(
|
|
509
|
+
Object.entries(answersRaw as Record<string, unknown>).filter(([, v]) => typeof v === "string") as [string, string][],
|
|
510
|
+
)
|
|
511
|
+
: undefined;
|
|
512
|
+
if (decision === "answer" && (!answers || Object.keys(answers).length === 0)) {
|
|
513
|
+
throw new Error("answers required for answer decision");
|
|
505
514
|
}
|
|
506
515
|
const input: ProviderPermissionDecisionInput = {
|
|
507
516
|
approvalId,
|
|
508
517
|
decision: decision as ProviderPermissionDecision,
|
|
509
518
|
...(typeof params.reason === "string" ? { reason: params.reason } : {}),
|
|
519
|
+
...(answers ? { answers } : {}),
|
|
510
520
|
};
|
|
511
521
|
if (this.control?.resolvePermissionDecision(input)) {
|
|
512
522
|
return { approvalId, decision, provider: "claude" };
|