Skip to content

Commit 6140b8f

Browse files
committed
feat: enterprise governance — yolo deny-rule enforcement and --max-turns budget
- Fix yolo mode to respect explicit deny rules from session config instead of blindly auto-approving all permissions. Deny rules now block even in yolo mode with a clear "BLOCKED by deny rule" message. - Add --max-turns CLI flag for CI/headless budget enforcement. Aborts the session when the assistant exceeds the configured turn limit, preventing runaway agents from burning API credits indefinitely. Addresses enterprise governance gaps identified by platform eng review: teams of 15+ engineers running CI pipelines need spend controls and safety guarantees that yolo mode won't bypass critical deny rules. https://claude.ai/code/session_01M6rR2wXn4PfMoUASy1qghV
1 parent 3741afc commit 6140b8f

1 file changed

Lines changed: 53 additions & 9 deletions

File tree

  • packages/opencode/src/cli/cmd

packages/opencode/src/cli/cmd/run.ts

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,12 @@ export const RunCommand = cmd({
351351
describe: "enable session tracing (default: true, disable with --no-trace)",
352352
default: true,
353353
})
354+
// altimate_change start — budget limits for CI/enterprise governance
355+
.option("max-turns", {
356+
type: "number",
357+
describe: "maximum number of assistant turns before aborting the session",
358+
})
359+
// altimate_change end
354360
},
355361
handler: async (args) => {
356362
let message = [...args.message, ...(args["--"] || [])]
@@ -549,6 +555,10 @@ You are speaking to a non-technical business executive. Follow these rules stric
549555

550556
async function loop() {
551557
const toggles = new Map<string, boolean>()
558+
// altimate_change start — max-turns budget enforcement
559+
let turnCount = 0
560+
const maxTurns = args.maxTurns
561+
// altimate_change end
552562

553563
for await (const event of events.stream) {
554564
if (
@@ -603,6 +613,18 @@ You are speaking to a non-technical business executive. Follow these rules stric
603613

604614
if (part.type === "step-start") {
605615
tracer?.logStepStart(part)
616+
// altimate_change start — enforce max-turns budget
617+
turnCount++
618+
if (maxTurns && turnCount > maxTurns) {
619+
error = `Budget exceeded: reached ${maxTurns} assistant turn${maxTurns !== 1 ? "s" : ""} limit`
620+
UI.println(
621+
UI.Style.TEXT_DANGER_BOLD + "!",
622+
UI.Style.TEXT_NORMAL + ` ${error}. Aborting session.`,
623+
)
624+
await sdk.session.abort({ sessionID })
625+
break
626+
}
627+
// altimate_change end
606628
if (emit("step_start", { part })) continue
607629
}
608630

@@ -664,18 +686,40 @@ You are speaking to a non-technical business executive. Follow these rules stric
664686
if (event.type === "permission.asked") {
665687
const permission = event.properties
666688
if (permission.sessionID !== sessionID) continue
667-
// altimate_change start - yolo mode: auto-approve instead of auto-reject
689+
// altimate_change start - yolo mode: auto-approve but respect explicit deny rules
668690
const yolo = args.yolo || Flag.ALTIMATE_CLI_YOLO
669691
if (yolo) {
670-
UI.println(
671-
UI.Style.TEXT_WARNING_BOLD + "!",
672-
UI.Style.TEXT_NORMAL +
673-
`yolo mode: auto-approved ${permission.permission} (${permission.patterns.join(", ")})`,
692+
// Check if any pattern matches an explicit deny rule from the session config
693+
const isDenied = rules.some(
694+
(r) =>
695+
r.action === "deny" &&
696+
r.permission === permission.permission &&
697+
permission.patterns.some((p) => {
698+
if (r.pattern === "*") return true
699+
return p.includes(r.pattern) || r.pattern.includes(p)
700+
}),
674701
)
675-
await sdk.permission.reply({
676-
requestID: permission.id,
677-
reply: "once",
678-
})
702+
if (isDenied) {
703+
UI.println(
704+
UI.Style.TEXT_DANGER_BOLD + "!",
705+
UI.Style.TEXT_NORMAL +
706+
`yolo mode: BLOCKED by deny rule: ${permission.permission} (${permission.patterns.join(", ")})`,
707+
)
708+
await sdk.permission.reply({
709+
requestID: permission.id,
710+
reply: "reject",
711+
})
712+
} else {
713+
UI.println(
714+
UI.Style.TEXT_WARNING_BOLD + "!",
715+
UI.Style.TEXT_NORMAL +
716+
`yolo mode: auto-approved ${permission.permission} (${permission.patterns.join(", ")})`,
717+
)
718+
await sdk.permission.reply({
719+
requestID: permission.id,
720+
reply: "once",
721+
})
722+
}
679723
} else {
680724
UI.println(
681725
UI.Style.TEXT_WARNING_BOLD + "!",

0 commit comments

Comments
 (0)