diff --git a/packages/cli/src/run/noninteractive.ts b/packages/cli/src/run/noninteractive.ts index b42154600d8..5043b28ee5e 100644 --- a/packages/cli/src/run/noninteractive.ts +++ b/packages/cli/src/run/noninteractive.ts @@ -67,6 +67,9 @@ type FormRequest = Extract["data"]["form"] // attached client must not cancel input that may belong to another session. const GLOBAL_FORM_SESSION_ID = "global" +const PERMISSION_REJECTED_FEEDBACK = + "This non-interactive run cannot ask the user for permission, so the request was rejected. Continue without this action." + export async function runNonInteractivePrompt(input: Input) { const controller = new AbortController() const stream = input.client.event.subscribe({ signal: controller.signal })[Symbol.asyncIterator]() @@ -133,9 +136,11 @@ export async function runNonInteractivePrompt(input: Input) { } const replyPermission = async (request: { id: string; action: string; resources: ReadonlyArray }) => { + // Nobody can approve here. Outside V1 compatibility, reject with feedback so the tool fails + // as ordinary model-visible output and the model continues without the action. + const continuing = !input.auto && input.compatibility !== "v1" if (!input.auto) { - permissionRejected = true - if (input.compatibility !== "v1") process.exitCode = 1 + if (!continuing) permissionRejected = true UI.println( UI.Style.TEXT_WARNING_BOLD + "!", UI.Style.TEXT_NORMAL + @@ -147,9 +152,10 @@ export async function runNonInteractivePrompt(input: Input) { sessionID: input.sessionID, requestID: request.id, decision: input.auto ? "once" : "reject", + ...(continuing ? { message: PERMISSION_REJECTED_FEEDBACK } : {}), }) .catch(() => {}) - if (!input.auto) { + if (!input.auto && !continuing) { await input.client.session.interrupt({ sessionID: input.sessionID }).catch(() => {}) } } diff --git a/packages/core/src/session/to-session-error.ts b/packages/core/src/session/to-session-error.ts index fefc3813cb8..8698c8a5645 100644 --- a/packages/core/src/session/to-session-error.ts +++ b/packages/core/src/session/to-session-error.ts @@ -41,6 +41,7 @@ export function toSessionError(cause: unknown): SessionError.Error { } } if (cause instanceof Permission.BlockedError) return { type: "permission.rejected", message: cause.message } + if (cause instanceof Permission.CorrectedError) return { type: "permission.rejected", message: cause.feedback } if (cause instanceof ToolFailure || cause instanceof Tool.Error) { if (cause.error === undefined) return { type: "tool.execution", message: cause.message } // The canonical error is the sole model-visible representation, so a cause