fix(grok-local): stop defaulting --permission-mode to dontAsk (#11898)

## 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 <noreply@paperclip.ing>
This commit is contained in:
Devin Foley 2026-08-21 14:50:43 -07:00 committed by GitHub
parent 69590890d4
commit fbd20b28d3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 45 additions and 4 deletions

View File

@ -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"

View File

@ -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");

View File

@ -203,7 +203,11 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
);
const command = asString(config.command, "grok");
const model = asString(config.model, DEFAULT_GROK_LOCAL_MODEL).trim();
const permissionMode = asString(config.permissionMode, "dontAsk").trim() || "dontAsk";
// No default permission mode: Grok >= 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);