agent-relay-runner 0.11.5 → 0.11.8

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-relay-runner",
3
- "version": "0.11.5",
3
+ "version": "0.11.8",
4
4
  "description": "Unified provider lifecycle runner for Agent Relay",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-relay-runner",
3
3
  "description": "Thin Agent Relay runner bridge for Claude Code",
4
- "version": "0.11.5",
4
+ "version": "0.11.8",
5
5
  "agentRelayContracts": {
6
6
  "providerPluginProtocol": 1
7
7
  }
@@ -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 {
@@ -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 abort");
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" };