feat(opencode): guard refactor + browser tool sources and playwright-core dep

This commit is contained in:
2026-08-21 19:21:44 -05:00
parent db66655c0f
commit fa83b81a35
4 changed files with 168 additions and 13 deletions
+1
View File
@@ -134,6 +134,7 @@
"opencode-poe-auth": "0.0.1",
"opentui-spinner": "catalog:",
"partial-json": "0.1.7",
"playwright-core": "1.62.1",
"remeda": "catalog:",
"semver": "^7.6.3",
"solid-js": "catalog:",
+16 -13
View File
@@ -45,14 +45,28 @@ function hostOf(url: string): string {
}
}
export function installGuard() {
export function egressCheck(url: string) {
const mode = process.env.NEURON_GUARD ?? "log"
const strict = mode === "strict"
const allow = (process.env.NEURON_GUARD_ALLOW ?? "")
.split(",")
.map((h) => h.trim())
.filter(Boolean)
const host = hostOf(url)
const entry: Entry = { t: new Date().toISOString(), host, url }
if (strict && !allow.includes(host)) {
for (const denied of DENYLIST) {
if (host === denied || host.endsWith("." + denied) || url.includes(denied)) {
entry.blocked = true
log(entry)
throw new Error(`Neuron guard: egress to ${denied} is blocked (NEURON_GUARD=strict)`)
}
}
}
log(entry)
}
export function installGuard() {
const original = globalThis.fetch
const guarded = async (input: RequestInfo | URL, init?: RequestInit) => {
const url =
@@ -61,18 +75,7 @@ export function installGuard() {
: input instanceof URL
? input.href
: (input && "url" in input && typeof input.url === "string" ? input.url : String(input))
const host = hostOf(url)
const entry: Entry = { t: new Date().toISOString(), host, url }
if (strict && !allow.includes(host)) {
for (const denied of DENYLIST) {
if (host === denied || host.endsWith("." + denied) || url.includes(denied)) {
entry.blocked = true
log(entry)
throw new Error(`Neuron guard: egress to ${denied} is blocked (NEURON_GUARD=strict)`)
}
}
}
log(entry)
egressCheck(url)
return original(input, init)
}
// preserve fetch-adjacent properties attached by the runtime
+132
View File
@@ -0,0 +1,132 @@
import { Effect, Schema } from "effect"
import { chromium, type Browser, type Page } from "playwright-core"
import * as Tool from "./tool"
import { egressCheck } from "@/guard"
import DESCRIPTION from "./browser.txt"
const MAX_CONTENT = 20_000
const Parameters = Schema.Struct({
action: Schema.Literals(["navigate", "click", "fill", "content", "screenshot", "close"]).annotate({
description: "Browser operation to perform",
}),
url: Schema.optional(Schema.String).annotate({ description: "Absolute URL for navigate" }),
selector: Schema.optional(Schema.String).annotate({ description: "CSS selector for click/fill" }),
text: Schema.optional(Schema.String).annotate({ description: "Text to type for fill" }),
path: Schema.optional(Schema.String).annotate({ description: "Absolute PNG path for screenshot" }),
all: Schema.optional(Schema.Boolean).annotate({
description: "For close: true closes the whole browser, false (default) closes only the active tab",
default: false,
}),
})
let browserPromise: Promise<Browser> | undefined
let active: Page | undefined
const getBrowser = () =>
Effect.promise(async () => {
if (!browserPromise) {
browserPromise = chromium.launch({ channel: "chrome", headless: true })
}
return browserPromise
})
const getPage = () =>
Effect.gen(function* () {
const browser = yield* getBrowser()
if (!active || active.isClosed()) {
active = yield* Effect.promise(() => browser.newPage())
}
return active
})
const visibleText = (page: Page) =>
Effect.tryPromise({
try: async () => {
const text = await page.evaluate(() => document.body?.innerText ?? "")
const title = await page.title()
const trimmed = text.length > MAX_CONTENT ? `${text.slice(0, MAX_CONTENT)}\n...[truncated]` : text
return `# ${title}\n\n${trimmed}`
},
catch: (error) => new Error(error instanceof Error ? error.message : String(error)),
}).pipe(Effect.orDie)
export const BrowserTool = Tool.define(
"browser",
Effect.gen(function* () {
return {
description: DESCRIPTION,
parameters: Parameters,
execute: (params: Schema.Schema.Type<typeof Parameters>, _ctx: Tool.Context) =>
Effect.gen(function* () {
switch (params.action) {
case "navigate": {
if (!params.url) throw new Error("url is required for navigate")
if (!params.url.startsWith("http://") && !params.url.startsWith("https://"))
throw new Error("URL must start with http:// or https://")
egressCheck(params.url)
const page = yield* getPage()
yield* Effect.tryPromise({
try: () => page.goto(params.url!, { waitUntil: "domcontentloaded", timeout: 45_000 }),
catch: (error) => new Error(error instanceof Error ? error.message : String(error)),
}).pipe(Effect.orDie)
const body = yield* visibleText(page)
return { title: `navigate ${params.url}`, metadata: {}, output: body }
}
case "click": {
if (!params.selector) throw new Error("selector is required for click")
const page = yield* getPage()
yield* Effect.tryPromise({
try: () => page.click(params.selector!, { timeout: 10_000 }),
catch: (error) => new Error(error instanceof Error ? error.message : String(error)),
}).pipe(Effect.orDie)
yield* Effect.promise(() => page.waitForLoadState("domcontentloaded").catch(() => undefined))
return { title: `click ${params.selector}`, metadata: {}, output: "clicked" }
}
case "fill": {
if (!params.selector || params.text === undefined) throw new Error("selector and text are required for fill")
const page = yield* getPage()
yield* Effect.tryPromise({
try: () => page.fill(params.selector!, params.text!, { timeout: 10_000 }),
catch: (error) => new Error(error instanceof Error ? error.message : String(error)),
}).pipe(Effect.orDie)
return { title: `fill ${params.selector}`, metadata: {}, output: "filled" }
}
case "content": {
const page = yield* getPage()
const body = yield* visibleText(page)
return { title: `content ${page.url()}`, metadata: {}, output: body }
}
case "screenshot": {
if (!params.path) throw new Error("path is required for screenshot")
const page = yield* getPage()
yield* Effect.tryPromise({
try: () => page.screenshot({ path: params.path!, fullPage: false }),
catch: (error) => new Error(error instanceof Error ? error.message : String(error)),
}).pipe(Effect.orDie)
return { title: `screenshot ${params.path}`, metadata: {}, output: `saved to ${params.path}` }
}
case "close": {
if (!active) return { title: "close", metadata: {}, output: "no active page" }
const page = active
if (params.all) {
const browser = yield* getBrowser()
yield* Effect.promise(() => browser.close())
browserPromise = undefined
active = undefined
return { title: "close", metadata: {}, output: "browser closed" }
}
yield* Effect.promise(() => page.close())
active = undefined
return { title: "close", metadata: {}, output: "tab closed" }
}
}
}),
}
}),
)
+19
View File
@@ -0,0 +1,19 @@
Drive a real headless Chrome browser.
One browser instance is shared across calls in this session; pages persist
between calls, so you can navigate once and then click, fill, and read on
subsequent calls. Every navigation passes through the Neuron egress guard —
in NEURON_GUARD=strict mode, denylisted hosts are refused.
Actions:
- `navigate` (url): open a URL in a new tab and make it active. Returns the
page title and readable text.
- `click` (selector): click an element by CSS selector.
- `fill` (selector, text): clear and type text into an input/textarea.
- `content`: return the active page's visible text (truncated).
- `screenshot` (path): save a PNG of the active page to an absolute path.
- `close`: close the active tab (or the whole browser with `all: true`).
Selectors are plain CSS (`#id`, `.class`, `a[href*="docs"]`). Prefer reading
`content` before clicking blind — know the page before you act on it.