From fbd20b28d31c7702180bacc1de95ddc7028b4246 Mon Sep 17 00:00:00 2001 From: Devin Foley Date: Fri, 21 Aug 2026 14:50:43 -0700 Subject: [PATCH] fix(grok-local): stop defaulting --permission-mode to dontAsk (#11898) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - The `grok_local` adapter runs the native Grok Build CLI in headless mode for unattended agent heartbeats > - Grok CLI 1.0 started to enforce the `dontAsk` permission mode as deny-by-default, and it takes precedence over `--always-approve` > - The adapter passes both flags on every run, so each run dies on its first tool call and is still recorded as a success > - This pull request removes the `dontAsk` default so unattended runs rely on `--always-approve` alone > - The benefit is that `grok_local` agents can execute tools again on current Grok CLI releases ## Linked Issues or Issue Description No public issue exists. Description per the bug template: **What happened?** Every `grok_local` run on Grok CLI 1.0.x stops on its first tool call. The stream shows the tool call move from `pending` to `failed` with "User cancelled the execution for tool `run_terminal_command`", and the session ends with `stopReason: "cancelled"` after one turn. The CLI exits 0, so Paperclip records the run as succeeded with no work done, and the issue lands in missing-disposition recovery. **Expected behavior** Unattended runs must auto-approve tool executions. The adapter already passes `--always-approve` for this. **Steps to reproduce** In a clean Linux environment with Grok CLI 1.0.3 and `XAI_API_KEY` set, run the adapter's exact invocation shape: `grok --output-format streaming-json --permission-mode dontAsk --always-approve --disable-web-search --single "Run the shell command: echo ok"` The tool call is denied. Drop `--permission-mode dontAsk` (or use `--permission-mode bypassPermissions`) and the same command executes the tool. On Grok 0.2.x the original combination worked because the CLI accepted `dontAsk` without enforcing it; the 0.2.39 embedded docs state the flag takes effect only for `bypassPermissions` / always-approve. **Paperclip version or commit** master (917d2350f) ## What Changed - `packages/adapters/grok-local/src/server/execute.ts`: `permissionMode` no longer defaults to `dontAsk`. The adapter passes no `--permission-mode` flag unless one is explicitly configured. `--always-approve` (default on) remains the unattended policy. - `packages/adapters/grok-local/src/index.ts`: config doc updated to explain the new default and the Grok 1.0 semantics. - `packages/adapters/grok-local/src/server/execute.test.ts`: default-args assertion now requires the absence of `--permission-mode`; new test covers explicit `permissionMode` pass-through. ## Verification - `npx vitest run packages/adapters/grok-local` — 7 files, 29 tests, all pass. - `pnpm --filter @paperclipai/adapter-grok-local typecheck` — clean. - Live matrix against Grok CLI 1.0.3 in a clean sandbox: `dontAsk --always-approve` denies the first tool call; `--always-approve` alone executes it; `bypassPermissions --always-approve` executes it; `dontAsk` alone denies it. ## Risks - Low risk. Operators who explicitly set `permissionMode` keep their value verbatim. Only the implicit default changes, and the old default is what breaks every run on current Grok CLI releases. - On Grok 0.2.x the flag was unenforced, so omitting it does not change behavior there. ## Model Used Claude Fable 5 (`claude-fable-5`), extended thinking, tool use, via Claude Code CLI. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge Co-authored-by: Paperclip --- packages/adapters/grok-local/src/index.ts | 2 +- .../grok-local/src/server/execute.test.ts | 41 ++++++++++++++++++- .../adapters/grok-local/src/server/execute.ts | 6 ++- 3 files changed, 45 insertions(+), 4 deletions(-) diff --git a/packages/adapters/grok-local/src/index.ts b/packages/adapters/grok-local/src/index.ts index 9e50feed8f..492464edd9 100644 --- a/packages/adapters/grok-local/src/index.ts +++ b/packages/adapters/grok-local/src/index.ts @@ -26,7 +26,7 @@ Core fields: - instructionsFilePath (string, optional): absolute path to a markdown instructions file. Paperclip stages it into the execution workspace as \`Agents.md\` when safe, otherwise falls back to \`--rules @file\` - promptTemplate (string, optional): run prompt template - model (string, optional): Grok model id. Defaults to grok-build. -- permissionMode (string, optional): Grok permission mode. Defaults to \`dontAsk\` +- permissionMode (string, optional): Grok permission mode passed via \`--permission-mode\`. Unset by default: Grok >= 1.0 enforces \`dontAsk\` as deny-by-default and it overrides \`--always-approve\`, so unattended runs rely on \`--always-approve\` alone unless you explicitly need a mode - reasoningEffort (string, optional): Grok reasoning effort passed via \`--reasoning-effort\` - maxTurns (number, optional): maximum agent turns for the run - command (string, optional): defaults to "grok" diff --git a/packages/adapters/grok-local/src/server/execute.test.ts b/packages/adapters/grok-local/src/server/execute.test.ts index 2b03c7dcbd..46899fec54 100644 --- a/packages/adapters/grok-local/src/server/execute.test.ts +++ b/packages/adapters/grok-local/src/server/execute.test.ts @@ -71,10 +71,11 @@ describe("grok_local execute", () => { "--output-format", "streaming-json", "--always-approve", - "--permission-mode", - "dontAsk", ]), ); + // Grok >= 1.0 enforces `dontAsk` as deny-by-default over --always-approve, + // so no permission mode may be passed unless explicitly configured. + expect(args).not.toContain("--permission-mode"); expect(await fs.readFile(path.join(root, "Agents.md"), "utf8")).toContain("You are Grok."); expect(await pathExists(path.join(root, ".claude", "skills", "paperclip", "SKILL.md"))).toBe(true); await options.onLog?.("stdout", '{"type":"text","data":"done"}\n'); @@ -203,6 +204,42 @@ describe("grok_local execute", () => { } }); + it("passes an explicitly configured permissionMode through to the CLI", async () => { + let seenArgs: string[] = []; + runProcessMock.mockImplementation(async (_runId, _target, _command, args) => { + seenArgs = args; + return { + exitCode: 0, + signal: null, + timedOut: false, + stdout: JSON.stringify({ type: "end", stopReason: "EndTurn", sessionId: "sess-1", requestId: "req-1" }), + stderr: "", + }; + }); + + const ctx: AdapterExecutionContext = { + runId: "run-permission-mode", + agent: { + id: "agent-1", + companyId: "company-1", + name: "Grok Agent", + adapterType: "grok_local", + adapterConfig: {}, + }, + runtime: { sessionId: null, sessionParams: null, sessionDisplayId: null, taskKey: null }, + config: { cwd: await makeTempRoot(), permissionMode: "bypassPermissions" }, + context: {}, + authToken: "run-token", + onLog: async () => {}, + }; + + await execute(ctx); + + const flagIndex = seenArgs.indexOf("--permission-mode"); + expect(flagIndex).toBeGreaterThan(-1); + expect(seenArgs[flagIndex + 1]).toBe("bypassPermissions"); + }); + it("cleans up staged assets when setup fails before the Grok process starts", async () => { const root = await makeTempRoot(); const instructionsPath = path.join(root, "managed", "AGENTS.md"); diff --git a/packages/adapters/grok-local/src/server/execute.ts b/packages/adapters/grok-local/src/server/execute.ts index e82ff1cec6..72ab25d6b0 100644 --- a/packages/adapters/grok-local/src/server/execute.ts +++ b/packages/adapters/grok-local/src/server/execute.ts @@ -203,7 +203,11 @@ export async function execute(ctx: AdapterExecutionContext): Promise= 1.0 enforces `dontAsk` as + // deny-by-default and it overrides --always-approve, so passing it broke + // every unattended run (the first tool call died with "User cancelled the + // execution for tool ..."). --always-approve alone is the unattended policy. + const permissionMode = asString(config.permissionMode, "").trim(); const reasoningEffort = asString(config.reasoningEffort, "").trim(); const maxTurns = asNumber(config.maxTurns, 0); const alwaysApprove = asBoolean(config.alwaysApprove, true);