cli-jaw 2.17.41 → 2.17.42
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/README.md +3 -3
- package/dist/bin/commands/browser.js +52 -5
- package/dist/bin/commands/browser.js.map +1 -1
- package/dist/src/agent/args.js +69 -19
- package/dist/src/agent/args.js.map +1 -1
- package/dist/src/agent/codex-app-events.js +31 -1
- package/dist/src/agent/codex-app-events.js.map +1 -1
- package/dist/src/agent/cursor-model-inventory.js +140 -0
- package/dist/src/agent/cursor-model-inventory.js.map +1 -0
- package/dist/src/agent/grok-models.js +80 -0
- package/dist/src/agent/grok-models.js.map +1 -0
- package/dist/src/browser/actions.js +383 -20
- package/dist/src/browser/actions.js.map +1 -1
- package/dist/src/browser/grounding-candidate.js +272 -0
- package/dist/src/browser/grounding-candidate.js.map +1 -0
- package/dist/src/browser/grounding-eval.js +247 -0
- package/dist/src/browser/grounding-eval.js.map +1 -0
- package/dist/src/browser/image-size.js +63 -0
- package/dist/src/browser/image-size.js.map +1 -0
- package/dist/src/browser/index.js +1 -0
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/browser/occlusion.js +148 -0
- package/dist/src/browser/occlusion.js.map +1 -0
- package/dist/src/browser/verify-candidate.js +162 -0
- package/dist/src/browser/verify-candidate.js.map +1 -0
- package/dist/src/browser/vision-input.js +74 -0
- package/dist/src/browser/vision-input.js.map +1 -0
- package/dist/src/browser/vision-provider.js +75 -0
- package/dist/src/browser/vision-provider.js.map +1 -0
- package/dist/src/browser/vision.js +360 -47
- package/dist/src/browser/vision.js.map +1 -1
- package/dist/src/browser/web-ai/candidate-reconcile.js +14 -5
- package/dist/src/browser/web-ai/candidate-reconcile.js.map +1 -1
- package/dist/src/cli/claude-model-discovery.js +175 -0
- package/dist/src/cli/claude-model-discovery.js.map +1 -0
- package/dist/src/cli/claude-models.js +46 -0
- package/dist/src/cli/claude-models.js.map +1 -1
- package/dist/src/cli/registry-live.js +104 -1
- package/dist/src/cli/registry-live.js.map +1 -1
- package/dist/src/cli/registry.js +28 -5
- package/dist/src/cli/registry.js.map +1 -1
- package/dist/src/code-mode/host.js +6 -0
- package/dist/src/code-mode/host.js.map +1 -1
- package/dist/src/code-mode/manager.js +37 -8
- package/dist/src/code-mode/manager.js.map +1 -1
- package/dist/src/code-mode/providers/catalog.js +53 -1
- package/dist/src/code-mode/providers/catalog.js.map +1 -1
- package/dist/src/code-mode/providers/codex-app.js +6 -0
- package/dist/src/code-mode/providers/codex-app.js.map +1 -1
- package/dist/src/code-mode/providers/live-models.js +85 -0
- package/dist/src/code-mode/providers/live-models.js.map +1 -0
- package/dist/src/code-mode/providers/provider-live-models.js +120 -0
- package/dist/src/code-mode/providers/provider-live-models.js.map +1 -0
- package/dist/src/code-mode/session.js +37 -0
- package/dist/src/code-mode/session.js.map +1 -1
- package/dist/src/prompt/templates/a1-system.md +24 -14
- package/dist/src/prompt/templates/control-system.md +6 -11
- package/dist/src/prompt/templates/employee.md +5 -5
- package/dist/src/routes/browser.js +17 -5
- package/dist/src/routes/browser.js.map +1 -1
- package/package.json +1 -1
- package/public/css/activity.css +20 -1
- package/public/css/layout.css +7 -3
- package/public/dist/.vite/manifest.json +104 -105
- package/public/dist/assets/Agent-CQ4EBgY8.js +1 -0
- package/public/dist/assets/CodeCanvas-DWsx6sv5.js +2 -0
- package/public/dist/assets/DashboardMeta-Dh-tasbY.js +1 -0
- package/public/dist/assets/Display-NrqY_IaN.js +1 -0
- package/public/dist/assets/{Permissions-BzbnqenZ.js → Permissions-Bh947tUG.js} +1 -1
- package/public/dist/assets/Permissions-DzNJCt8K.js +1 -0
- package/public/dist/assets/app-BCBnxiGr.css +1 -0
- package/public/dist/assets/{app-x5Kray4W.js → app-Q0prKOFx.js} +4 -4
- package/public/dist/assets/{bgtask-badge-W9djRQVu.js → bgtask-badge-DAcWaUfS.js} +1 -1
- package/public/dist/assets/bgtask-badge-DHA2OQm8.js +1 -0
- package/public/dist/assets/{employees--_dmDw_a.js → employees-CWS3Juca.js} +1 -1
- package/public/dist/assets/{iframe-renderer-D4BmLtAq.js → iframe-renderer-DCDKBMb7.js} +2 -2
- package/public/dist/assets/iframe-renderer-RpvBly2x.js +1 -0
- package/public/dist/assets/manager-Nk7-RYPs.css +1 -0
- package/public/dist/assets/manager-XkR-DRaQ.js +5 -0
- package/public/dist/assets/manager-shortcuts-jsiX-zV6.js +1 -0
- package/public/dist/assets/memory-4YH8sz0L.js +1 -0
- package/public/dist/assets/{memory-DENQWzM8.js → memory-DhNdKZLO.js} +1 -1
- package/public/dist/assets/{message-history-BsrLcGJj.js → message-history-DbRTl1P0.js} +1 -1
- package/public/dist/assets/message-history-lVcLpAw8.js +1 -0
- package/public/dist/assets/{render-CF6Q1oIh.js → render-DP8SJtdZ.js} +1 -1
- package/public/dist/assets/settings--EGy3_B9.js +1 -0
- package/public/dist/assets/settings-85n9NjvX.js +1 -0
- package/public/dist/assets/{settings-B-C_jdZR.js → settings-C9_D-pn0.js} +32 -32
- package/public/dist/assets/settings-core-8wBl1mg-.js +1 -0
- package/public/dist/assets/{settings-core-BzAJLsKN.js → settings-core-BcVD2FgV.js} +1 -1
- package/public/dist/assets/{settings-embedding-dEMyByVi.js → settings-embedding-BSpq785l.js} +1 -1
- package/public/dist/assets/{settings-embedding-BSA42oKM.css → settings-embedding-LIMq5ebM.css} +1 -1
- package/public/dist/assets/{sidebar-fFEOpvJl.js → sidebar-BVhLb5Ft.js} +3 -3
- package/public/dist/assets/skills-6GFkHTlQ.js +1 -0
- package/public/dist/assets/{skills-Da_b06ci.js → skills-X7qxmHka.js} +1 -1
- package/public/dist/assets/{slash-commands-BbPUHHF4.js → slash-commands-BnDH4IU_.js} +1 -1
- package/public/dist/assets/slash-commands-qu8fPAZ3.js +1 -0
- package/public/dist/assets/{trace-drawer-B-QmzUfP.js → trace-drawer-DasaS1z4.js} +1 -1
- package/public/dist/assets/{ui-CkRUrIfp.js → ui-BtiBTXng.js} +1 -1
- package/public/dist/assets/ui-BucJ1zMA.js +1 -0
- package/public/dist/index.html +2 -2
- package/public/dist/manager/index.html +3 -3
- package/public/dist/settings/index.html +2 -2
- package/public/js/features/settings.ts +16 -0
- package/public/manager/src/App.tsx +10 -12
- package/public/manager/src/AppChrome.tsx +2 -3
- package/public/manager/src/InstancePreview.tsx +47 -0
- package/public/manager/src/SidebarRailRouter.tsx +34 -30
- package/public/manager/src/code/CodeComposer.tsx +31 -6
- package/public/manager/src/code/CodeSessionList.tsx +25 -7
- package/public/manager/src/code/CodeToastHost.tsx +49 -0
- package/public/manager/src/code/CodeTranscript.tsx +136 -26
- package/public/manager/src/code/CodeWorkbench.tsx +16 -2
- package/public/manager/src/code/ComposerFooter.tsx +130 -46
- package/public/manager/src/code/ContextUsageMeter.tsx +72 -0
- package/public/manager/src/code/ProviderGlyph.tsx +58 -0
- package/public/manager/src/code/code-composer.css +159 -175
- package/public/manager/src/code/code-controller-runtime.ts +3 -1
- package/public/manager/src/code/code-session-state.ts +7 -1
- package/public/manager/src/code/code-toasts.ts +73 -0
- package/public/manager/src/code/code-workbench.css +42 -1
- package/public/manager/src/code/code.css +130 -15
- package/public/manager/src/code/context-usage.ts +74 -0
- package/public/manager/src/code/pending-user-item.ts +46 -0
- package/public/manager/src/code/session-order.ts +70 -0
- package/public/manager/src/code/tool-summary.ts +101 -0
- package/public/manager/src/code/use-dictation.ts +130 -0
- package/public/manager/src/components/CommandCenter.tsx +4 -8
- package/public/manager/src/components/InstanceDetailPanel.tsx +0 -18
- package/public/manager/src/components/Workbench.tsx +6 -10
- package/public/manager/src/components/WorkbenchHeader.tsx +0 -11
- package/public/manager/src/dashboard-settings/dashboard-settings-ui.ts +3 -1
- package/public/manager/src/hooks/useDashboardView.ts +38 -38
- package/public/manager/src/manager-components.css +0 -8
- package/public/manager/src/manager-shortcut-runner.ts +0 -1
- package/public/manager/src/manager-shortcuts.ts +0 -2
- package/public/manager/src/settings/pages/Agent.tsx +5 -2
- package/public/manager/src/settings/pages/DashboardMeta.tsx +20 -1
- package/public/manager/src/settings/pages/Permissions.tsx +23 -4
- package/public/manager/src/settings/pages/components/agent/PermissionQuickSection.tsx +6 -2
- package/public/manager/src/settings/pages/components/agent/agent-save.ts +24 -1
- package/public/manager/src/settings/settings-icons.ts +3 -1
- package/public/manager/src/settings/settings-registry.ts +3 -3
- package/public/manager/src/settings-controls.css +0 -1
- package/public/manager/src/settings-standalone.tsx +8 -8
- package/public/manager/src/types.ts +0 -1
- package/scripts/check-private-boundary.mjs +101 -0
- package/scripts/grounding-eval.mjs +223 -0
- package/public/dist/assets/Agent-B8ZEWJ6B.js +0 -1
- package/public/dist/assets/CodeCanvas-Bj3lTSu6.js +0 -2
- package/public/dist/assets/DashboardMeta-CmZavI4H.js +0 -1
- package/public/dist/assets/Display-DFkjKcMk.js +0 -1
- package/public/dist/assets/Permissions-CJftCO6p.js +0 -1
- package/public/dist/assets/app-DVbrIl8f.css +0 -1
- package/public/dist/assets/bgtask-badge-CuAc8bAl.js +0 -1
- package/public/dist/assets/iframe-renderer-By2WO5bd.js +0 -1
- package/public/dist/assets/manager-CxLXWCGi.css +0 -1
- package/public/dist/assets/manager-DA3dF3eN.js +0 -5
- package/public/dist/assets/manager-shortcuts-CzGkE0ZG.js +0 -1
- package/public/dist/assets/memory-thjr2apY.js +0 -1
- package/public/dist/assets/message-history-CfU7_P5t.js +0 -1
- package/public/dist/assets/settings-DXA5L4DR.js +0 -1
- package/public/dist/assets/settings-Dy_IaNVH.js +0 -1
- package/public/dist/assets/settings-core-D7sAQqhC.js +0 -1
- package/public/dist/assets/skills-BcIievZp.js +0 -1
- package/public/dist/assets/slash-commands-B-5Shayl.js +0 -1
- package/public/dist/assets/ui-CJLdMfRx.js +0 -1
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/browser/occlusion.ts — what will actually receive a click at a point.
|
|
3
|
+
*
|
|
4
|
+
* A coordinate click dispatches wherever the point lands. If a cookie banner,
|
|
5
|
+
* modal or sticky header covers the target, that element receives the click
|
|
6
|
+
* and the call still reports success — the most common silent failure in
|
|
7
|
+
* coordinate-based automation, because nothing in the response says the wrong
|
|
8
|
+
* thing was clicked.
|
|
9
|
+
*
|
|
10
|
+
* Ported from BrowserOS's hit test. Relatedness is decided IN THE PAGE against
|
|
11
|
+
* the actual node, because that is the only place element identity is
|
|
12
|
+
* knowable: comparing a reconciled ref's ARIA role against a DOM tag name is a
|
|
13
|
+
* category error, since a `link` is an `a`, a `textbox` is an `input`, and a
|
|
14
|
+
* `button` is often a `div`.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Decide whether a hit means the click would go somewhere unintended.
|
|
18
|
+
*
|
|
19
|
+
* With no marked target — a pure coordinate click into canvas — there is
|
|
20
|
+
* nothing to contradict, so the verdict is `unknown` rather than `clear`.
|
|
21
|
+
* No evidence of a problem is not evidence of no problem, and collapsing the
|
|
22
|
+
* two would let an unchecked click report as a verified one.
|
|
23
|
+
*/
|
|
24
|
+
export function judgeHit(hit) {
|
|
25
|
+
// The hit test could not run — an unreadable cross-origin frame, a page
|
|
26
|
+
// that navigated mid-check. Fail OPEN: an infrastructure failure must not
|
|
27
|
+
// block a legitimate click, and pretending to know is worse than saying
|
|
28
|
+
// we do not.
|
|
29
|
+
if (!hit)
|
|
30
|
+
return { blocked: false, reason: 'unknown' };
|
|
31
|
+
// The walk stopped at a frame it could not see into. Whatever is behind it
|
|
32
|
+
// is unknowable, and a target inside that frame would look identical to a
|
|
33
|
+
// cover. Refusing on that evidence would block legitimate clicks into
|
|
34
|
+
// every cross-origin embed.
|
|
35
|
+
if (hit.opaqueFrame)
|
|
36
|
+
return { blocked: false, reason: 'unknown' };
|
|
37
|
+
if (hit.relatesToTarget === undefined)
|
|
38
|
+
return { blocked: false, reason: 'unknown' };
|
|
39
|
+
if (hit.relatesToTarget)
|
|
40
|
+
return { blocked: false, reason: 'clear' };
|
|
41
|
+
return {
|
|
42
|
+
blocked: true,
|
|
43
|
+
blocker: hit.descriptor,
|
|
44
|
+
reason: hit.crossedFrame
|
|
45
|
+
? `the point resolves into an iframe, to ${hit.descriptor}, not the intended target`
|
|
46
|
+
: `${hit.descriptor} covers the target at this point`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The page-side hit test. **A real function, deliberately not a source string.**
|
|
51
|
+
*
|
|
52
|
+
* Playwright sets `isFunction: typeof pageFunction === "function"`. Pass a
|
|
53
|
+
* string and the page evaluates the expression, obtains a function object, and
|
|
54
|
+
* never calls it — the result serializes to `undefined`. An earlier version of
|
|
55
|
+
* this shipped as a string, which made the entire check a permanent no-op. It
|
|
56
|
+
* was invisible precisely because the guard fails open: `undefined` became
|
|
57
|
+
* `null` became "unknown", indistinguishable from the intended safe path.
|
|
58
|
+
*
|
|
59
|
+
* Everything below runs in the page, so it may only use DOM globals.
|
|
60
|
+
*/
|
|
61
|
+
export function hitTestInPage(arg) {
|
|
62
|
+
const describe = (el) => {
|
|
63
|
+
if (!el || !el.tagName)
|
|
64
|
+
return 'element';
|
|
65
|
+
const tag = String(el.tagName).toLowerCase();
|
|
66
|
+
if (el.id)
|
|
67
|
+
return tag + '#' + el.id;
|
|
68
|
+
if (typeof el.className === 'string' && el.className.trim()) {
|
|
69
|
+
return tag + '.' + el.className.trim().split(/\s+/)[0];
|
|
70
|
+
}
|
|
71
|
+
return tag;
|
|
72
|
+
};
|
|
73
|
+
const up = (node) => {
|
|
74
|
+
if (!node)
|
|
75
|
+
return null;
|
|
76
|
+
return node.parentNode || node.host || (node.getRootNode ? node.getRootNode().host ?? null : null) || null;
|
|
77
|
+
};
|
|
78
|
+
const reaches = (from, to) => {
|
|
79
|
+
for (let node = from, guard = 0; node && guard < 200; node = up(node), guard++) {
|
|
80
|
+
if (node === to)
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
84
|
+
};
|
|
85
|
+
let target = null;
|
|
86
|
+
if (arg.targetPoint) {
|
|
87
|
+
target = document.elementFromPoint(arg.targetPoint.x, arg.targetPoint.y);
|
|
88
|
+
}
|
|
89
|
+
let doc = document;
|
|
90
|
+
let x = arg.x;
|
|
91
|
+
let y = arg.y;
|
|
92
|
+
let crossedFrame = false;
|
|
93
|
+
let opaqueFrame = false;
|
|
94
|
+
let hit = doc.elementFromPoint(x, y);
|
|
95
|
+
// Bounded: a frame chain should be shallow, and a cycle must not hang the page.
|
|
96
|
+
for (let depth = 0; depth < 16; depth++) {
|
|
97
|
+
if (!hit || (hit.tagName !== 'IFRAME' && hit.tagName !== 'FRAME'))
|
|
98
|
+
break;
|
|
99
|
+
if (target && (hit === target || reaches(target, hit)))
|
|
100
|
+
break;
|
|
101
|
+
let childDoc = null;
|
|
102
|
+
try {
|
|
103
|
+
childDoc = hit.contentDocument ?? null;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
childDoc = null;
|
|
107
|
+
}
|
|
108
|
+
if (!childDoc) {
|
|
109
|
+
opaqueFrame = true;
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
const rect = hit.getBoundingClientRect();
|
|
113
|
+
// elementFromPoint in the child is relative to the child's own
|
|
114
|
+
// viewport, so subtract the frame's position and its border.
|
|
115
|
+
x -= rect.left + hit.clientLeft;
|
|
116
|
+
y -= rect.top + hit.clientTop;
|
|
117
|
+
doc = childDoc;
|
|
118
|
+
crossedFrame = true;
|
|
119
|
+
const next = doc.elementFromPoint(x, y);
|
|
120
|
+
if (!next)
|
|
121
|
+
break;
|
|
122
|
+
hit = next;
|
|
123
|
+
}
|
|
124
|
+
if (!hit)
|
|
125
|
+
return null;
|
|
126
|
+
const ancestry = [];
|
|
127
|
+
for (let node = up(hit), guard = 0; node && guard < 24; node = up(node), guard++) {
|
|
128
|
+
if (node.tagName)
|
|
129
|
+
ancestry.push(describe(node));
|
|
130
|
+
}
|
|
131
|
+
const result = { descriptor: describe(hit), ancestry, crossedFrame };
|
|
132
|
+
if (opaqueFrame)
|
|
133
|
+
result.opaqueFrame = true;
|
|
134
|
+
if (target) {
|
|
135
|
+
// The hit is the target, inside it, or contains it. Clicking a
|
|
136
|
+
// button's inner span is clicking the button. A label forwarding to
|
|
137
|
+
// this control counts too.
|
|
138
|
+
let related = hit === target || reaches(hit, target) || reaches(target, hit);
|
|
139
|
+
if (!related && hit.closest) {
|
|
140
|
+
const lbl = hit.closest('label');
|
|
141
|
+
if (lbl && (lbl.control === target || (lbl.contains ? lbl.contains(target) : false)))
|
|
142
|
+
related = true;
|
|
143
|
+
}
|
|
144
|
+
result.relatesToTarget = related;
|
|
145
|
+
}
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=occlusion.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"occlusion.js","sourceRoot":"","sources":["../../../src/browser/occlusion.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/browser/verify-candidate.ts — a second look that can actually disagree.
|
|
3
|
+
*
|
|
4
|
+
* The option this replaces re-ran the identical query against the identical
|
|
5
|
+
* screenshot and then discarded the coordinates, keeping only whether the
|
|
6
|
+
* second answer still found something. Same input, same question: the only way
|
|
7
|
+
* it could fail was model non-determinism, and the only thing it reliably
|
|
8
|
+
* bought was a second round-trip. Its test asserted the string
|
|
9
|
+
* `verifyBeforeClick` appeared in the source, so a guard that guarded nothing
|
|
10
|
+
* passed a suite that checked nothing.
|
|
11
|
+
*
|
|
12
|
+
* A verification that can disagree has to change the input. Cropping tightly
|
|
13
|
+
* around the candidate and asking again does that: the target now dominates
|
|
14
|
+
* the frame, so a model that was guessing has to guess differently, and an
|
|
15
|
+
* answer landing far from the original candidate is evidence it was wrong.
|
|
16
|
+
*
|
|
17
|
+
* Geometry only. The capture and the model call belong to the caller.
|
|
18
|
+
*/
|
|
19
|
+
/** Default crop around a candidate, in CSS pixels. */
|
|
20
|
+
export const VERIFY_CROP = { width: 280, height: 200 };
|
|
21
|
+
/**
|
|
22
|
+
* How far from the original candidate a second answer may land, as a fraction
|
|
23
|
+
* of the crop. Beyond this the two answers point at different things, and the
|
|
24
|
+
* disagreement matters more than either one.
|
|
25
|
+
*
|
|
26
|
+
* It is a fraction per axis, so on the default 280x200 crop it permits 126px
|
|
27
|
+
* horizontally and 90px vertically — one number, two distances.
|
|
28
|
+
*/
|
|
29
|
+
export const VERIFY_DRIFT_LIMIT = 0.45;
|
|
30
|
+
/**
|
|
31
|
+
* A crop centred on the candidate, kept inside the viewport.
|
|
32
|
+
*
|
|
33
|
+
* Being outside the viewport is fixed by SHIFTING the rectangle, so a
|
|
34
|
+
* candidate near an edge still gets a full-size crop and the drift threshold
|
|
35
|
+
* means the same thing everywhere. A viewport smaller than the crop is the one
|
|
36
|
+
* case that genuinely shrinks it, since there is nothing else to do.
|
|
37
|
+
*/
|
|
38
|
+
export function cropAroundPoint(point, viewport, size = VERIFY_CROP) {
|
|
39
|
+
const width = Math.min(size.width, viewport.width);
|
|
40
|
+
const height = Math.min(size.height, viewport.height);
|
|
41
|
+
const x = Math.max(0, Math.min(Math.round(point.x - width / 2), viewport.width - width));
|
|
42
|
+
const y = Math.max(0, Math.min(Math.round(point.y - height / 2), viewport.height - height));
|
|
43
|
+
return { x, y, width, height };
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* How long an observation may be trusted before the click it informs.
|
|
47
|
+
*
|
|
48
|
+
* Verification adds a second model round-trip, which roughly doubles the
|
|
49
|
+
* interval between seeing the page and acting on it. The freshness guard on
|
|
50
|
+
* the reconciliation path catches navigation, but nothing catches scroll,
|
|
51
|
+
* reflow or animation — so a long enough gap means the coordinate describes a
|
|
52
|
+
* page that no longer exists.
|
|
53
|
+
*
|
|
54
|
+
* This does not detect movement. It bounds how long we are willing to assume
|
|
55
|
+
* there was none, which is a weaker claim and the honest one.
|
|
56
|
+
*/
|
|
57
|
+
export const OBSERVATION_MAX_AGE_MS = 30_000;
|
|
58
|
+
export function isObservationStale(observedAt, now, maxAgeMs = OBSERVATION_MAX_AGE_MS) {
|
|
59
|
+
// A clock that ran backwards tells us nothing, so treat it as unusable
|
|
60
|
+
// rather than as freshness.
|
|
61
|
+
if (!Number.isFinite(observedAt) || !Number.isFinite(now))
|
|
62
|
+
return true;
|
|
63
|
+
return now - observedAt > maxAgeMs || now < observedAt;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* How much of the freshness budget is left, in milliseconds.
|
|
67
|
+
*
|
|
68
|
+
* A provider child was given a flat 60s while the observation it works from
|
|
69
|
+
* expired at 30s, so two round-trips could spend two minutes producing an
|
|
70
|
+
* answer the very next line was guaranteed to refuse. Deriving the child's
|
|
71
|
+
* timeout from what remains kills it at the moment its answer stops being
|
|
72
|
+
* usable instead of at an unrelated constant.
|
|
73
|
+
*
|
|
74
|
+
* Never negative, and never above `cap`: the existing 60s ceiling still
|
|
75
|
+
* applies when the budget is larger than it.
|
|
76
|
+
*/
|
|
77
|
+
export function remainingObservationBudget(observedAt, now, cap, maxAgeMs = OBSERVATION_MAX_AGE_MS) {
|
|
78
|
+
if (!Number.isFinite(observedAt) || !Number.isFinite(now) || !Number.isFinite(cap))
|
|
79
|
+
return 0;
|
|
80
|
+
const spent = now - observedAt;
|
|
81
|
+
if (spent < 0)
|
|
82
|
+
return 0;
|
|
83
|
+
return Math.max(0, Math.min(cap, maxAgeMs - spent));
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Fit a requested capture rectangle inside the viewport.
|
|
87
|
+
*
|
|
88
|
+
* Playwright trims a clip to the viewport before capturing, so a rectangle
|
|
89
|
+
* extending past the edge yields a smaller image than asked for — and one
|
|
90
|
+
* lying entirely outside fails with an opaque assertion about the clipped area
|
|
91
|
+
* being empty. Neither says which rectangle was wrong or what the viewport was.
|
|
92
|
+
*
|
|
93
|
+
* Clamping here means the caller learns the capture was truncated, and a
|
|
94
|
+
* genuinely impossible rectangle gets a named error instead.
|
|
95
|
+
*/
|
|
96
|
+
export function clampClipToViewport(clip, viewport) {
|
|
97
|
+
if (clip.x >= viewport.width || clip.y >= viewport.height) {
|
|
98
|
+
throw new Error(`clip (${clip.x}, ${clip.y}) starts outside the ${viewport.width}x${viewport.height} viewport`);
|
|
99
|
+
}
|
|
100
|
+
const width = Math.min(clip.width, viewport.width - clip.x);
|
|
101
|
+
const height = Math.min(clip.height, viewport.height - clip.y);
|
|
102
|
+
if (width <= 0 || height <= 0) {
|
|
103
|
+
throw new Error(`clip ${clip.width}x${clip.height} at (${clip.x}, ${clip.y}) has no area inside the ${viewport.width}x${viewport.height} viewport`);
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
clip: { x: clip.x, y: clip.y, width, height },
|
|
107
|
+
truncated: width !== clip.width || height !== clip.height,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Judge a second answer taken inside `crop`.
|
|
112
|
+
*
|
|
113
|
+
* `localPoint` is where the re-run landed within the crop, in CSS pixels
|
|
114
|
+
* relative to the crop's own origin. The returned point is back in page
|
|
115
|
+
* coordinates, so a caller that agrees can use it directly — the second look
|
|
116
|
+
* replaces the first estimate rather than merely blessing it.
|
|
117
|
+
*
|
|
118
|
+
* Drift is measured from the ORIGINAL CANDIDATE, not from the crop centre.
|
|
119
|
+
* Those coincide in the middle of a viewport but not near an edge, where the
|
|
120
|
+
* crop clamps: a candidate 5px from the left sits 5px into its crop while the
|
|
121
|
+
* crop centre is 140px in. Measuring from the centre there would refuse a
|
|
122
|
+
* second answer that re-found the exact same point — the one case that should
|
|
123
|
+
* agree most strongly.
|
|
124
|
+
*/
|
|
125
|
+
export function judgeVerification(localPoint, crop, candidate, driftLimit = VERIFY_DRIFT_LIMIT) {
|
|
126
|
+
// The target is not in a crop centred on where we thought it was. That is
|
|
127
|
+
// the first answer being wrong, which is exactly what this is for.
|
|
128
|
+
if (!localPoint)
|
|
129
|
+
return { agreed: false, reason: 'the target was not in the verification crop' };
|
|
130
|
+
// The model answered about an image of exactly this size, so a point
|
|
131
|
+
// outside it is impossible by construction and means the scaling was
|
|
132
|
+
// wrong. Without this, a candidate near a shifted crop's edge can drift
|
|
133
|
+
// the permitted fraction and land off the far side — and off the viewport,
|
|
134
|
+
// since the returned point is clicked directly.
|
|
135
|
+
if (localPoint.x < 0 || localPoint.y < 0
|
|
136
|
+
|| localPoint.x > crop.width || localPoint.y > crop.height) {
|
|
137
|
+
return {
|
|
138
|
+
agreed: false,
|
|
139
|
+
reason: `the second answer (${Math.round(localPoint.x)}, ${Math.round(localPoint.y)}) is outside the ${crop.width}x${crop.height} verification crop`,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
// Fall back to the crop centre only when no candidate was supplied, which
|
|
143
|
+
// is the same point whenever the crop was not clamped.
|
|
144
|
+
const cx = candidate ? candidate.x - crop.x : crop.width / 2;
|
|
145
|
+
const cy = candidate ? candidate.y - crop.y : crop.height / 2;
|
|
146
|
+
const dx = Math.abs(localPoint.x - cx) / crop.width;
|
|
147
|
+
const dy = Math.abs(localPoint.y - cy) / crop.height;
|
|
148
|
+
const drift = Math.max(dx, dy);
|
|
149
|
+
if (drift > driftLimit) {
|
|
150
|
+
return {
|
|
151
|
+
agreed: false,
|
|
152
|
+
reason: `the second answer drifted ${Math.round(drift * 100)}% from the original candidate`,
|
|
153
|
+
drift,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
agreed: true,
|
|
158
|
+
point: { x: crop.x + localPoint.x, y: crop.y + localPoint.y },
|
|
159
|
+
drift,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
//# sourceMappingURL=verify-candidate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify-candidate.js","sourceRoot":"","sources":["../../../src/browser/verify-candidate.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/browser/vision-input.ts — untrusted-input handling for the vision path.
|
|
3
|
+
*
|
|
4
|
+
* Kept separate from vision.ts so it can be tested without pulling in the CDP
|
|
5
|
+
* action layer: these are pure functions over strings, and they guard the one
|
|
6
|
+
* place where caller-supplied text reaches a child process.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* The target description reaches the vision path from the HTTP body of
|
|
10
|
+
* `POST /api/browser/vision-click` and is interpolated into a prompt for a
|
|
11
|
+
* child process. Treat it as untrusted text, not as instructions.
|
|
12
|
+
*
|
|
13
|
+
* Be clear about what this is worth. The prompt's delimited span is a
|
|
14
|
+
* model-behavior hint, not a security boundary; the instructions that follow
|
|
15
|
+
* the span are what actually constrain the answer. This function's job is
|
|
16
|
+
* narrower: keep the target on one line, stop it closing its own delimiter,
|
|
17
|
+
* and bound its size. It does not, and cannot, stop the text from *reading*
|
|
18
|
+
* like an instruction.
|
|
19
|
+
*/
|
|
20
|
+
export const MAX_TARGET_LENGTH = 200;
|
|
21
|
+
/**
|
|
22
|
+
* Collapse every run of two or more double quotes to a single one.
|
|
23
|
+
*
|
|
24
|
+
* A single `replace(/"""/g, '"')` is not enough: `String.replace` scans left
|
|
25
|
+
* to right without rescanning its own output, so `n` quotes become
|
|
26
|
+
* `floor(n/3) + n%3`, and any `n >= 7` still leaves three in a row. Matching
|
|
27
|
+
* runs of length two or more removes the whole class in one pass, so there is
|
|
28
|
+
* no arity that survives.
|
|
29
|
+
*/
|
|
30
|
+
function collapseQuoteRuns(value) {
|
|
31
|
+
return value.replace(/"{2,}/g, '"');
|
|
32
|
+
}
|
|
33
|
+
/** Drop a trailing high surrogate so a truncated target stays well-formed. */
|
|
34
|
+
function trimLoneSurrogate(value) {
|
|
35
|
+
return /[\uD800-\uDBFF]$/.test(value) ? value.slice(0, -1) : value;
|
|
36
|
+
}
|
|
37
|
+
export function sanitizeTarget(target) {
|
|
38
|
+
if (typeof target !== 'string')
|
|
39
|
+
throw new Error('vision-click target must be a string');
|
|
40
|
+
const cleaned = collapseQuoteRuns(target
|
|
41
|
+
// C0 and C1 controls, plus the Unicode line and paragraph
|
|
42
|
+
// separators, which JS \s also matches but which are line breaks
|
|
43
|
+
// rather than ordinary whitespace and belong in this class.
|
|
44
|
+
.replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]+/g, ' ')
|
|
45
|
+
// Zero-width characters can split a token invisibly and pad length.
|
|
46
|
+
.replace(/[\u200b-\u200f\u2060\ufeff]+/g, '')
|
|
47
|
+
// Bidirectional overrides can reorder the value where it is echoed
|
|
48
|
+
// back to a terminal.
|
|
49
|
+
.replace(/[\u202a-\u202e\u2066-\u2069]+/g, ''))
|
|
50
|
+
.replace(/\s+/g, ' ')
|
|
51
|
+
.trim();
|
|
52
|
+
if (!cleaned)
|
|
53
|
+
throw new Error('vision-click target is empty after sanitization');
|
|
54
|
+
return cleaned.length > MAX_TARGET_LENGTH
|
|
55
|
+
? trimLoneSurrogate(cleaned.slice(0, MAX_TARGET_LENGTH))
|
|
56
|
+
: cleaned;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* An agentic `codex exec` can emit command output, so its stdout is not
|
|
60
|
+
* bounded by the answer we asked for. Keep the newest units: the coordinate
|
|
61
|
+
* JSON arrives in a late event and the scan reads from the end, so a truncated
|
|
62
|
+
* head line is expected garbage and the parser already skips it.
|
|
63
|
+
*
|
|
64
|
+
* This counts UTF-16 code units, not bytes — non-ASCII output can occupy up to
|
|
65
|
+
* four bytes per unit, so treat it as an order-of-magnitude bound.
|
|
66
|
+
*/
|
|
67
|
+
export const MAX_CODEX_STDOUT_UNITS = 1024 * 1024;
|
|
68
|
+
/** @deprecated Misnamed: this bounds code units. Use MAX_CODEX_STDOUT_UNITS. */
|
|
69
|
+
export const MAX_CODEX_STDOUT_BYTES = MAX_CODEX_STDOUT_UNITS;
|
|
70
|
+
export function appendBounded(buffer, chunk, limit = MAX_CODEX_STDOUT_UNITS) {
|
|
71
|
+
const next = buffer + chunk;
|
|
72
|
+
return next.length > limit ? next.slice(next.length - limit) : next;
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=vision-input.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vision-input.js","sourceRoot":"","sources":["../../../src/browser/vision-input.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,GAAG,CAAC;AAErC;;;;;;;;GAQG;AACH,SAAS,iBAAiB,CAAC,KAAa;IACpC,OAAO,KAAK,CAAC,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;AACxC,CAAC;AAED,8EAA8E;AAC9E,SAAS,iBAAiB,CAAC,KAAa;IACpC,OAAO,kBAAkB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;AACvE,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,MAAe;IAC1C,IAAI,OAAO,MAAM,KAAK,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;IACxF,MAAM,OAAO,GAAG,iBAAiB,CAC7B,MAAM;QACF,0DAA0D;QAC1D,iEAAiE;QACjE,4DAA4D;SAC3D,OAAO,CAAC,4CAA4C,EAAE,GAAG,CAAC;QAC3D,oEAAoE;SACnE,OAAO,CAAC,+BAA+B,EAAE,EAAE,CAAC;QAC7C,mEAAmE;QACnE,sBAAsB;SACrB,OAAO,CAAC,gCAAgC,EAAE,EAAE,CAAC,CACrD;SACI,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;SACpB,IAAI,EAAE,CAAC;IACZ,IAAI,CAAC,OAAO;QAAE,MAAM,IAAI,KAAK,CAAC,iDAAiD,CAAC,CAAC;IACjF,OAAO,OAAO,CAAC,MAAM,GAAG,iBAAiB;QACrC,CAAC,CAAC,iBAAiB,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,iBAAiB,CAAC,CAAC;QACxD,CAAC,CAAC,OAAO,CAAC;AAClB,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,IAAI,GAAG,IAAI,CAAC;AAElD,gFAAgF;AAChF,MAAM,CAAC,MAAM,sBAAsB,GAAG,sBAAsB,CAAC;AAE7D,MAAM,UAAU,aAAa,CAAC,MAAc,EAAE,KAAa,EAAE,KAAK,GAAG,sBAAsB;IACvF,MAAM,IAAI,GAAG,MAAM,GAAG,KAAK,CAAC;IAC5B,OAAO,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACxE,CAAC"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/browser/vision-provider.ts — how the vision provider is invoked.
|
|
3
|
+
*
|
|
4
|
+
* The invocation used to be a literal array inside the pipeline, which is why
|
|
5
|
+
* it drifted from the documented safety position without anyone noticing:
|
|
6
|
+
* `--dangerously-bypass-approvals-and-sandbox` was passed unconditionally for
|
|
7
|
+
* what is a pure image-classification call, while the skill docs stated that
|
|
8
|
+
* cli-jaw never adds it automatically. Describing the invocation here makes
|
|
9
|
+
* that disagreement visible and testable instead of buried in a spawn call.
|
|
10
|
+
*
|
|
11
|
+
* The flag cannot simply be deleted. It is documented as the only known
|
|
12
|
+
* workaround for the Windows sandbox killing `codex exec` children with exit
|
|
13
|
+
* `-1073741502` and an empty stderr. So it becomes an explicit opt-in that is
|
|
14
|
+
* off by default, and the Windows consequence is stated rather than implied.
|
|
15
|
+
*
|
|
16
|
+
* Note the contrast with `agent/args.ts`, which does add the flag when the
|
|
17
|
+
* user selected the Auto (YOLO) permission policy. That is not the same case:
|
|
18
|
+
* an agent turn exists to run commands, so choosing Auto is the user saying
|
|
19
|
+
* they accept that. A vision lookup runs none, so it starts from off.
|
|
20
|
+
*/
|
|
21
|
+
/** Disables the codexclaw plugin for the duration of the lookup. */
|
|
22
|
+
export const CODEXCLAW_PLUGIN_DISABLE_CONFIG = 'plugins."codexclaw@personal".enabled=false';
|
|
23
|
+
/**
|
|
24
|
+
* Build the argument list for a vision lookup.
|
|
25
|
+
*
|
|
26
|
+
* Pure, so the exact invocation can be asserted without spawning anything —
|
|
27
|
+
* which matters here, because the defect this replaces was a single wrong
|
|
28
|
+
* argument that no test could see.
|
|
29
|
+
*/
|
|
30
|
+
export function buildVisionInvocation(opts) {
|
|
31
|
+
const bypassedSandbox = opts.bypassSandbox === true;
|
|
32
|
+
const args = [
|
|
33
|
+
'exec', '-i', opts.screenshotPath, '--json',
|
|
34
|
+
// Do not persist a thread for a one-shot lookup.
|
|
35
|
+
'--ephemeral',
|
|
36
|
+
'-c', CODEXCLAW_PLUGIN_DISABLE_CONFIG,
|
|
37
|
+
'--skip-git-repo-check',
|
|
38
|
+
];
|
|
39
|
+
if (bypassedSandbox)
|
|
40
|
+
args.push('--dangerously-bypass-approvals-and-sandbox');
|
|
41
|
+
args.push(opts.prompt);
|
|
42
|
+
return { command: 'codex', args, bypassedSandbox };
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Explain a spawn failure in terms of the sandbox, when that is what it was.
|
|
46
|
+
*
|
|
47
|
+
* Exit `-1073741502` with no stderr is the Windows sandbox terminating the
|
|
48
|
+
* child. Without this the caller sees an empty error and reasonably concludes
|
|
49
|
+
* the model failed, which sends them looking in the wrong place entirely.
|
|
50
|
+
*/
|
|
51
|
+
export const WINDOWS_SANDBOX_KILL_CODE = -1073741502;
|
|
52
|
+
/**
|
|
53
|
+
* The same status as an unsigned 32-bit value (`0xC0000142`).
|
|
54
|
+
*
|
|
55
|
+
* Node normally surfaces a Windows exit status as a signed integer, but the
|
|
56
|
+
* value travels through enough layers — shells, wrappers, JSON round-trips —
|
|
57
|
+
* that the unsigned form does turn up. Matching only one representation would
|
|
58
|
+
* mean the explanation silently never fires on exactly the platform it exists
|
|
59
|
+
* for, which is the failure mode this whole phase is about.
|
|
60
|
+
*/
|
|
61
|
+
export const WINDOWS_SANDBOX_KILL_CODE_UNSIGNED = WINDOWS_SANDBOX_KILL_CODE >>> 0;
|
|
62
|
+
function isWindowsSandboxKill(code) {
|
|
63
|
+
return code === WINDOWS_SANDBOX_KILL_CODE || code === WINDOWS_SANDBOX_KILL_CODE_UNSIGNED;
|
|
64
|
+
}
|
|
65
|
+
export function explainExit(code, stderr, bypassedSandbox) {
|
|
66
|
+
const detail = stderr.trim().slice(0, 200);
|
|
67
|
+
if (isWindowsSandboxKill(code) && !detail) {
|
|
68
|
+
return bypassedSandbox
|
|
69
|
+
? `codex exec was terminated (exit ${code}) even with the sandbox bypass enabled`
|
|
70
|
+
: `codex exec was terminated by the Windows sandbox (exit ${code}, no stderr). `
|
|
71
|
+
+ 'Retry with the sandbox bypass only if you accept that it disables both approvals and the sandbox.';
|
|
72
|
+
}
|
|
73
|
+
return `codex exec failed (code ${code})${detail ? ': ' + detail : ''}`;
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=vision-provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vision-provider.js","sourceRoot":"","sources":["../../../src/browser/vision-provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,oEAAoE;AACpE,MAAM,CAAC,MAAM,+BAA+B,GAAG,4CAA4C,CAAC;AAwB5F;;;;;;GAMG;AACH,MAAM,UAAU,qBAAqB,CAAC,IAA6B;IAC/D,MAAM,eAAe,GAAG,IAAI,CAAC,aAAa,KAAK,IAAI,CAAC;IACpD,MAAM,IAAI,GAAG;QACT,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,cAAc,EAAE,QAAQ;QAC3C,iDAAiD;QACjD,aAAa;QACb,IAAI,EAAE,+BAA+B;QACrC,uBAAuB;KAC1B,CAAC;IACF,IAAI,eAAe;QAAE,IAAI,CAAC,IAAI,CAAC,4CAA4C,CAAC,CAAC;IAC7E,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACvB,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,eAAe,EAAE,CAAC;AACvD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,UAAU,CAAC;AAErD;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,kCAAkC,GAAG,yBAAyB,KAAK,CAAC,CAAC;AAElF,SAAS,oBAAoB,CAAC,IAAmB;IAC7C,OAAO,IAAI,KAAK,yBAAyB,IAAI,IAAI,KAAK,kCAAkC,CAAC;AAC7F,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,IAAmB,EAAE,MAAc,EAAE,eAAwB;IACrF,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IAC3C,IAAI,oBAAoB,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;QACxC,OAAO,eAAe;YAClB,CAAC,CAAC,mCAAmC,IAAI,wCAAwC;YACjF,CAAC,CAAC,0DAA0D,IAAI,gBAAgB;kBAC5E,mGAAmG,CAAC;IAChH,CAAC;IACD,OAAO,2BAA2B,IAAI,IAAI,MAAM,CAAC,CAAC,CAAC,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;AAC5E,CAAC"}
|