Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions packages/opencode/src/cli/cmd/attach.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,21 @@ export const AttachCommand = cmd({
describe: "start the minimal interactive interface",
default: false,
})
.option("auto", {
type: "boolean",
describe: "auto-approve permissions that are not explicitly denied (dangerous!)",
default: false,
})
.option("yolo", {
type: "boolean",
hidden: true,
default: false,
})
.option("dangerously-skip-permissions", {
type: "boolean",
hidden: true,
default: false,
})
.option("replay", {
type: "boolean",
hidden: true,
Expand Down Expand Up @@ -90,6 +105,7 @@ export const AttachCommand = cmd({
fork: args.fork,
replay: noReplay ? false : undefined,
replayLimit: args.replayLimit,
auto: args.auto || args.yolo || args["dangerously-skip-permissions"],
})
return
}
Expand Down Expand Up @@ -139,6 +155,7 @@ export const AttachCommand = cmd({
continue: args.continue,
sessionID: args.session,
fork: args.fork,
auto: args.auto || args.yolo || args["dangerously-skip-permissions"],
},
directory,
headers,
Expand Down
5 changes: 4 additions & 1 deletion packages/opencode/src/cli/cmd/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -892,6 +892,7 @@ export const RunCommand = effectCmd({
thinking,
backgroundSubagents: flags.experimentalBackgroundSubagents,
demo: args.demo,
auto,
})
} catch (error) {
dieInteractive(error)
Expand Down Expand Up @@ -929,6 +930,7 @@ export const RunCommand = effectCmd({
thinking,
backgroundSubagents: flags.experimentalBackgroundSubagents,
demo: args.demo,
auto,
})
} catch (error) {
dieInteractive(error)
Expand Down Expand Up @@ -972,6 +974,7 @@ type MiniCommandInput = {
replay?: boolean
replayLimit?: number
demo?: boolean
auto?: boolean
}

export async function runMini(input: MiniCommandInput) {
Expand Down Expand Up @@ -1002,7 +1005,7 @@ export async function runMini(input: MiniCommandInput) {
replay: input.replay ?? true,
"replay-limit": input.replayLimit,
replayLimit: input.replayLimit,
auto: false,
auto: input.auto ?? false,
yolo: false,
"dangerously-skip-permissions": false,
dangerouslySkipPermissions: false,
Expand Down
5 changes: 5 additions & 0 deletions packages/opencode/src/cli/cmd/run/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ type RunRuntimeInput = {
replay?: boolean
replayLimit?: number
demo?: RunInput["demo"]
auto?: boolean
}

type RunLocalInput = {
Expand All @@ -74,6 +75,7 @@ type RunLocalInput = {
replay?: boolean
replayLimit?: number
demo?: RunInput["demo"]
auto?: boolean
}

type StreamTransportModule = Pick<
Expand Down Expand Up @@ -483,6 +485,7 @@ async function runInteractiveRuntime(input: RunRuntimeInput, deps: RunRuntimeDep
providers: () => state.providers,
footer,
trace: log,
auto: input.auto,
})
if (footer.isClosed) {
await handle.close()
Expand Down Expand Up @@ -748,6 +751,7 @@ export async function runInteractiveLocalMode(input: RunLocalInput): Promise<voi
replay: input.replay,
replayLimit: input.replayLimit,
demo: input.demo,
auto: input.auto,
resolveSession: () => {
if (session) {
return session
Expand Down Expand Up @@ -797,6 +801,7 @@ export async function runInteractiveMode(
replay: input.replay,
replayLimit: input.replayLimit,
demo: input.demo,
auto: input.auto,
boot: async () => ({
sdk: input.sdk,
directory: input.directory,
Expand Down
11 changes: 11 additions & 0 deletions packages/opencode/src/cli/cmd/run/stream.transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ type StreamInput = {
footer: FooterApi
trace?: Trace
signal?: AbortSignal
// Auto-approve (YOLO): reply "once" to every permission request instead of
// surfacing a prompt. Questions still block; explicit deny rules still win.
auto?: boolean
}

type Wait = {
Expand Down Expand Up @@ -522,6 +525,14 @@ function createLayer(input: StreamInput) {
return
}

// YOLO: auto-approve permission requests without surfacing a prompt.
// Questions are left to the user; explicit deny rules already reject
// server-side before a permission.asked is ever emitted.
if (input.auto && event.type === "permission.asked") {
void input.sdk.permission.reply({ requestID: event.properties.id, reply: "once" }).catch(() => {})
return
}

Comment on lines +528 to +535

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="packages/opencode/src/cli/cmd/run/stream.transport.ts"

echo "== outline =="
ast-grep outline "$FILE" --view expanded || true

echo
echo "== line count =="
wc -l "$FILE"

echo
echo "== relevant slice around 480-620 =="
sed -n '480,620p' "$FILE" | cat -n

Repository: CreatorGhost/TheCode

Length of output: 7901


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="packages/opencode/src/cli/cmd/run/stream.transport.ts"

echo "== search for blocker/permission handling =="
rg -n "permission|blocker|seedBlocker|trackBlocker|auto" "$FILE"

echo
echo "== broader related symbols in file =="
sed -n '1,260p' "$FILE" | cat -n

Repository: CreatorGhost/TheCode

Length of output: 12163


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="packages/opencode/src/cli/cmd/run/stream.transport.ts"

python3 - <<'PY'
from pathlib import Path
p = Path("packages/opencode/src/cli/cmd/run/stream.transport.ts")
text = p.read_text()
for needle in [
    "seedBlocker(",
    "trackBlocker(",
    "applyEvent(",
    "bootstrap(",
    "replayOnResize(",
    "permission.reply",
]:
    idxs = [i for i in range(len(text)) if text.startswith(needle, i)]
    print(f"{needle}: {len(idxs)}")
    for i in idxs[:5]:
        line = text.count("\n", 0, i) + 1
        print("  line", line)
PY

Repository: CreatorGhost/TheCode

Length of output: 397


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="packages/opencode/src/cli/cmd/run/stream.transport.ts"

# Show the exact implementation around permission handling and bootstrapping.
sed -n '260,420p' "$FILE" | cat -n
echo
sed -n '420,620p' "$FILE" | cat -n

Repository: CreatorGhost/TheCode

Length of output: 12835


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="packages/opencode/src/cli/cmd/run/stream.transport.ts"

echo "== bootstrap permissions around 600-640 =="
sed -n '600,640p' "$FILE" | cat -n

echo
echo "== replay permissions around 790-815 =="
sed -n '790,815p' "$FILE" | cat -n

echo
echo "== resize restore around 1118-1140 =="
sed -n '1118,1140p' "$FILE" | cat -n

echo
echo "== applyEvent call site around 920-1015 =="
sed -n '920,1015p' "$FILE" | cat -n

echo
echo "== replayOnResize implementation around 1038-1088 =="
sed -n '1038,1088p' "$FILE" | cat -n

Repository: CreatorGhost/TheCode

Length of output: 10695


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="packages/opencode/src/cli/cmd/run/stream.transport.ts"

python3 - <<'PY'
from pathlib import Path
p = Path("packages/opencode/src/cli/cmd/run/stream.transport.ts")
lines = p.read_text().splitlines()
for start, end, label in [
    (600, 640, "bootstrap permissions"),
    (790, 815, "replay permissions"),
    (1118, 1140, "resize restore"),
    (920, 1015, "applyEvent call site"),
    (1038, 1088, "replayOnResize implementation"),
]:
    print(f"== {label} ==")
    for i in range(start-1, min(end, len(lines))):
        print(f"{i+1:5d}: {lines[i]}")
    print()
PY

Repository: CreatorGhost/TheCode

Length of output: 10616


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="packages/opencode/src/cli/cmd/run/stream.transport.ts"

python3 - <<'PY'
from pathlib import Path
p = Path("packages/opencode/src/cli/cmd/run/stream.transport.ts")
lines = p.read_text().splitlines()
for start, end, label in [
    (620, 640, "bootstrap permissions"),
    (798, 810, "replay permissions"),
    (1124, 1136, "resize restore"),
    (930, 950, "applyEvent call site"),
    (1040, 1080, "replayOnResize implementation"),
]:
    print(f"== {label} ==")
    for i in range(start-1, min(end, len(lines))):
        print(f"{i+1:5d}: {lines[i]}")
    print()
PY

Repository: CreatorGhost/TheCode

Length of output: 5041


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect how permission.asked / permission.replied are reduced into SessionData.
FILE="packages/opencode/src/cli/cmd/run/session-data.ts"

echo "== outline =="
ast-grep outline "$FILE" --view expanded || true

echo
echo "== relevant permission-related slices =="
rg -n "permission\.asked|permission\.replied|question\.asked|question\.replied|question\.rejected|blocker|permissions|questions" "$FILE"

echo
python3 - <<'PY'
from pathlib import Path
p = Path("packages/opencode/src/cli/cmd/run/session-data.ts")
lines = p.read_text().splitlines()
for start, end in [(1, 260), (260, 520)]:
    print(f"== lines {start}-{end} ==")
    for i in range(start-1, min(end, len(lines))):
        print(f"{i+1:5d}: {lines[i]}")
    print()
PY

Repository: CreatorGhost/TheCode

Length of output: 22229


Auto-reply existing permission blockers and handle reply failures

  • Swallowing permission.reply(...) errors leaves the request pending with no retry path; if the auto-reply fails, the session stays blocked.
  • --auto/--yolo only applies to live permission.asked events. Pending permissions seeded during bootstrap/replay still bypass auto-approval, so resumed sessions can remain blocked.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/opencode/src/cli/cmd/run/stream.transport.ts` around lines 528 -
535, Update the auto-approval flow around input.sdk.permission.reply so
--auto/--yolo also processes permission requests already pending from bootstrap
or replay, not only live permission.asked events. Handle reply failures with the
session’s retry or error-propagation path instead of swallowing them, ensuring
failed requests do not remain silently blocked.

seedBlocker(event.properties.id)
}

Expand Down
1 change: 1 addition & 0 deletions packages/opencode/src/cli/cmd/run/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export type RunInput = {
thinking: boolean
backgroundSubagents: boolean
demo?: boolean
auto?: boolean
}

// The semantic role of a scrollback entry. Maps 1:1 to theme colors.
Expand Down
1 change: 1 addition & 0 deletions packages/opencode/src/cli/cmd/tui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ export const TuiThreadCommand = cmd({
replay: noReplay ? false : undefined,
replayLimit: args.replayLimit,
demo: args.demo,
auto: args.auto || args.yolo || args["dangerously-skip-permissions"],
})
return
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ Options:
-u, --username basic auth username (defaults to OPENCODE_SERVER_USERNAME or 'opencode')
[string]
--mini start the minimal interactive interface [boolean] [default: false]
--auto auto-approve permissions that are not explicitly denied (dangerous!)
[boolean] [default: false]
--no-replay disable mini session history replay on resume and after resize [boolean]
--replay-limit cap visible mini replay to the newest N messages [number]"
`;
Expand Down
63 changes: 63 additions & 0 deletions packages/opencode/test/cli/run/stream.transport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,69 @@ describe("run stream transport", () => {
}
})

test("auto mode replies once to permission requests without prompting", async () => {
const src = eventFeed()
const ui = footer()
const client = sdk({ stream: src.stream })
const replySpy = spyOn(client.permission, "reply").mockImplementation(() => ok(undefined) as never)
const transport = await createSessionTransport({
sdk: client,
sessionID: "session-1",
thinking: true,
limits: () => ({}),
footer: ui.api,
auto: true,
})

try {
// Processed in order: a foreign-session permission and a question are pushed
// BEFORE the real request, so by the time the real reply fires they've already
// been handled — proving neither was auto-approved.
src.push({
id: "evt-perm-foreign",
type: "permission.asked",
properties: {
id: "perm-foreign",
sessionID: "session-other",
permission: "edit",
patterns: ["src/other.ts"],
metadata: {},
always: [],
},
} satisfies SdkEvent)
src.push({
id: "evt-question-1",
type: "question.asked",
properties: {
id: "que-1",
sessionID: "session-1",
questions: [{ question: "Proceed?", header: "confirm", options: [{ label: "Yes", description: "go" }] }],
},
} satisfies SdkEvent)
src.push({
id: "evt-perm-1",
type: "permission.asked",
properties: {
id: "perm-1",
sessionID: "session-1",
permission: "edit",
patterns: ["src/foo.ts"],
metadata: {},
always: [],
},
} satisfies SdkEvent)

await waitFor(() => (replySpy.mock.calls.length > 0 ? true : undefined))
// Only the in-session permission is auto-approved; questions still block and a
// foreign session is never auto-replied.
expect(replySpy).toHaveBeenCalledTimes(1)
expect(replySpy).toHaveBeenCalledWith({ requestID: "perm-1", reply: "once" })
} finally {
src.close()
await transport.close()
}
})

test("rebuilds session output on resize and continues live deltas from replayed state", async () => {
const src = eventFeed()
const ui = footer()
Expand Down
Loading