Summary
bun run script/httpapi-exercise.ts --mode effect intermittently fails the v2.session.permission.create scenario. Reproduced twice in a row in CI on PR #208 (unrelated PR). Only flaky in effect mode, which runs against the real Effect runtime — coverage and auth modes pass reliably (they don't exercise the real async plugin-loading path).
Root cause
Real race in application code, not a test-harness artifact.
PermissionV2.ask (packages/core/src/permission.ts) evaluates a permission request against the calling agent's configured ruleset (agents.resolve(...).permissions). The default agent's ruleset — including the { action: "read", resource: "*.env", effect: "ask" } rule the test exercises — is registered by AgentPlugin/ConfigAgentPlugin, which PluginInternal (packages/core/src/plugin/internal.ts) runs in a forked, non-blocking fiber immediately after a location is built, deliberately not blocking the location layer itself (see the doc comment on PluginInternal.Service).
This leaves a real window, right after a session/location is first created, where AgentV2.Service's agent registry is still empty. A permission check that races this window sees no matching agent, falls back to missingAgentPermissions (deny-all), and returns effect: "deny" instead of the expected effect: "ask" — which is exactly the assertion that fails:
Error: permission create should create a pending request
at check (test/server/httpapi-exercise/assertions.ts:47:25)
This is the same general hazard as #205/#206 (scope-leak) and #207 (tight poll timeout), but a distinct root cause: a genuine ordering race between plugin registration and permission evaluation, not a test-harness state leak or an overly tight timeout.
The codebase already has an established pattern for this exact hazard: both packages/opencode/src/session/prompt.ts and packages/opencode/src/server/routes/instance/httpapi/handlers/provider.ts await PluginInternal.Service.ready (bounded by a 5s timeout) before reading state that plugins populate.
Fix
Apply the same pattern to the session.permission.create HTTP handler (packages/server/src/handlers/permission.ts): await PluginInternal.Service.ready (bounded, 5s timeout) before calling permission.ask(...).
(Note: the equivalent wait was not added inside packages/core/src/permission.ts itself — doing so there creates a real ES module circular-import cycle among permission.ts / plugin/internal.ts / plugin/agent.ts / skill.ts, which crashes at startup with ReferenceError: Cannot access 'node' before initialization. Waiting at the HTTP handler layer, where PluginInternal.Service is already available via the session's location context, avoids the cycle entirely.)
Verification
v2.session.permission.create passes 10/10 in --mode effect.
--mode coverage --fail-on-missing --fail-on-skip: 245/245 pass.
--mode auth --fail-on-missing --fail-on-skip: 245/245 pass.
bun turbo typecheck: passes across all packages.
🤖 Generated with Claude Code
Summary
bun run script/httpapi-exercise.ts --mode effectintermittently fails thev2.session.permission.createscenario. Reproduced twice in a row in CI on PR #208 (unrelated PR). Only flaky ineffectmode, which runs against the real Effect runtime —coverageandauthmodes pass reliably (they don't exercise the real async plugin-loading path).Root cause
Real race in application code, not a test-harness artifact.
PermissionV2.ask(packages/core/src/permission.ts) evaluates a permission request against the calling agent's configured ruleset (agents.resolve(...).permissions). The default agent's ruleset — including the{ action: "read", resource: "*.env", effect: "ask" }rule the test exercises — is registered byAgentPlugin/ConfigAgentPlugin, whichPluginInternal(packages/core/src/plugin/internal.ts) runs in a forked, non-blocking fiber immediately after a location is built, deliberately not blocking the location layer itself (see the doc comment onPluginInternal.Service).This leaves a real window, right after a session/location is first created, where
AgentV2.Service's agent registry is still empty. A permission check that races this window sees no matching agent, falls back tomissingAgentPermissions(deny-all), and returnseffect: "deny"instead of the expectedeffect: "ask"— which is exactly the assertion that fails:This is the same general hazard as #205/#206 (scope-leak) and #207 (tight poll timeout), but a distinct root cause: a genuine ordering race between plugin registration and permission evaluation, not a test-harness state leak or an overly tight timeout.
The codebase already has an established pattern for this exact hazard: both
packages/opencode/src/session/prompt.tsandpackages/opencode/src/server/routes/instance/httpapi/handlers/provider.tsawaitPluginInternal.Service.ready(bounded by a 5s timeout) before reading state that plugins populate.Fix
Apply the same pattern to the
session.permission.createHTTP handler (packages/server/src/handlers/permission.ts): awaitPluginInternal.Service.ready(bounded, 5s timeout) before callingpermission.ask(...).(Note: the equivalent wait was not added inside
packages/core/src/permission.tsitself — doing so there creates a real ES module circular-import cycle amongpermission.ts/plugin/internal.ts/plugin/agent.ts/skill.ts, which crashes at startup withReferenceError: Cannot access 'node' before initialization. Waiting at the HTTP handler layer, wherePluginInternal.Serviceis already available via the session's location context, avoids the cycle entirely.)Verification
v2.session.permission.createpasses 10/10 in--mode effect.--mode coverage --fail-on-missing --fail-on-skip: 245/245 pass.--mode auth --fail-on-missing --fail-on-skip: 245/245 pass.bun turbo typecheck: passes across all packages.🤖 Generated with Claude Code