refactor(browser): defer permission enforcement

This commit is contained in:
LukeParkerDev 2026-09-02 09:19:52 +10:00
parent 4d54418f14
commit 5eefeee0ce
3 changed files with 9 additions and 30 deletions

View file

@ -28,6 +28,9 @@ Control events use OpenCode's existing authenticated, server-wide event feed.
Consumers filter by `connectionID`; this identifier is correlation, not private
event delivery. State and results use RPC calls rather than broadcast events.
The plugin requests normal agent permissions before acting on a URL. Browser
content is untrusted. Pages use the desktop's network, with no server-side tunnel.
The desktop owns Chromium, page isolation, and native controls.
Per-URL permission checks are deferred to the final permission layer (#46530).
Until that layer lands, browser actions do not enforce URL-specific ask or deny
rules. Attachment ownership, page validation, and cancellation remain enforced.
Browser content is untrusted. Pages use the desktop's network, with no server-side
tunnel. The desktop owns Chromium, page isolation, and native controls.

View file

@ -95,20 +95,8 @@ export default Plugin.define({
Effect.gen(function* () {
const browser = browsers.get(tool.sessionID)
if (!browser) return yield* new Tool.Error({ message: "No desktop browser is connected." })
if (action.type !== "open") {
if (!browser.state) return yield* new Tool.Error({ message: "Open the browser first." })
const url = action.type === "navigate" ? action.url : browser.state.url
yield* ctx.permission
.assert({
action: "browser",
resources: [url],
metadata: { type: action.type, url },
sessionID: tool.sessionID,
agent: tool.agent,
source: { type: "tool", messageID: tool.messageID, id: tool.id },
})
.pipe(Effect.mapError((error) => new Tool.Error({ message: "Browser action failed", error })))
}
if (action.type !== "open" && !browser.state)
return yield* new Tool.Error({ message: "Open the browser first." })
const requestID = crypto.randomUUID()
const pending = yield* Deferred.make<Browser.Result, Tool.Error>()
browser.pending.set(requestID, pending)

View file

@ -29,14 +29,12 @@ const fixture = Effect.gen(function* () {
project: false,
content: JSON.stringify({
plugins: ["-opencode.browser"],
permissions: [{ action: "browser", resource: "*", effect: "allow" }],
}),
},
models: { fetch: false },
fs: { filewatcher: false, fff: false },
})
const captured = Promise.withResolvers<Tool.Info>()
const permissions: Array<{ action: string; resources: readonly string[] }> = []
yield* opencode.plugin({ ...plugin, id: "browser-test" })
yield* opencode.plugin({
id: "browser-test-observer",
@ -47,9 +45,6 @@ const fixture = Effect.gen(function* () {
const tool = draft.get("browser")
if (tool && ctx.location.directory === location.directory) captured.resolve(tool)
})
yield* ctx.permission.hook("evaluate", (event) =>
Effect.sync(() => permissions.push({ action: event.action, resources: event.resources })),
)
}).pipe(Effect.orDie),
})
yield* opencode.plugin.list({ location })
@ -80,7 +75,6 @@ const fixture = Effect.gen(function* () {
opencode,
location,
rpc,
permissions,
execute,
next,
attach: Effect.fn(function* (connectionID: string) {
@ -196,7 +190,7 @@ test(
)
test(
"commands use published state and permissions, and RPC results render text and screenshot bytes",
"commands use published state, and RPC results render text and screenshot bytes",
() =>
Effect.gen(function* () {
const host = yield* fixture
@ -212,7 +206,6 @@ test(
options,
)
expect((yield* Fiber.join(open.pending)).metadata).toEqual({ url: state.url })
expect(host.permissions).toEqual([])
yield* host.rpc.state({ ...attached.input, state }, options)
const navigate = yield* host.command({ type: "navigate", url: "https://example.org/next" })
@ -263,11 +256,6 @@ test(
],
metadata: { url: updated.url },
})
expect(host.permissions).toEqual([
{ action: "browser", resources: [updated.url] },
{ action: "browser", resources: [updated.url] },
{ action: "browser", resources: [updated.url] },
])
const failure = yield* host.command({ type: "snapshot" })
yield* host.rpc.result(
{ ...attached.input, requestID: failure.requestID, outcome: { type: "failure", message: "Stale document" } },