lazycodex-ai 5.1.13 → 5.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +18 -17
- package/dist/cli-node/index.js +18 -17
- package/package.json +1 -1
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
- package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
- package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +4 -4
- package/packages/omo-codex/plugin/components/lsp/dist/cli.js +33 -19
- package/packages/omo-codex/plugin/components/lsp/dist/codex-hook.js +27 -17
- package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
- package/packages/omo-codex/plugin/components/lsp/src/codex-hook.ts +36 -18
- package/packages/omo-codex/plugin/components/lsp/test/codex-hook-not-installed.test.ts +203 -0
- package/packages/omo-codex/plugin/components/rules/CHANGELOG.md +1 -0
- package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-6.md +1 -1
- package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/rules/package.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
- package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/package-lock.json +12 -12
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/plugin/skills/browser/SKILL.md +26 -2
- package/packages/omo-codex/plugin/skills/browser/references/commands.md +10 -1
- package/packages/omo-codex/plugin/skills/browser/references/install.md +7 -0
- package/packages/omo-codex/plugin/skills/browser/runtime/omowright/manifest.json +1 -1
- package/packages/omo-codex/plugin/skills/browser/scripts/browser-engine-guard.mjs +506 -0
- package/packages/omo-codex/plugin/skills/browser/scripts/omowright.mjs +29 -2
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +2 -2
- package/packages/shared-skills/skills/browser/SKILL.md +26 -2
- package/packages/shared-skills/skills/browser/references/commands.md +10 -1
- package/packages/shared-skills/skills/browser/references/install.md +7 -0
- package/packages/shared-skills/skills/browser/runtime/omowright/manifest.json +1 -1
- package/packages/shared-skills/skills/browser/scripts/browser-engine-guard.mjs +506 -0
- package/packages/shared-skills/skills/browser/scripts/omowright.mjs +29 -2
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
export const BROWSER_CONFIRMATION_POLICY = "ask-before-irreversible"
|
|
2
|
+
export const BROWSER_ENGINE_ENV = "OMO_BROWSER_ENGINE"
|
|
3
|
+
export const BROWSER_STATE_EVENT = "omo.browser.state"
|
|
4
|
+
|
|
5
|
+
const BRIDGE_TOOL = "omo_browser_bridge"
|
|
6
|
+
const ENGINES = new Set(["connected", "builtin", "none"])
|
|
7
|
+
const NOT_CONNECTED_CODES = new Set(["no_browser_connected", "no_daemon", "unsupported"])
|
|
8
|
+
const CLASSIFY_HTML_BYTES = 4096
|
|
9
|
+
const QUESTION_HEADER = "Browser"
|
|
10
|
+
const ALLOW = "Allow"
|
|
11
|
+
const DECLINE = "Don't allow"
|
|
12
|
+
const READ_ONLY_RAW_TOOLS = new Set(["observe", "snapshot", "get_html", "screenshot", "tab_list", "console", "network"])
|
|
13
|
+
|
|
14
|
+
const OWNED_SESSION_CREATORS = new Set(["connect", "connectPipe", "connectCloakProfile"])
|
|
15
|
+
const PASSTHROUGH_FUNCTIONS = new Set([
|
|
16
|
+
"bskDoctor",
|
|
17
|
+
"bskOnboard",
|
|
18
|
+
"installBskCli",
|
|
19
|
+
"readDaemonInfo",
|
|
20
|
+
"resolveBskHome",
|
|
21
|
+
"listBrowsers",
|
|
22
|
+
"catalogBrowsers",
|
|
23
|
+
"detectBrowsers",
|
|
24
|
+
"identifyBrowser",
|
|
25
|
+
"probeBrowserSignals",
|
|
26
|
+
"findCloakBrowserPath",
|
|
27
|
+
"resolveCloakProfile",
|
|
28
|
+
"registerExternalExtension",
|
|
29
|
+
"unregisterExternalExtension",
|
|
30
|
+
"externalExtensionEntry",
|
|
31
|
+
"buildSnapshotExpression",
|
|
32
|
+
"buildCloakBrowserArgs",
|
|
33
|
+
"compactSnapshot",
|
|
34
|
+
"snapshotTokens",
|
|
35
|
+
"describeLayers",
|
|
36
|
+
"layersHeader",
|
|
37
|
+
"decodeFrames",
|
|
38
|
+
"encodeFrame",
|
|
39
|
+
"reconcileFrames",
|
|
40
|
+
"normalizeDialogPolicy",
|
|
41
|
+
"resolveDialogAction",
|
|
42
|
+
"sanitizeCookies",
|
|
43
|
+
"toHar",
|
|
44
|
+
"BskRpcError",
|
|
45
|
+
"BridgeProtocolError",
|
|
46
|
+
"BridgeUnavailableError",
|
|
47
|
+
"BrowserCdpCommandError",
|
|
48
|
+
"UnsupportedOperationError",
|
|
49
|
+
])
|
|
50
|
+
|
|
51
|
+
class CodedError extends Error {
|
|
52
|
+
constructor(name, code, message, options) {
|
|
53
|
+
super(message, options)
|
|
54
|
+
this.name = name
|
|
55
|
+
this.code = code
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export class BrowserEngineRefusal extends CodedError {
|
|
60
|
+
constructor(code, message) {
|
|
61
|
+
super("BrowserEngineRefusal", code, message)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class BrowserNotConnectedError extends CodedError {
|
|
66
|
+
constructor(message, options) {
|
|
67
|
+
super("BrowserNotConnectedError", "browser_not_connected", message, options)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export class BrowserUserStoppedError extends CodedError {
|
|
72
|
+
constructor(message = "The user stopped browser use. Report that and do not start another browser session this turn.") {
|
|
73
|
+
super("BrowserUserStoppedError", "user_stopped", message)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export class BrowserActionDeclinedError extends CodedError {
|
|
78
|
+
constructor(message, code = "user_declined") {
|
|
79
|
+
super("BrowserActionDeclinedError", code, message)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const IRREVERSIBLE_LABELS = [
|
|
84
|
+
/\b(send|post|publish|tweet|reply)\b/i,
|
|
85
|
+
/\b(pay|purchase|buy|subscribe|donate|checkout|submit|confirm|transfer|approve|merge)\b/i,
|
|
86
|
+
/\b(e-?sign|sign\s+(the\s+|this\s+|your\s+)?(document|contract|agreement|transaction|petition|form))\b/i,
|
|
87
|
+
/\border\b(?!\s+(history|status|details|number|tracking))/i,
|
|
88
|
+
/\b(delete|remove|unsubscribe|deactivate)\b/i,
|
|
89
|
+
/\bcancel\s+(my\s+|your\s+)?(subscription|membership|plan|account)\b/i,
|
|
90
|
+
/\bclose\s+(my\s+|your\s+)?account\b/i,
|
|
91
|
+
/결제|구매|주문|구독|삭제|제거|탈퇴|전송|발송|게시|발행|제출|승인|송금|이체|병합/,
|
|
92
|
+
]
|
|
93
|
+
const IRREVERSIBLE_ACTION_PATH = /\/(send|post|publish|pay|purchase|order|subscribe|delete|remove)\b/i
|
|
94
|
+
const SCRIPT_DRIVES_UI = /\.(click|submit|requestSubmit)\s*\(|\bdispatchEvent\b/
|
|
95
|
+
const SCRIPT_BEACON = /\bsendBeacon\s*\(/
|
|
96
|
+
const SCRIPT_REACHES_NETWORK = /\b(fetch|XMLHttpRequest)\b/
|
|
97
|
+
const SCRIPT_WRITES = /["'`](POST|PUT|PATCH|DELETE)["'`]/i
|
|
98
|
+
const ENTER_KEY = /(^|\+)(Enter|Return)$/i
|
|
99
|
+
const SENDING_CHORD = /(^|\+)(Control|Ctrl|Meta|Cmd|Command)\+/i
|
|
100
|
+
|
|
101
|
+
export function isIrreversibleLabel(label) {
|
|
102
|
+
return typeof label === "string" && IRREVERSIBLE_LABELS.some((pattern) => pattern.test(label))
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function labelsOf(descriptor) {
|
|
106
|
+
return [descriptor?.text, descriptor?.ariaLabel, descriptor?.title, descriptor?.value].filter((label) => typeof label === "string" && label.length > 0)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function markupIsEditable(html) {
|
|
110
|
+
return /\bcontenteditable\b(?!\s*=\s*["']?false)|\brole\s*=\s*["']textbox["']|<textarea\b/i.test(html)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function labelsFromMarkup(html) {
|
|
114
|
+
const labels = []
|
|
115
|
+
for (const match of html.matchAll(/\b(?:aria-label|title|value|alt)\s*=\s*"([^"]*)"/gi)) labels.push(match[1])
|
|
116
|
+
labels.push(html.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim())
|
|
117
|
+
return labels.filter((label) => label.length > 0)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function scriptNeedsConfirmation(expression) {
|
|
121
|
+
if (typeof expression !== "string") return false
|
|
122
|
+
if (SCRIPT_DRIVES_UI.test(expression) || SCRIPT_BEACON.test(expression)) return true
|
|
123
|
+
return SCRIPT_REACHES_NETWORK.test(expression) && SCRIPT_WRITES.test(expression)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function describeElementExpression(selector) {
|
|
127
|
+
return `(function (selector) {
|
|
128
|
+
const el = selector ? document.querySelector(selector) : document.activeElement
|
|
129
|
+
if (!el || el === document.body || el === document.documentElement) return null
|
|
130
|
+
const text = (el.innerText || el.textContent || "").trim().slice(0, 200)
|
|
131
|
+
const form = el.form || (el.closest ? el.closest("form") : null)
|
|
132
|
+
return {
|
|
133
|
+
tag: el.localName,
|
|
134
|
+
editable: el.isContentEditable === true || el.localName === "textarea" || el.getAttribute("role") === "textbox",
|
|
135
|
+
text,
|
|
136
|
+
ariaLabel: el.getAttribute("aria-label") || undefined,
|
|
137
|
+
title: el.getAttribute("title") || undefined,
|
|
138
|
+
value: el.localName === "button" || (el.localName === "input" && /^(submit|button|image)$/i.test(el.type)) ? el.value : undefined,
|
|
139
|
+
form: form ? {
|
|
140
|
+
action: form.getAttribute("action") || "",
|
|
141
|
+
method: (form.getAttribute("method") || "get").toLowerCase(),
|
|
142
|
+
submitLabels: Array.from(form.querySelectorAll("button, input[type=submit], input[type=image]")).filter((b) => (b.type || "submit") === "submit").map((b) => (b.innerText || b.value || b.getAttribute("aria-label") || "").trim().slice(0, 100)),
|
|
143
|
+
} : undefined,
|
|
144
|
+
}
|
|
145
|
+
})(${JSON.stringify(selector ?? null)})`
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function codeOf(error) {
|
|
149
|
+
return typeof error?.code === "string" ? error.code : error instanceof Error ? error.name : "error"
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function targetKind(target) {
|
|
153
|
+
if (typeof target === "string") return /^@?e\d+$/.test(target) ? { kind: "ref", ref: target } : { kind: "selector", selector: target }
|
|
154
|
+
if (target && typeof target === "object") {
|
|
155
|
+
if (typeof target.ref === "string") return { kind: "ref", ref: target.ref }
|
|
156
|
+
if (typeof target.selector === "string") return { kind: "selector", selector: target.selector }
|
|
157
|
+
}
|
|
158
|
+
return { kind: "point" }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function createBridge(host, context) {
|
|
162
|
+
const call = async (args) => {
|
|
163
|
+
if (host === undefined) return undefined
|
|
164
|
+
try {
|
|
165
|
+
const result = await host.callTool(BRIDGE_TOOL, args)
|
|
166
|
+
return result?.hasError === true ? undefined : result
|
|
167
|
+
} catch {
|
|
168
|
+
return undefined
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
async emit(status, extra = {}) {
|
|
173
|
+
const data = { engine: context.engine, status }
|
|
174
|
+
if (context.sessionId !== undefined) data.session = context.sessionId
|
|
175
|
+
for (const [key, value] of Object.entries(extra)) if (value !== undefined) data[key] = value
|
|
176
|
+
await call({ op: "state", data })
|
|
177
|
+
},
|
|
178
|
+
async stopped() {
|
|
179
|
+
return (await call({ op: "status" }))?.details?.stopped === true
|
|
180
|
+
},
|
|
181
|
+
async markStopped() {
|
|
182
|
+
await call({ op: "stopped" })
|
|
183
|
+
},
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function describeTab(tab) {
|
|
188
|
+
if (!tab || typeof tab !== "object") return undefined
|
|
189
|
+
const favicon = tab.favicon_url ?? tab.faviconUrl ?? tab.favIconUrl ?? tab.favicon
|
|
190
|
+
const described = { id: tab.id, url: tab.url, title: tab.title }
|
|
191
|
+
if (typeof favicon === "string") described.favicon = favicon
|
|
192
|
+
return described
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function currentTab(rawSession) {
|
|
196
|
+
try {
|
|
197
|
+
const { tabs } = await rawSession.tabList({ scope: "agent" })
|
|
198
|
+
return describeTab(tabs?.find((tab) => tab.active) ?? tabs?.[0])
|
|
199
|
+
} catch {
|
|
200
|
+
return undefined
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function questionFor(variant, description) {
|
|
205
|
+
const options = [
|
|
206
|
+
{ label: ALLOW, description: "Do it this once." },
|
|
207
|
+
{ label: DECLINE, description: "Stop this action and tell the agent." },
|
|
208
|
+
]
|
|
209
|
+
const question = `Allow the agent to ${description}?`
|
|
210
|
+
if (variant === "ask_user_question") {
|
|
211
|
+
return { waitForAnswer: true, questions: [{ header: QUESTION_HEADER, question, multiSelect: false, options }] }
|
|
212
|
+
}
|
|
213
|
+
return { wait_for_answer: true, questions: [{ id: "confirm_browser_action", header: QUESTION_HEADER, question, options }] }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function chosenAnswer(variant, details) {
|
|
217
|
+
if (details?.status !== "answered") return undefined
|
|
218
|
+
const entries = Object.values(details.answers ?? {})
|
|
219
|
+
if (entries.length !== 1) return undefined
|
|
220
|
+
return variant === "ask_user_question" ? entries[0] : entries[0]?.answers?.[0]
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function confirmWithUser({ host, bridge, rawSession, description, action }) {
|
|
224
|
+
const decline = (unavailableReason) =>
|
|
225
|
+
unavailableReason === undefined
|
|
226
|
+
? new BrowserActionDeclinedError(`The user declined: ${description}. Nothing was done; report that and do not retry it.`)
|
|
227
|
+
: new BrowserActionDeclinedError(`Nothing was done: ${description}. ${unavailableReason}`, "confirmation_unavailable")
|
|
228
|
+
let variant
|
|
229
|
+
try {
|
|
230
|
+
const names = host === undefined ? [] : await host.listTools()
|
|
231
|
+
variant = ["ask_user_question", "request_user_input"].find((name) => names.includes(name))
|
|
232
|
+
} catch {
|
|
233
|
+
variant = undefined
|
|
234
|
+
}
|
|
235
|
+
if (host === undefined || variant === undefined) {
|
|
236
|
+
throw decline("The user could not be asked because this session has no question tool.")
|
|
237
|
+
}
|
|
238
|
+
const tab = await currentTab(rawSession)
|
|
239
|
+
await bridge.emit("awaiting_confirmation", { action, tab })
|
|
240
|
+
let answer
|
|
241
|
+
try {
|
|
242
|
+
const result = await host.callTool(variant, questionFor(variant, description))
|
|
243
|
+
answer = result?.hasError === true ? undefined : chosenAnswer(variant, result?.details)
|
|
244
|
+
} catch {
|
|
245
|
+
answer = undefined
|
|
246
|
+
}
|
|
247
|
+
if (answer !== ALLOW) {
|
|
248
|
+
await bridge.emit("failed", { action, reason: "user_declined", tab })
|
|
249
|
+
throw decline()
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function hostOf(url) {
|
|
254
|
+
try {
|
|
255
|
+
return new URL(url).host
|
|
256
|
+
} catch {
|
|
257
|
+
return undefined
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function describeForConfirmation(rawSession, label, rawSessionTab) {
|
|
262
|
+
const site = hostOf(rawSessionTab?.url)
|
|
263
|
+
return site === undefined ? label : `${label} on ${site}`
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function wrapSession(rawSession, context, bridge, host) {
|
|
267
|
+
const nowStopped = async () => {
|
|
268
|
+
if (!(await bridge.stopped())) return
|
|
269
|
+
try {
|
|
270
|
+
await rawSession.stop()
|
|
271
|
+
} catch {
|
|
272
|
+
// The daemon may already have ended the session.
|
|
273
|
+
}
|
|
274
|
+
throw new BrowserUserStoppedError()
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const confirm = async (action, label) => {
|
|
278
|
+
const tab = await currentTab(rawSession)
|
|
279
|
+
await confirmWithUser({ host, bridge, rawSession, description: await describeForConfirmation(rawSession, label, tab), action })
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const readTarget = async (target) => {
|
|
283
|
+
const kind = targetKind(target)
|
|
284
|
+
if (kind.kind === "point") return { unknown: true, labels: [] }
|
|
285
|
+
try {
|
|
286
|
+
if (kind.kind === "ref") {
|
|
287
|
+
const reply = await rawSession.getHtml({ ref: kind.ref, maxBytes: CLASSIFY_HTML_BYTES })
|
|
288
|
+
const html = String(reply?.html ?? "")
|
|
289
|
+
return { unknown: false, labels: labelsFromMarkup(html), editable: markupIsEditable(html) }
|
|
290
|
+
}
|
|
291
|
+
const reply = await rawSession.evaluate(describeElementExpression(kind.selector), { returnByValue: true, awaitPromise: false })
|
|
292
|
+
if (!reply || reply.ok === false) return { unknown: true, labels: [] }
|
|
293
|
+
if (reply.value === null || reply.value === undefined) return { unknown: false, labels: [], missing: true }
|
|
294
|
+
return { unknown: false, labels: labelsOf(reply.value), descriptor: reply.value }
|
|
295
|
+
} catch {
|
|
296
|
+
return { unknown: true, labels: [] }
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const preflightClick = async (target) => {
|
|
301
|
+
const read = await readTarget(target)
|
|
302
|
+
if (read.unknown) return confirm("click", "click a control whose label could not be read")
|
|
303
|
+
const label = read.labels.find(isIrreversibleLabel)
|
|
304
|
+
if (label !== undefined) return confirm("click", `click "${label}"`)
|
|
305
|
+
return undefined
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const preflightPress = async (key, options) => {
|
|
309
|
+
if (typeof key !== "string" || !ENTER_KEY.test(key)) return undefined
|
|
310
|
+
if (SENDING_CHORD.test(key)) return confirm("press", `press ${key}, which sends in many apps`)
|
|
311
|
+
if (/(^|\+)Shift\+/i.test(key)) return undefined
|
|
312
|
+
const sendsFromMessageBox = () => confirm("press", "press Enter in a message box, which sends in many apps")
|
|
313
|
+
const target = options?.target
|
|
314
|
+
if (target !== undefined && targetKind(target).kind !== "selector") {
|
|
315
|
+
const read = await readTarget(target)
|
|
316
|
+
if (read.unknown) return confirm("press", "press Enter on a control that could not be read")
|
|
317
|
+
if (read.editable) return sendsFromMessageBox()
|
|
318
|
+
const label = read.labels.find(isIrreversibleLabel)
|
|
319
|
+
return label === undefined ? undefined : confirm("press", `press Enter on "${label}"`)
|
|
320
|
+
}
|
|
321
|
+
let described
|
|
322
|
+
try {
|
|
323
|
+
const reply = await rawSession.evaluate(describeElementExpression(target === undefined ? undefined : targetKind(target).selector), {
|
|
324
|
+
returnByValue: true,
|
|
325
|
+
awaitPromise: false,
|
|
326
|
+
})
|
|
327
|
+
if (!reply || reply.ok === false) return confirm("press", "press Enter in a field that could not be read")
|
|
328
|
+
described = reply.value
|
|
329
|
+
} catch {
|
|
330
|
+
return confirm("press", "press Enter in a field that could not be read")
|
|
331
|
+
}
|
|
332
|
+
if (described === null || described === undefined) return undefined
|
|
333
|
+
const form = described.form
|
|
334
|
+
if (described.editable === true && (form === undefined || form.method !== "get")) return sendsFromMessageBox()
|
|
335
|
+
const own = labelsOf(described).find(isIrreversibleLabel)
|
|
336
|
+
if (own !== undefined && described.tag !== "input") return confirm("press", `press Enter on "${own}"`)
|
|
337
|
+
if (form === undefined) return undefined
|
|
338
|
+
const submit = (form.submitLabels ?? []).find(isIrreversibleLabel)
|
|
339
|
+
if (submit !== undefined) return confirm("press", `submit a form with "${submit}"`)
|
|
340
|
+
if (form.method === "post" && IRREVERSIBLE_ACTION_PATH.test(String(form.action))) return confirm("press", "submit a form that sends")
|
|
341
|
+
return undefined
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const act = async (kind, run, { navigates = false, preflight } = {}) => {
|
|
345
|
+
await nowStopped()
|
|
346
|
+
if (preflight !== undefined) await preflight()
|
|
347
|
+
await bridge.emit("acting", { action: kind })
|
|
348
|
+
let result
|
|
349
|
+
try {
|
|
350
|
+
result = await run()
|
|
351
|
+
} catch (error) {
|
|
352
|
+
if (codeOf(error) === "user_aborted") {
|
|
353
|
+
await bridge.markStopped()
|
|
354
|
+
await bridge.emit("user_stopped", { action: kind })
|
|
355
|
+
try {
|
|
356
|
+
await rawSession.stop()
|
|
357
|
+
} catch {
|
|
358
|
+
// The daemon already ended it.
|
|
359
|
+
}
|
|
360
|
+
throw new BrowserUserStoppedError()
|
|
361
|
+
}
|
|
362
|
+
await bridge.emit("failed", { action: kind, reason: codeOf(error) })
|
|
363
|
+
throw error
|
|
364
|
+
}
|
|
365
|
+
if (navigates) await bridge.emit("navigated", { action: kind, tab: await currentTab(rawSession) })
|
|
366
|
+
await bridge.emit("idle", { action: kind })
|
|
367
|
+
return result
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const plain = (method, navigates) => (...args) => act(method, () => rawSession[method](...args), { navigates })
|
|
371
|
+
const handlers = {
|
|
372
|
+
navigate: plain("navigate", true),
|
|
373
|
+
back: plain("back", true),
|
|
374
|
+
forward: plain("forward", true),
|
|
375
|
+
reload: plain("reload", true),
|
|
376
|
+
waitForNavigation: plain("waitForNavigation", true),
|
|
377
|
+
tabCreate: plain("tabCreate", true),
|
|
378
|
+
tabSelect: plain("tabSelect", true),
|
|
379
|
+
tabClose: plain("tabClose", true),
|
|
380
|
+
tabBorrow: plain("tabBorrow", true),
|
|
381
|
+
tabReturn: plain("tabReturn", true),
|
|
382
|
+
fill: plain("fill", false),
|
|
383
|
+
select: plain("select", false),
|
|
384
|
+
hover: plain("hover", false),
|
|
385
|
+
focus: plain("focus", false),
|
|
386
|
+
blur: plain("blur", false),
|
|
387
|
+
scrollTo: plain("scrollTo", false),
|
|
388
|
+
wheel: plain("wheel", false),
|
|
389
|
+
click: (target, options) => act("click", () => rawSession.click(target, options), { preflight: () => preflightClick(target) }),
|
|
390
|
+
press: (key, options) => act("press", () => rawSession.press(key, options), { preflight: () => preflightPress(key, options) }),
|
|
391
|
+
evaluate: (expression, options) =>
|
|
392
|
+
act("evaluate", () => rawSession.evaluate(expression, options), {
|
|
393
|
+
preflight: () => (scriptNeedsConfirmation(expression) ? confirm("evaluate", "run a script that can click, submit or send in your browser") : undefined),
|
|
394
|
+
}),
|
|
395
|
+
tool: async (name, params) => {
|
|
396
|
+
if (!READ_ONLY_RAW_TOOLS.has(name)) {
|
|
397
|
+
throw new BrowserEngineRefusal("browser_tool_blocked", `session.tool("${name}") skips the confirmation policy; only reads go through it. Call the matching session method instead.`)
|
|
398
|
+
}
|
|
399
|
+
return await rawSession.tool(name, params)
|
|
400
|
+
},
|
|
401
|
+
async stop() {
|
|
402
|
+
if (rawSession.stopped) return null
|
|
403
|
+
try {
|
|
404
|
+
return await rawSession.stop()
|
|
405
|
+
} finally {
|
|
406
|
+
await bridge.emit("stopped")
|
|
407
|
+
}
|
|
408
|
+
},
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return new Proxy(rawSession, {
|
|
412
|
+
get(target, property) {
|
|
413
|
+
if (Object.hasOwn(handlers, property)) return handlers[property]
|
|
414
|
+
const value = Reflect.get(target, property, target)
|
|
415
|
+
return typeof value === "function" ? value.bind(target) : value
|
|
416
|
+
},
|
|
417
|
+
})
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const GUARDED_SESSIONS = new WeakMap()
|
|
421
|
+
|
|
422
|
+
function connectErrorFor(error) {
|
|
423
|
+
if (NOT_CONNECTED_CODES.has(codeOf(error))) {
|
|
424
|
+
return new BrowserNotConnectedError(
|
|
425
|
+
"Connect your browser: this session is set to drive the user's own browser, and none is connected. Ask the user to open the browser that has the BrowserSkill extension enabled, then try again. Do not use or launch another browser.",
|
|
426
|
+
{ cause: error },
|
|
427
|
+
)
|
|
428
|
+
}
|
|
429
|
+
return error
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export function guardOmowright(raw, { env = process.env, host } = {}) {
|
|
433
|
+
const engine = env[BROWSER_ENGINE_ENV]
|
|
434
|
+
if (engine === undefined || engine === "") return raw
|
|
435
|
+
|
|
436
|
+
const connectBrowserSkill = async (options) => {
|
|
437
|
+
if (!ENGINES.has(engine)) {
|
|
438
|
+
throw new BrowserEngineRefusal("browser_engine_unsupported", `${BROWSER_ENGINE_ENV} is "${engine}", which is not connected, builtin or none; browser use is refused.`)
|
|
439
|
+
}
|
|
440
|
+
if (engine === "none") {
|
|
441
|
+
throw new BrowserEngineRefusal("browser_engine_none", "Agent browser access is off for this project. Tell the user; do not try another browser.")
|
|
442
|
+
}
|
|
443
|
+
if (engine === "builtin") {
|
|
444
|
+
throw new BrowserEngineRefusal("browser_engine_builtin", "This session uses the app's built-in browser. Do not call connectBrowserSkill(); use the app's in-app browser tools.")
|
|
445
|
+
}
|
|
446
|
+
const context = { engine, sessionId: undefined }
|
|
447
|
+
const bridge = createBridge(host, context)
|
|
448
|
+
if (await bridge.stopped()) throw new BrowserUserStoppedError()
|
|
449
|
+
let rawSession
|
|
450
|
+
try {
|
|
451
|
+
rawSession = await raw.connectBrowserSkill(options)
|
|
452
|
+
} catch (error) {
|
|
453
|
+
await bridge.emit("connect_failed", { reason: codeOf(error) })
|
|
454
|
+
throw connectErrorFor(error)
|
|
455
|
+
}
|
|
456
|
+
context.sessionId = rawSession.sessionId
|
|
457
|
+
const session = wrapSession(rawSession, context, bridge, host)
|
|
458
|
+
GUARDED_SESSIONS.set(session, rawSession)
|
|
459
|
+
await bridge.emit("started")
|
|
460
|
+
return session
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const bskSnapshot = (session, options) => raw.bskSnapshot(GUARDED_SESSIONS.get(session) ?? session, options)
|
|
464
|
+
|
|
465
|
+
return allowlistedLibrary(raw, engine, { connectBrowserSkill, bskSnapshot })
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function ownedBrowserRefusal(engine) {
|
|
469
|
+
const why =
|
|
470
|
+
engine === "none"
|
|
471
|
+
? "Agent browser access is off for this project."
|
|
472
|
+
: engine === "builtin"
|
|
473
|
+
? "This session uses the app's built-in browser."
|
|
474
|
+
: engine === "connected"
|
|
475
|
+
? "This session drives the user's own browser only."
|
|
476
|
+
: `${BROWSER_ENGINE_ENV} is "${engine}", which is not connected, builtin or none.`
|
|
477
|
+
const refusal = new BrowserEngineRefusal("browser_engine_owned_blocked", `${why} Do not launch an owned browser; tell the user instead.`)
|
|
478
|
+
refusal.engine = engine
|
|
479
|
+
return refusal
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function blockedExport(name, engine) {
|
|
483
|
+
const blocked = function () {
|
|
484
|
+
const refusal = new BrowserEngineRefusal(
|
|
485
|
+
OWNED_SESSION_CREATORS.has(name) ? "browser_engine_owned_blocked" : "browser_engine_export_blocked",
|
|
486
|
+
OWNED_SESSION_CREATORS.has(name)
|
|
487
|
+
? ownedBrowserRefusal(engine).message
|
|
488
|
+
: `omowright.${name} is not available while ${BROWSER_ENGINE_ENV} is set; it can act on a browser without the engine and confirmation policy.`,
|
|
489
|
+
)
|
|
490
|
+
refusal.engine = engine
|
|
491
|
+
if (new.target !== undefined) throw refusal
|
|
492
|
+
return Promise.reject(refusal)
|
|
493
|
+
}
|
|
494
|
+
Object.defineProperty(blocked, "name", { value: name })
|
|
495
|
+
return blocked
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function allowlistedLibrary(raw, engine, guarded) {
|
|
499
|
+
const library = {}
|
|
500
|
+
for (const [name, value] of Object.entries(raw)) {
|
|
501
|
+
if (typeof value !== "function") library[name] = value
|
|
502
|
+
else if (PASSTHROUGH_FUNCTIONS.has(name)) library[name] = value
|
|
503
|
+
else library[name] = blockedExport(name, engine)
|
|
504
|
+
}
|
|
505
|
+
return Object.assign(library, guarded)
|
|
506
|
+
}
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import { existsSync } from "node:fs"
|
|
2
2
|
import { dirname, join } from "node:path"
|
|
3
3
|
import { fileURLToPath, pathToFileURL } from "node:url"
|
|
4
|
+
import { guardOmowright } from "./browser-engine-guard.mjs"
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
BROWSER_CONFIRMATION_POLICY,
|
|
8
|
+
BROWSER_ENGINE_ENV,
|
|
9
|
+
BROWSER_STATE_EVENT,
|
|
10
|
+
BrowserActionDeclinedError,
|
|
11
|
+
BrowserEngineRefusal,
|
|
12
|
+
BrowserNotConnectedError,
|
|
13
|
+
BrowserUserStoppedError,
|
|
14
|
+
guardOmowright,
|
|
15
|
+
} from "./browser-engine-guard.mjs"
|
|
4
16
|
|
|
5
17
|
const scriptDir = dirname(fileURLToPath(import.meta.url))
|
|
6
18
|
const skillRoot = dirname(scriptDir)
|
|
@@ -14,12 +26,27 @@ export function resolveOmowrightEntry(env = process.env) {
|
|
|
14
26
|
return candidates.find((candidate) => existsSync(candidate))
|
|
15
27
|
}
|
|
16
28
|
|
|
17
|
-
|
|
29
|
+
function kernelHost() {
|
|
30
|
+
const { tool, tool_schema: toolSchema } = globalThis
|
|
31
|
+
if (typeof toolSchema !== "function" || tool === undefined || tool === null) return undefined
|
|
32
|
+
return {
|
|
33
|
+
async listTools() {
|
|
34
|
+
const listed = await toolSchema()
|
|
35
|
+
return Array.isArray(listed?.tools) ? listed.tools : []
|
|
36
|
+
},
|
|
37
|
+
async callTool(name, args) {
|
|
38
|
+
return await tool[name](args)
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function loadOmowright(env = process.env, host = kernelHost()) {
|
|
18
44
|
const entry = resolveOmowrightEntry(env)
|
|
19
45
|
if (entry === undefined) {
|
|
20
46
|
throw new Error(
|
|
21
47
|
"omowright is not staged in this skill; run `node packages/shared-skills/stage-omowright-runtime.mjs` in a checkout, or set OMOWRIGHT_ROOT to a directory holding its bundled index.js",
|
|
22
48
|
)
|
|
23
49
|
}
|
|
24
|
-
|
|
50
|
+
const raw = await import(pathToFileURL(entry).href)
|
|
51
|
+
return { entry, omowright: guardOmowright(raw, { env, host }) }
|
|
25
52
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// omo-codex-install:
|
|
2
|
+
// omo-codex-install:62eae83a8d26230269a3fe89b2e018a0c44778dd7fc5d6666e662f44b6f435ac:a302291f316633797c5e133aa3c3e55608bcb3cf6f72bb4e49fc19b26ebdf5ec
|
|
3
3
|
var __esm = (fn, res, err) => () => {
|
|
4
4
|
if (fn)
|
|
5
5
|
try {
|
|
@@ -9984,7 +9984,7 @@ var package_default;
|
|
|
9984
9984
|
var init_package = __esm(() => {
|
|
9985
9985
|
package_default = {
|
|
9986
9986
|
name: "@oh-my-opencode/omo-codex",
|
|
9987
|
-
version: "5.1.
|
|
9987
|
+
version: "5.1.15",
|
|
9988
9988
|
type: "module",
|
|
9989
9989
|
private: true,
|
|
9990
9990
|
description: "Codex harness adapter for oh-my-openagent. Vendored Codex plugin namespace (omo) + TypeScript installer + telemetry.",
|
|
@@ -18,7 +18,31 @@ one where a human is a single call away. Never substitute one engine for the oth
|
|
|
18
18
|
the attached engine is not set up, run the onboarding script and tell the user its one remaining
|
|
19
19
|
step.
|
|
20
20
|
|
|
21
|
-
## Step 0 —
|
|
21
|
+
## Step 0 — which engine this session is allowed to use
|
|
22
|
+
|
|
23
|
+
When `OMO_BROWSER_ENGINE` is set (the OmO desktop app sets it for every session), it wins over the table above:
|
|
24
|
+
|
|
25
|
+
| Value | What you do |
|
|
26
|
+
|---|---|
|
|
27
|
+
| `connected` | Use `connectBrowserSkill()` only. If the user's browser is not connected you get a "Connect your browser" error: relay it and stop. Never open another browser |
|
|
28
|
+
| `builtin` | Do not call `connectBrowserSkill()`; use the app's in-app browser tools |
|
|
29
|
+
| `none` | Do not do browser work. Say that agent browser access is off for this project |
|
|
30
|
+
| unset | The table above, as before (terminal use) |
|
|
31
|
+
|
|
32
|
+
While any engine is set, `loadOmowright()` returns a guarded library. The owned engine (`connectPipe`,
|
|
33
|
+
`connectCloakProfile`, `connect`) and every other export that acts on a browser is refused, so the table above
|
|
34
|
+
does not apply: do not look for a way around it, and tell the user what the session allows. Under `connected`
|
|
35
|
+
the app sees what the browser is doing, and before a click, Enter or script that sends, posts, pays, orders,
|
|
36
|
+
subscribes, deletes or closes an account, and before Enter in a message box, it asks the user first. A "No" fails the action with
|
|
37
|
+
`BrowserActionDeclinedError`: report that, never retry it or go around it (`session.tool()` lets only reads
|
|
38
|
+
through; `evaluate` is guarded too). If the user presses Stop, the next call throws `BrowserUserStoppedError`: tell
|
|
39
|
+
the user browser use was stopped and start no new session this turn.
|
|
40
|
+
|
|
41
|
+
The guard prevents mistakes by a cooperating agent. It is not a security boundary: code that imports the raw
|
|
42
|
+
entry (`resolveOmowrightEntry()`) is not guarded, and a host without the `omo_browser_bridge` tool cannot show state or
|
|
43
|
+
honor Stop, though questions are still asked.
|
|
44
|
+
|
|
45
|
+
## Step 1 — load omowright and prove the stack
|
|
22
46
|
|
|
23
47
|
```js
|
|
24
48
|
const { loadOmowright } = await import("<skill-root>/scripts/omowright.mjs")
|
|
@@ -94,7 +118,7 @@ changing the extension's automation settings.
|
|
|
94
118
|
out everywhere. No flow here needs it.
|
|
95
119
|
- **`focused: false` by default.** The browser belongs to someone who is probably using it.
|
|
96
120
|
- **One short, named session per task,** always stopped.
|
|
97
|
-
- **Bot-scored or WAF targets go to the owned engine
|
|
121
|
+
- **Bot-scored or WAF targets go to the owned engine** (not while `OMO_BROWSER_ENGINE` is set: then say the site needs a browser the session does not allow). The attached engine's daemon enables console
|
|
98
122
|
capture on every tab it drives, which is a known automation signal; CloakBrowser through
|
|
99
123
|
`connectCloakProfile()` is the stealth path.
|
|
100
124
|
|
|
@@ -98,8 +98,17 @@ Every refusal is a `BskRpcError` with the daemon's own `code`:
|
|
|
98
98
|
| `invalid_params` | wrong option shape | fix the call, do not retry as-is |
|
|
99
99
|
| `permission_denied` | `evaluate` on a tab outside the Agent Window, or a denied borrow | stop; the user said no |
|
|
100
100
|
| `timeout` | the tool did not finish in its budget | read the page state before retrying once |
|
|
101
|
-
| `user_aborted` | the user pressed Stop in the browser | stop the task and report |
|
|
101
|
+
| `user_aborted` | the user pressed Stop in the browser | stop the task and report; under `OMO_BROWSER_ENGINE` this surfaces as `BrowserUserStoppedError` and no session starts again this turn |
|
|
102
102
|
| `cdp_failed` | the page cannot be attached (restricted URL, DevTools open) | say which page and why |
|
|
103
103
|
|
|
104
|
+
Typed errors under `OMO_BROWSER_ENGINE` (see SKILL.md Step 0):
|
|
105
|
+
|
|
106
|
+
| Error | `code` | Meaning |
|
|
107
|
+
|---|---|---|
|
|
108
|
+
| `BrowserNotConnectedError` | `browser_not_connected` | the user's browser is not connected; ask them to connect it |
|
|
109
|
+
| `BrowserEngineRefusal` | `browser_engine_none` / `browser_engine_builtin` / `browser_engine_unsupported` / `browser_tool_blocked` | this engine setting does not allow that call |
|
|
110
|
+
| `BrowserActionDeclinedError` | `user_declined` | the user said no, or no question could be asked; do not retry |
|
|
111
|
+
| `BrowserUserStoppedError` | `user_stopped` | the user stopped browser use |
|
|
112
|
+
|
|
104
113
|
Long calls can be cancelled: `const h = session.client.callWithHandle("tool.navigate", {...})`
|
|
105
114
|
then `await session.client.cancel(h.rpcId)`.
|
|
@@ -8,6 +8,13 @@ Three pieces. The onboarding script prepares all three; a human finishes exactly
|
|
|
8
8
|
| Browser extension | registered from the Web Store listing through Chrome's external-extension mechanism | **yes, up to one click** — the browser asks the user to enable it |
|
|
9
9
|
| Daemon process | auto-starts on the first call | nothing to do |
|
|
10
10
|
|
|
11
|
+
## Inside the OmO desktop app
|
|
12
|
+
|
|
13
|
+
The app installs the `bsk` CLI and starts its daemon itself, and keeps the daemon running when the app closes.
|
|
14
|
+
Do not run `browser-install.mjs` there unless the doctor says the app's engine is missing; a session set to
|
|
15
|
+
`OMO_BROWSER_ENGINE=builtin` or `none` never uses this skill's attached engine at all. `BSK_HOME` and `BSK_BIN`
|
|
16
|
+
still decide where the CLI and daemon are found, here and everywhere else.
|
|
17
|
+
|
|
11
18
|
## Supported
|
|
12
19
|
|
|
13
20
|
| | |
|