Skip to content

Commit b5af460

Browse files
fix(opencode): apply runtime mode changes to existing OpenCode sessions (#26182)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 5cf49d2 commit b5af460

2 files changed

Lines changed: 89 additions & 10 deletions

File tree

‎packages/provider-opencode/src/server/adapter.test.ts‎

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ const makeOpenCodeRuntimeHarness = Effect.fn("makeOpenCodeRuntimeHarness")(funct
184184
const startTurn = (
185185
text = "hello",
186186
startProviderThread: OrchestrationV2ProviderThread = providerThread,
187+
turnPolicy: ProviderAdapter.ProviderAdapterV2RuntimePolicy = policy,
187188
) =>
188189
runtime.startTurn({
189190
appThread: {
@@ -224,7 +225,7 @@ const makeOpenCodeRuntimeHarness = Effect.fn("makeOpenCodeRuntimeHarness")(funct
224225
attachments: [],
225226
},
226227
modelSelection,
227-
runtimePolicy: policy,
228+
runtimePolicy: turnPolicy,
228229
});
229230
return {
230231
nativeSessionId,
@@ -239,6 +240,62 @@ const makeOpenCodeRuntimeHarness = Effect.fn("makeOpenCodeRuntimeHarness")(funct
239240
});
240241

241242
describe("OpenCodeAdapterV2", () => {
243+
it.effect("applies a changed runtime policy to the session on resume and turn start", () =>
244+
Effect.gen(function* () {
245+
const updates: Array<{ sessionID: string; permission: unknown }> = [];
246+
const fullAccess = runtimePolicy("full-access", { cwd: "/workspace" });
247+
const supervised = runtimePolicy("approval-required", { cwd: "/workspace" });
248+
const harness = yield* makeOpenCodeRuntimeHarness("policy-change", "root", {
249+
event: { subscribe: async () => ({ stream: asyncEventStream().stream }) },
250+
session: {
251+
create: async (input: { permission: unknown }) => ({
252+
data: {
253+
id: "root",
254+
permission: input.permission,
255+
time: { created: 1, updated: 1 },
256+
},
257+
}),
258+
get: async () => ({
259+
data: {
260+
id: "root",
261+
permission: openCodePermissionRules(fullAccess),
262+
time: { created: 1, updated: 1 },
263+
},
264+
}),
265+
update: async (input: { sessionID: string; permission: unknown }) => {
266+
updates.push(input);
267+
return { data: { id: input.sessionID, time: { created: 1, updated: 2 } } };
268+
},
269+
promptAsync: async () => ({ data: true }),
270+
abort: async () => ({ data: true }),
271+
children: async () => ({ data: [] }),
272+
},
273+
});
274+
275+
yield* harness.runtime.resumeThread({
276+
providerThread: harness.providerThread,
277+
runtimePolicy: fullAccess,
278+
});
279+
assert.deepStrictEqual(updates, []);
280+
281+
yield* harness.runtime.resumeThread({
282+
providerThread: harness.providerThread,
283+
runtimePolicy: supervised,
284+
});
285+
assert.deepStrictEqual(updates, [
286+
{ sessionID: "root", permission: openCodePermissionRules(supervised) },
287+
]);
288+
289+
yield* harness.startTurn("hello", harness.providerThread, fullAccess);
290+
assert.deepStrictEqual(updates.slice(1), [
291+
{ sessionID: "root", permission: openCodePermissionRules(fullAccess) },
292+
]);
293+
}).pipe(
294+
Effect.provide(Layer.merge(IdAllocator.layer, McpProviderSessions.layer)),
295+
Effect.scoped,
296+
),
297+
);
298+
242299
it.effect.each(["completed", "failed", "unavailable"] as const)(
243300
"normalizes OpenCode step usage for %s turns",
244301
(ending) =>

‎packages/provider-opencode/src/server/adapter.ts‎

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,7 @@ interface OpenCodeThreadState {
379379
parentSubagent: OpenCodeSubagentContext | null;
380380
nextChildTurnOrdinal: number;
381381
nextAdmissionGeneration: number;
382+
permission: PermissionRuleset;
382383
}
383384

384385
interface OpenCodeRequestOwner {
@@ -1395,6 +1396,7 @@ export const makeOpenCodeAdapterV2 = Effect.fn("makeOpenCodeAdapterV2")(function
13951396
parentSubagent: context,
13961397
nextChildTurnOrdinal: 1,
13971398
nextAdmissionGeneration: 1,
1399+
permission: childPermission,
13981400
});
13991401
yield* emitProviderEvent({
14001402
type: "app_thread.created",
@@ -2775,6 +2777,8 @@ export const makeOpenCodeAdapterV2 = Effect.fn("makeOpenCodeAdapterV2")(function
27752777
if (existing !== undefined) {
27762778
existing.providerThread = providerThread;
27772779
if (appThread !== null) existing.appThread = appThread;
2780+
if (nativeSession.permission !== undefined)
2781+
existing.permission = nativeSession.permission;
27782782
return existing;
27792783
}
27802784
const state: OpenCodeThreadState = {
@@ -2789,11 +2793,29 @@ export const makeOpenCodeAdapterV2 = Effect.fn("makeOpenCodeAdapterV2")(function
27892793
parentSubagent: subagentsByChildSessionId.get(nativeSession.id) ?? null,
27902794
nextChildTurnOrdinal: 1,
27912795
nextAdmissionGeneration: 1,
2796+
permission: nativeSession.permission ?? [],
27922797
};
27932798
threads.set(nativeSession.id, state);
27942799
return state;
27952800
};
27962801

2802+
const applyPermissionRules = Effect.fnUntraced(function* (
2803+
state: OpenCodeThreadState,
2804+
policy: ProviderAdapter.ProviderAdapterV2RuntimePolicy,
2805+
) {
2806+
const permission = openCodePermissionRules(policy);
2807+
if (
2808+
permission.length === state.permission.length &&
2809+
permission.every((rule, index) => permissionRuleEquals(rule, state.permission[index]!))
2810+
) {
2811+
return;
2812+
}
2813+
yield* sdkCall("session.update", { sessionID: state.nativeSessionId, permission }, () =>
2814+
client.session.update({ sessionID: state.nativeSessionId, permission }),
2815+
);
2816+
state.permission = permission;
2817+
});
2818+
27972819
const resolvePromptParts = Effect.fnUntraced(function* (
27982820
turnInput: ProviderAdapter.ProviderAdapterV2TurnInput,
27992821
) {
@@ -2988,14 +3010,10 @@ export const makeOpenCodeAdapterV2 = Effect.fn("makeOpenCodeAdapterV2")(function
29883010
providerThread: threadInput.existingProviderThread,
29893011
});
29903012
}
3013+
const permission = openCodePermissionRules(threadInput.runtimePolicy);
29913014
// OpenCode generates a title only when session.create leaves it unset.
2992-
const response = yield* sdkCall(
2993-
"session.create",
2994-
{ permission: openCodePermissionRules(threadInput.runtimePolicy) },
2995-
() =>
2996-
client.session.create({
2997-
permission: openCodePermissionRules(threadInput.runtimePolicy),
2998-
}),
3015+
const response = yield* sdkCall("session.create", { permission }, () =>
3016+
client.session.create({ permission }),
29993017
);
30003018
const nativeSession = unwrapData("session.create", response);
30013019
const createdAt = yield* DateTime.now;
@@ -3019,7 +3037,7 @@ export const makeOpenCodeAdapterV2 = Effect.fn("makeOpenCodeAdapterV2")(function
30193037
status: created.status,
30203038
updatedAt: created.updatedAt,
30213039
};
3022-
registerThread(nativeSession, providerThread, null);
3040+
registerThread(nativeSession, providerThread, null).permission = permission;
30233041
return providerThread;
30243042
},
30253043
(effect, threadInput) =>
@@ -3048,7 +3066,10 @@ export const makeOpenCodeAdapterV2 = Effect.fn("makeOpenCodeAdapterV2")(function
30483066
status: "idle" as const,
30493067
updatedAt: dateTimeFromEpoch(nativeSession.time.updated, resumedAt),
30503068
};
3051-
registerThread(nativeSession, providerThread, null);
3069+
const state = registerThread(nativeSession, providerThread, null);
3070+
if (threadInput.runtimePolicy !== undefined) {
3071+
yield* applyPermissionRules(state, threadInput.runtimePolicy);
3072+
}
30523073
return providerThread;
30533074
},
30543075
(effect, threadInput) =>
@@ -3096,6 +3117,7 @@ export const makeOpenCodeAdapterV2 = Effect.fn("makeOpenCodeAdapterV2")(function
30963117
`OpenCode model '${turnInput.modelSelection.model}' must use provider/model format`,
30973118
);
30983119
}
3120+
yield* applyPermissionRules(state, turnInput.runtimePolicy);
30993121
const isCompaction =
31003122
turnInput.message.text.trim() === "/compact" &&
31013123
turnInput.message.attachments.length === 0;

0 commit comments

Comments
 (0)