@wenathlan/extension 1.1.31 → 1.1.33

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 CHANGED
@@ -2,28 +2,32 @@
2
2
 
3
3
  Devthink is a **consent-first browser-agent bridge** distributed as a TypeScript library and a Chromium Manifest V3 extension. It turns a user-provided browser objective into a bounded, reviewable plan. The user must start the active-tab session and approve the plan before any page action reaches the browser.
4
4
 
5
- Version: **1.1.31**. License: **GPL-3.0-only**. The repository is `wenathlan/extension`; the npm-compatible scoped package identifier is `@wenathlan/extension`.
5
+ Version: **1.1.33**. License: **GPL-3.0-only**. The repository is `wenathlan/extension`; the npm-compatible scoped package identifier is `@wenathlan/extension`.
6
6
 
7
7
  ## What it does
8
8
 
9
- | Capability | Behavior in 1.1.31 |
9
+ | Capability | Behavior in 1.1.33 |
10
10
  | --- | --- |
11
- | Active-tab session | The user starts a short-lived session for one HTTPS tab and one origin. |
12
- | Page observation | The extension captures a bounded semantic inventory of interactive elements and form labels. |
13
- | Plan proposal | A local plan can be created immediately; an optional user-configured HTTPS endpoint can return a typed plan proposal. |
11
+ | Active-tab session | The user starts a short-lived session for one HTTPS tab and one origin; the session records its origin grants. |
12
+ | Page observation | The extension captures the complete semantic inventory: every interactive element, every form control, every select option and the full page text. |
13
+ | Plan proposal | A local plan can be created immediately; an optional user-configured HTTPS endpoint can return a typed plan proposal of any length. |
14
14
  | Review gate | Every remote proposal starts in `pending`; it cannot reach the page bridge before explicit approval. |
15
- | Browser tools | Reviewed plans may observe, inspect, focus, click, type approved text, scroll, select an existing option, hover, extract bounded text or links, wait a bounded pause or navigate within the same approved origin. |
15
+ | Browser tools | Reviewed plans cover ninety action kinds: pointer paths and coordinate clicks, text, aria, name, xpath, index and point target resolution, timed typing, key holds, sliders, dates, colors, shadow dom piercing, iframe entry, dialog answering, retry rules, a complete read vocabulary with clickable maps and verification reads, page mutation under review, and browser-level tab, window, zoom, snapshot and download commands. |
16
+ | Target resolution | Steps address elements through reviewed `targetref` modes: css selector, visible text, aria role and name, accessible name, xpath, clickable map index or viewport point; ambiguous matches are refused with candidate lists and every resolution returns a matched element summary for review. |
17
+ | Optional capabilities | `tabs`, `downloads`, `clipboardRead` and `clipboardWrite` are optional permissions; browser kinds check their capability, the review panel can request a grant and every grant is audited. |
18
+ | Unlimited by choice | Wait durations, plan size, plan expiry, audit retention and outcome retention carry no code ceilings; every bound is a user choice. |
19
+ | Structured results | Step results carry structured JSON details, rendered in the side panel beside each reviewed step. |
16
20
  | Session pause | The user can pause and resume the active session; a paused session blocks every execution and preview while keeping the reviewed plan alive. |
17
- | Plan progress | Each reviewed step is tracked, completed steps are marked in the side panel and the approved plan closes automatically once every step has executed. |
21
+ | Plan progress | Each reviewed step is tracked with its outcome history, completed steps are marked, plans close automatically once every step has executed, and prior history survives plan replacement. |
18
22
  | Stop and audit | The user can stop the session at any time. Configuration, proposals, decisions and results remain in local extension storage. |
19
23
 
20
24
  The project is deliberately **not** a hosted control platform. It does not depend on a provider-specific sandbox, server URL or browser profile. A configured endpoint is optional, has to use HTTPS and receives only the session record and bounded observation required to produce a plan.
21
25
 
22
26
  ## Security boundary
23
27
 
24
- > Devthink never requests broad host access at installation. The user can grant a single HTTPS origin only after entering it in the extension popup.
28
+ > Devthink never requests broad host access at installation. The user can grant a single HTTPS origin only after entering it in the extension popup, and privileged browser capabilities only as optional permissions requested from the review panel.
25
29
 
26
- The default manifest requests `activeTab`, `storage`, `scripting` and `sidePanel`. It does not request debugger, cookies, history, downloads, proxy control, native messaging, web requests, full-time host permissions or credentials. A service worker cannot be relied on for unattended 24-hour agent computation; any such system must be designed, installed and governed separately. [1] [2]
30
+ The default manifest requests `activeTab`, `storage`, `scripting` and `sidePanel`, plus optional permissions for `tabs`, `downloads`, `clipboardRead` and `clipboardWrite` that stay dormant until the user grants them at runtime. It does not request debugger, cookies, history, proxy control, native messaging, web requests, full-time host permissions or credentials. A service worker cannot be relied on for unattended 24-hour agent computation; any such system must be designed, installed and governed separately. [1] [2]
27
31
 
28
32
  Browser actions remain blocked if the session is missing, stopped or expired; the tab or origin changed; the plan was not approved; the plan expired; or the tool proposal is not in the local allowlist. The page bridge checks the origin again immediately before it acts.
29
33
 
@@ -42,14 +46,15 @@ The endpoint field intentionally has no default URL. Enter a URL such as `https:
42
46
 
43
47
  ```json
44
48
  {
45
- "version": "1.1.31",
49
+ "version": "1.1.33",
46
50
  "objective": "User supplied objective",
47
51
  "session": { "id": "uuid", "tabid": 1, "origin": "https://example.com" },
48
- "observation": { "url": "https://example.com/path", "interactive": [] }
52
+ "observation": { "schemaversion": 2, "url": "https://example.com/path", "interactive": [] },
53
+ "capabilities": { "tabs": false, "downloads": false, "clipboardread": false, "clipboardwrite": false }
49
54
  }
50
55
  ```
51
56
 
52
- It must return a plan proposal using the same version. Every step needs a human-readable summary. Navigation is restricted to the active session origin, and any unknown or malformed proposal is rejected locally. The endpoint is a planner, not an authority to control the browser.
57
+ It must return a plan proposal using the same version. Every step needs a human-readable summary and may carry a reviewed JSON `options` field. Navigation is restricted to the active session origin, and any unknown or malformed proposal is rejected locally. The endpoint is a planner, not an authority to control the browser.
53
58
 
54
59
  ## Library use
55
60
 
package/dist/cli.js CHANGED
@@ -2,19 +2,40 @@
2
2
 
3
3
  // cli/devthink.ts
4
4
  import { readFile } from "node:fs/promises";
5
+ import { createHash } from "node:crypto";
5
6
  import { resolve } from "node:path";
6
- var forbidden = /* @__PURE__ */ new Set(["debugger", "cookies", "webRequest", "history", "bookmarks", "downloads", "nativeMessaging", "proxy", "management"]);
7
+ var forbidden = /* @__PURE__ */ new Set(["debugger", "cookies", "webRequest", "history", "bookmarks", "nativeMessaging", "proxy", "management"]);
8
+ var allowedoptional = /* @__PURE__ */ new Set(["tabs", "downloads", "clipboardRead", "clipboardWrite"]);
9
+ var pinnedidentitykey = "58e21373cec5ade3e6698da5b36ecc2bf7e7e2f755c6fb5e3a6605b535b93985";
10
+ function decodeidentitykey(key) {
11
+ if (!/^[A-Za-z0-9+/]+={0,2}$/.test(key)) throw new Error("The manifest key must be strict base64 without whitespace.");
12
+ if (key.length % 4 !== 0) throw new Error(`The manifest key length ${key.length} is not a multiple of four, so it is not valid base64.`);
13
+ const der = Buffer.from(key, "base64");
14
+ if (der.length < 150 || der.length > 600) throw new Error(`The manifest key decodes to ${der.length} bytes, outside the RSA subject public key range.`);
15
+ if (!der.subarray(0, 2).equals(Buffer.from([48, 130]))) throw new Error("The manifest key does not decode to a DER subject public key sequence.");
16
+ const declared = der.readUInt16BE(2);
17
+ if (declared !== der.length - 4) throw new Error(`The manifest key DER length ${declared} does not match the decoded ${der.length - 4} bytes.`);
18
+ return der;
19
+ }
7
20
  async function main() {
8
21
  const command = process.argv[2] ?? "help";
9
22
  if (command === "manifest") {
10
23
  const manifest = JSON.parse(await readFile(resolve("extension/manifest.json"), "utf8"));
11
24
  const packagejson = JSON.parse(await readFile(resolve("package.json"), "utf8"));
12
25
  const permissions = manifest.permissions ?? [];
26
+ const optional = manifest.optional_permissions ?? [];
13
27
  const denied = permissions.filter((permission) => forbidden.has(permission));
28
+ const deniedoptional = optional.filter((permission) => !allowedoptional.has(permission));
14
29
  if (manifest.version !== packagejson.version) throw new Error("Manifest version must match package.json.");
30
+ if (!manifest.key) throw new Error("A stable manifest key is required for the extension identity.");
31
+ const identitykey = decodeidentitykey(manifest.key);
32
+ if (identitykey.length !== 294) throw new Error(`The manifest key decodes to ${identitykey.length} bytes; the published Devthink identity key decodes to 294.`);
33
+ const digest = createHash("sha256").update(identitykey).digest("hex");
34
+ if (digest !== pinnedidentitykey) throw new Error(`The manifest identity key digest ${digest} does not match the pinned published key; changing the extension identity must be a reviewed decision.`);
15
35
  if ((manifest.host_permissions ?? []).length) throw new Error("Mandatory host permissions are not allowed.");
16
36
  if (denied.length) throw new Error(`Forbidden permissions: ${denied.join(", ")}`);
17
- console.log(JSON.stringify({ valid: true, version: manifest.version, permissions }, null, 2));
37
+ if (deniedoptional.length) throw new Error(`Forbidden optional permissions: ${deniedoptional.join(", ")}`);
38
+ console.log(JSON.stringify({ valid: true, version: manifest.version, identitykeybytes: identitykey.length, permissions, optionalpermissions: optional }, null, 2));
18
39
  return;
19
40
  }
20
41
  console.log("Usage: devthink manifest");
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export * from "./memory.js";
2
- export { actionrisk as deriveactionrisk, canexecute, hostpattern, normalizeendpoint, validatestep } from "./policy.js";
2
+ export { actionrisk as deriveactionrisk, canexecute, hostpattern, normalizeendpoint, resolutionverdict, validatestep, validatetargetref } from "./policy.js";
3
3
  export * from "./protocol.js";
4
4
  export * from "./types.js";
5
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,UAAU,IAAI,gBAAgB,EAAE,UAAU,EAAE,WAAW,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACvH,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,UAAU,IAAI,gBAAgB,EAAE,UAAU,EAAE,WAAW,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAC7J,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -34,12 +34,99 @@ var sessionmemory = class {
34
34
  async setprogress(value) {
35
35
  return this.adapter.set("progress", value);
36
36
  }
37
+ async getcapabilities() {
38
+ return this.adapter.get("capabilities");
39
+ }
40
+ async setcapabilities(value) {
41
+ return this.adapter.set("capabilities", value);
42
+ }
43
+ async getsettings() {
44
+ return this.adapter.get("settings");
45
+ }
46
+ async setsettings(value) {
47
+ return this.adapter.set("settings", value);
48
+ }
37
49
  async getaudit() {
38
50
  return await this.adapter.get("audit") ?? [];
39
51
  }
52
+ async getoutcomes() {
53
+ return await this.adapter.get("outcomes") ?? [];
54
+ }
55
+ /** Records one audit event; retention is a user setting and an absent setting keeps every event. */
40
56
  async addaudi(event) {
41
57
  const records = await this.getaudit();
42
- await this.adapter.set("audit", [event, ...records].slice(0, 100));
58
+ const combined = [event, ...records];
59
+ const retention = (await this.getsettings())?.auditretention;
60
+ await this.adapter.set("audit", retention === void 0 ? combined : combined.slice(0, retention));
61
+ }
62
+ /** Records one step outcome; retention is a user setting and an absent setting keeps every outcome. */
63
+ async addoutcome(outcome) {
64
+ const records = await this.getoutcomes();
65
+ const combined = [outcome, ...records];
66
+ const retention = (await this.getsettings())?.outcomeretention;
67
+ await this.adapter.set("outcomes", retention === void 0 ? combined : combined.slice(0, retention));
68
+ }
69
+ /** Stores one clickable map under its observation version so every captured map stays available. */
70
+ async setmap(map) {
71
+ return this.adapter.set(`map${map.version}`, map);
72
+ }
73
+ /** Returns one stored clickable map by its observation version. */
74
+ async getmap(version) {
75
+ return this.adapter.get(`map${version}`);
76
+ }
77
+ /** Advances and persists the observation version counter used to stamp clickable maps. */
78
+ async nextobservationversion() {
79
+ const current = await this.adapter.get("observationversion") ?? 0;
80
+ const next = current + 1;
81
+ await this.adapter.set("observationversion", next);
82
+ return next;
83
+ }
84
+ /** Returns the latest observation version used to stamp a clickable map. */
85
+ async getobservationversion() {
86
+ return this.adapter.get("observationversion");
87
+ }
88
+ /** Returns the key hold registry, persisted so holds survive service worker restarts. */
89
+ async getholds() {
90
+ return await this.adapter.get("holds") ?? [];
91
+ }
92
+ /** Replaces the key hold registry after one press or release transition. */
93
+ async setholds(holds) {
94
+ return this.adapter.set("holds", holds);
95
+ }
96
+ /** Returns every dialog decision recorded for the audit trail. */
97
+ async getdialogs() {
98
+ return await this.adapter.get("dialogs") ?? [];
99
+ }
100
+ /** Records one dialog decision with the reviewed answer and the observed dialog text. */
101
+ async adddialog(decision) {
102
+ const records = await this.getdialogs();
103
+ await this.adapter.set("dialogs", [decision, ...records]);
104
+ }
105
+ /** Returns every retry outcome recorded with attempts and movement deltas. */
106
+ async getretries() {
107
+ return await this.adapter.get("retries") ?? [];
108
+ }
109
+ /** Records one retry outcome with the attempts made and the movement delta observed. */
110
+ async addretry(outcome) {
111
+ const records = await this.getretries();
112
+ await this.adapter.set("retries", [outcome, ...records]);
113
+ }
114
+ /** Returns every resolution summary stored per target mode. */
115
+ async getresolutions() {
116
+ return await this.adapter.get("resolutions") ?? [];
117
+ }
118
+ /** Records one resolution summary for later selector derivation. */
119
+ async addresolution(summary) {
120
+ const records = await this.getresolutions();
121
+ await this.adapter.set("resolutions", [summary, ...records]);
122
+ }
123
+ /** Returns the reviewed default dialog policy kept for the session auto handler. */
124
+ async getdialogpolicy() {
125
+ return this.adapter.get("dialogpolicy");
126
+ }
127
+ /** Stores the reviewed default dialog policy of the latest approved plan. */
128
+ async setdialogpolicy(policy) {
129
+ return this.adapter.set("dialogpolicy", policy);
43
130
  }
44
131
  };
45
132
  function randomid() {
@@ -47,10 +134,12 @@ function randomid() {
47
134
  }
48
135
 
49
136
  // policy.ts
50
- var sensitiveactions = /* @__PURE__ */ new Set(["click", "type", "navigate", "select"]);
51
- var interactionactions = /* @__PURE__ */ new Set(["focus", "scroll", "hover"]);
52
- var allowedactions = /* @__PURE__ */ new Set(["observe", "inspect", "focus", "click", "type", "navigate", "scroll", "select", "hover", "extract", "wait"]);
53
- var targetactions = /* @__PURE__ */ new Set(["inspect", "focus", "click", "type", "scroll", "select", "hover"]);
137
+ var sensitiveactions = /* @__PURE__ */ new Set(["click", "type", "navigate", "select", "presskey", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "reload", "back", "forward", "writestorage", "setattribute", "removeattribute", "evaluate", "tabcreate", "tabactivate", "tabclose", "tabreload", "windowcreate", "windowclose", "windowresize", "downloadfile", "clickpoint", "shiftclick", "dismissdialog", "enterframe", "typetime", "appendtext", "setvalue", "typeedit", "keyhold", "keyrelease", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor"]);
138
+ var interactionactions = /* @__PURE__ */ new Set(["focus", "scroll", "hover", "clickdeep", "rightclick", "doubleclick", "scrollpage", "scrollby", "scrollend", "scrolltop", "fullscreen", "zoomset", "movepointer", "clicktext", "clickaria", "clickname", "expanddetails", "pierceshadow", "retryaction"]);
139
+ var readactions = /* @__PURE__ */ new Set(["observe", "inspect", "extract", "wait", "waitfor", "waittext", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "readlinks", "readimages", "readmeta", "readforms", "readstorage", "highlight", "tablist", "windowlist", "tabsnapshot", "mapclicks", "verifyvisible", "verifyenabled", "resolvexpath"]);
140
+ var allowedactions = /* @__PURE__ */ new Set([...sensitiveactions, ...interactionactions, ...readactions]);
141
+ var targetactions = /* @__PURE__ */ new Set(["inspect", "focus", "click", "type", "scroll", "select", "hover", "clickdeep", "rightclick", "doubleclick", "drag", "drop", "upload", "clear", "check", "uncheck", "toggle", "submit", "readattribute", "readstyle", "readgeometry", "readvalue", "readtext", "readhtml", "countelements", "readtable", "highlight", "setattribute", "removeattribute", "waitfor", "shiftclick", "typetime", "appendtext", "setvalue", "typeedit", "submitsearch", "selectmulti", "chooseradio", "setslider", "setdate", "setcolor", "expanddetails", "verifyvisible", "verifyenabled", "pierceshadow"]);
142
+ var valueactions = /* @__PURE__ */ new Set(["presskey", "drag", "drop", "upload", "readattribute", "removeattribute", "waittext", "evaluate", "zoomset", "tabactivate", "tabclose", "tabreload", "windowclose", "windowresize", "tabcreate", "windowcreate", "downloadfile", "typetime", "appendtext", "setvalue", "typeedit", "keyhold", "keyrelease", "chooseradio", "setslider", "setdate", "setcolor"]);
54
143
  function normalizeendpoint(value) {
55
144
  const endpoint = new URL(value.trim());
56
145
  if (endpoint.protocol !== "https:") throw new Error("Devthink accepts HTTPS endpoints only.");
@@ -67,16 +156,110 @@ function actionrisk(kind) {
67
156
  if (sensitiveactions.has(kind)) return "sensitive";
68
157
  return interactionactions.has(kind) ? "interaction" : "read";
69
158
  }
159
+ function parseoptions(step) {
160
+ if (step.options === void 0) return {};
161
+ let parsed;
162
+ try {
163
+ parsed = JSON.parse(step.options);
164
+ } catch {
165
+ throw new Error("Step options must be a JSON object.");
166
+ }
167
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("Step options must be a JSON object.");
168
+ return parsed;
169
+ }
70
170
  function waitduration(step) {
71
171
  const requested = step.value ? Number.parseInt(step.value, 10) : 250;
72
172
  if (!Number.isFinite(requested) || requested < 0) throw new Error("Wait duration must be zero or a positive number of milliseconds.");
73
- return Math.min(requested, 1e4);
173
+ return requested;
174
+ }
175
+ function isnumericid(value) {
176
+ return typeof value === "string" && /^\d+$/.test(value);
177
+ }
178
+ function numericoption(options, key) {
179
+ return options[key] === void 0 || typeof options[key] === "number" && Number.isFinite(options[key]);
180
+ }
181
+ function nonnegativeoption(options, key) {
182
+ return numericoption(options, key) && !(typeof options[key] === "number" && options[key] < 0);
183
+ }
184
+ function isnonempty(value) {
185
+ return typeof value === "string" && value.trim().length > 0;
186
+ }
187
+ function ispoint(value) {
188
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
189
+ const point = value;
190
+ return typeof point.x === "number" && Number.isFinite(point.x) && typeof point.y === "number" && Number.isFinite(point.y);
191
+ }
192
+ function resolutionverdict(count) {
193
+ if (!Number.isFinite(count) || count <= 0) return "absent";
194
+ return count === 1 ? "resolved" : "ambiguous";
195
+ }
196
+ function validatetargetref(reference) {
197
+ if (!reference || typeof reference !== "object" || Array.isArray(reference)) return { allowed: false, reason: "The reviewed target reference must be an object." };
198
+ const ref = reference;
199
+ if (ref.mode === "selector") return isnonempty(ref.selector) ? { allowed: true } : { allowed: false, reason: "The selector target reference needs a non-empty selector." };
200
+ if (ref.mode === "text") return isnonempty(ref.text) ? { allowed: true } : { allowed: false, reason: "The text target reference needs non-empty text." };
201
+ if (ref.mode === "aria") {
202
+ if (!isnonempty(ref.role)) return { allowed: false, reason: "The aria target reference needs a non-empty role." };
203
+ return isnonempty(ref.name) ? { allowed: true } : { allowed: false, reason: "The aria target reference needs a non-empty name." };
204
+ }
205
+ if (ref.mode === "name") return isnonempty(ref.name) ? { allowed: true } : { allowed: false, reason: "The name target reference needs a non-empty name." };
206
+ if (ref.mode === "xpath") return isnonempty(ref.xpath) ? { allowed: true } : { allowed: false, reason: "The xpath target reference needs a non-empty expression." };
207
+ if (ref.mode === "index") {
208
+ const index = ref.index;
209
+ return typeof index === "number" && Number.isInteger(index) && index >= 1 ? { allowed: true } : { allowed: false, reason: "The index target reference needs a positive integer map number." };
210
+ }
211
+ if (ref.mode === "point") {
212
+ const pointok = typeof ref.x === "number" && Number.isFinite(ref.x) && typeof ref.y === "number" && Number.isFinite(ref.y);
213
+ return pointok ? { allowed: true } : { allowed: false, reason: "The point target reference needs numeric x and y coordinates." };
214
+ }
215
+ return { allowed: false, reason: "The target reference mode must be selector, text, aria, name, xpath, index or point." };
216
+ }
217
+ function origingranted(session, origin) {
218
+ if (!session) return false;
219
+ const grants = session.grants ?? [session.origin];
220
+ return grants.includes(origin);
221
+ }
222
+ function validateinnerstep(options, origin) {
223
+ const stepid = options.stepid;
224
+ const kind = options.kind;
225
+ if (isnonempty(stepid)) {
226
+ if (kind !== void 0) return { allowed: false, reason: "The reviewed wrapper must reference a step id or an inline step, not both." };
227
+ return { allowed: true };
228
+ }
229
+ if (typeof kind !== "string" || !kind.trim()) return { allowed: false, reason: "A reviewed step id or inline step kind is required in options." };
230
+ if (kind === "retryaction" || kind === "enterframe") return { allowed: false, reason: "The reviewed inner step cannot be another wrapper kind." };
231
+ if (!allowedactions.has(kind)) return { allowed: false, reason: "The reviewed inner step kind is unsupported." };
232
+ const inneroptions = options.options;
233
+ if (inneroptions !== void 0 && (!inneroptions || typeof inneroptions !== "object" || Array.isArray(inneroptions))) return { allowed: false, reason: "The reviewed inner step options must be an object." };
234
+ const inner = {
235
+ id: "inner",
236
+ kind,
237
+ summary: "Reviewed inner step.",
238
+ risk: actionrisk(kind),
239
+ ...isnonempty(options.target) ? { target: options.target } : {},
240
+ ...isnonempty(options.value) ? { value: options.value } : {},
241
+ ...inneroptions !== void 0 ? { options: JSON.stringify(inneroptions) } : {}
242
+ };
243
+ return validatestep(inner, origin);
74
244
  }
75
245
  function validatestep(step, origin) {
76
246
  if (!allowedactions.has(step.kind)) return { allowed: false, reason: "Unsupported action kind." };
77
247
  if (!step.summary.trim()) return { allowed: false, reason: "A human-readable action summary is required." };
78
- if (targetactions.has(step.kind) && !step.target?.trim()) return { allowed: false, reason: "A page target is required." };
248
+ let options;
249
+ try {
250
+ options = parseoptions(step);
251
+ } catch {
252
+ return { allowed: false, reason: "Step options must be a JSON object." };
253
+ }
254
+ const hastargetref = options.targetref !== void 0;
255
+ if (targetactions.has(step.kind) && !step.target?.trim() && !hastargetref) return { allowed: false, reason: "A page target is required." };
256
+ if (valueactions.has(step.kind) && !step.value?.trim()) return { allowed: false, reason: "A reviewed value is required." };
79
257
  if (step.kind === "select" && !step.value?.trim()) return { allowed: false, reason: "A reviewed option value is required." };
258
+ if (step.kind === "navigate" && !step.value) return { allowed: false, reason: "A navigation URL is required." };
259
+ if (hastargetref) {
260
+ const reference = validatetargetref(options.targetref);
261
+ if (!reference.allowed) return reference;
262
+ }
80
263
  if (step.kind === "wait") {
81
264
  try {
82
265
  waitduration(step);
@@ -85,13 +268,99 @@ function validatestep(step, origin) {
85
268
  }
86
269
  }
87
270
  if (step.kind === "navigate") {
88
- if (!step.value) return { allowed: false, reason: "A navigation URL is required." };
89
271
  try {
90
- if (new URL(step.value).origin !== origin) return { allowed: false, reason: "Navigation must remain within the approved origin." };
272
+ if (new URL(step.value ?? "").origin !== origin) return { allowed: false, reason: "Navigation must remain within the approved origin." };
91
273
  } catch {
92
274
  return { allowed: false, reason: "Navigation URL is invalid." };
93
275
  }
94
276
  }
277
+ if (step.kind === "tabcreate" || step.kind === "windowcreate" || step.kind === "downloadfile") {
278
+ try {
279
+ const url = new URL(step.value ?? "");
280
+ if (url.protocol !== "https:") return { allowed: false, reason: "The reviewed URL must use HTTPS." };
281
+ } catch {
282
+ return { allowed: false, reason: "The reviewed URL is invalid." };
283
+ }
284
+ }
285
+ if (step.kind === "tabactivate" || step.kind === "tabclose" || step.kind === "tabreload" || step.kind === "windowclose" || step.kind === "windowresize") {
286
+ if (!isnumericid(step.value)) return { allowed: false, reason: "A numeric browser id is required." };
287
+ }
288
+ if (step.kind === "zoomset") {
289
+ const zoom = Number(step.value);
290
+ if (!Number.isFinite(zoom) || zoom <= 0) return { allowed: false, reason: "The reviewed zoom must be a positive number." };
291
+ }
292
+ if (step.kind === "setattribute" || step.kind === "writestorage") {
293
+ const keyname = step.kind === "setattribute" ? "name" : "key";
294
+ if (typeof options[keyname] !== "string" || !options[keyname].trim()) return { allowed: false, reason: `A reviewed ${keyname} is required in options.` };
295
+ if (typeof options.value !== "string") return { allowed: false, reason: "A reviewed value is required in options." };
296
+ }
297
+ if (step.kind === "windowresize") {
298
+ if (typeof options.width !== "number" || typeof options.height !== "number" || !Number.isFinite(options.width) || !Number.isFinite(options.height)) return { allowed: false, reason: "Reviewed width and height numbers are required in options." };
299
+ }
300
+ if ((step.kind === "scrollpage" || step.kind === "scrollby") && (!numericoption(options, "x") || !numericoption(options, "y"))) return { allowed: false, reason: "Scroll amounts must be numbers in options." };
301
+ if (step.kind === "waitfor" && options.timeout !== void 0 && (typeof options.timeout !== "number" || options.timeout < 0)) return { allowed: false, reason: "The waitfor timeout must be zero or a positive number of milliseconds." };
302
+ if (step.kind === "movepointer") {
303
+ const path = options.pointpath;
304
+ if (!path || typeof path !== "object" || Array.isArray(path)) return { allowed: false, reason: "A reviewed pointpath with start and end points is required in options." };
305
+ const points = path;
306
+ if (!ispoint(points.start) || !ispoint(points.end)) return { allowed: false, reason: "The reviewed pointpath needs numeric start and end points." };
307
+ if (points.waypoints !== void 0 && (!Array.isArray(points.waypoints) || !points.waypoints.every((waypoint) => ispoint(waypoint)))) return { allowed: false, reason: "The reviewed pointpath waypoints must be numeric points." };
308
+ if (!nonnegativeoption(points, "duration")) return { allowed: false, reason: "The reviewed pointpath duration must be zero or a positive number of milliseconds." };
309
+ const speed = options.speedprofile;
310
+ if (speed !== void 0) {
311
+ if (!speed || typeof speed !== "object" || Array.isArray(speed)) return { allowed: false, reason: "The reviewed speed profile must be an object." };
312
+ const profile = speed;
313
+ if (profile.easing !== void 0 && profile.easing !== "linear" && profile.easing !== "easeinout") return { allowed: false, reason: "The reviewed easing must be linear or easeinout." };
314
+ if (!nonnegativeoption(profile, "peak")) return { allowed: false, reason: "The reviewed peak velocity must be zero or a positive number." };
315
+ if (!nonnegativeoption(profile, "jitter")) return { allowed: false, reason: "The reviewed jitter window must be zero or a positive number of milliseconds." };
316
+ }
317
+ }
318
+ if (step.kind === "clickpoint" && (!hastargetref || options.targetref.mode !== "point")) return { allowed: false, reason: "A reviewed point target reference is required in options." };
319
+ if (step.kind === "clicktext" && (!hastargetref || options.targetref.mode !== "text")) return { allowed: false, reason: "A reviewed text target reference is required in options." };
320
+ if (step.kind === "clickaria" && (!hastargetref || options.targetref.mode !== "aria")) return { allowed: false, reason: "A reviewed aria target reference is required in options." };
321
+ if (step.kind === "clickname" && (!hastargetref || options.targetref.mode !== "name")) return { allowed: false, reason: "A reviewed name target reference is required in options." };
322
+ if (step.kind === "resolvexpath" && (!hastargetref || options.targetref.mode !== "xpath")) return { allowed: false, reason: "A reviewed xpath target reference is required in options." };
323
+ if (step.kind === "typetime" && options.delay !== void 0 && (typeof options.delay !== "number" || !Number.isFinite(options.delay) || options.delay < 0)) return { allowed: false, reason: "The reviewed per keystroke delay must be zero or a positive number of milliseconds." };
324
+ if (step.kind === "submitsearch") {
325
+ if (!isnonempty(options.results)) return { allowed: false, reason: "A reviewed results region selector is required in options." };
326
+ if (options.timeout !== void 0 && (typeof options.timeout !== "number" || !Number.isFinite(options.timeout) || options.timeout < 0)) return { allowed: false, reason: "The submitsearch timeout must be zero or a positive number of milliseconds." };
327
+ }
328
+ if (step.kind === "selectmulti") {
329
+ const values = options.values;
330
+ if (!Array.isArray(values) || values.length === 0 || !values.every((value) => isnonempty(value))) return { allowed: false, reason: "A reviewed list of option values is required in options." };
331
+ }
332
+ if (step.kind === "setslider") {
333
+ const slider = Number(step.value);
334
+ if (!Number.isFinite(slider)) return { allowed: false, reason: "The reviewed slider value must be a number." };
335
+ }
336
+ if (step.kind === "setdate" && !/^\d{4}-\d{2}-\d{2}$/.test(step.value ?? "")) return { allowed: false, reason: "The reviewed date must use the yyyy-mm-dd form." };
337
+ if (step.kind === "setcolor" && !/^#[0-9a-fA-F]{6}$/.test(step.value ?? "")) return { allowed: false, reason: "The reviewed color must use the #rrggbb form." };
338
+ if (step.kind === "keyhold" && options.holdid !== void 0 && !isnonempty(options.holdid)) return { allowed: false, reason: "The reviewed hold id must be a non-empty string." };
339
+ if (step.kind === "dismissdialog") {
340
+ const accept = options.accept;
341
+ const answer = options.answer;
342
+ if (accept === void 0 && !isnonempty(answer)) return { allowed: false, reason: "A reviewed accept flag or prompt answer is required in options." };
343
+ if (accept !== void 0 && typeof accept !== "boolean") return { allowed: false, reason: "The reviewed dialog accept flag must be a boolean." };
344
+ if (answer !== void 0 && !isnonempty(answer)) return { allowed: false, reason: "The reviewed prompt answer must be a non-empty string." };
345
+ }
346
+ if (step.kind === "pierceshadow" && options.shadow !== void 0) {
347
+ if (!Array.isArray(options.shadow) || !options.shadow.every((item) => isnonempty(item))) return { allowed: false, reason: "The reviewed shadow path must be a list of non-empty selectors." };
348
+ }
349
+ if (step.kind === "enterframe") {
350
+ const path = options.framepath;
351
+ if (!Array.isArray(path) || path.length === 0 || !path.every((item) => typeof item === "number" && Number.isInteger(item) && item >= 0)) return { allowed: false, reason: "A reviewed frame path of frame indexes is required in options." };
352
+ return validateinnerstep(options, origin);
353
+ }
354
+ if (step.kind === "retryaction") {
355
+ const inner = validateinnerstep(options, origin);
356
+ if (!inner.allowed) return inner;
357
+ const rule = options.retryrule;
358
+ if (!rule || typeof rule !== "object" || Array.isArray(rule)) return { allowed: false, reason: "A reviewed retry rule with attempts is required in options." };
359
+ const retry = rule;
360
+ if (typeof retry.attempts !== "number" || !Number.isInteger(retry.attempts) || retry.attempts < 1) return { allowed: false, reason: "The reviewed retry attempts must be a positive integer with no code ceiling." };
361
+ if (!nonnegativeoption(retry, "settle")) return { allowed: false, reason: "The reviewed retry settle window must be zero or a positive number of milliseconds." };
362
+ if (!nonnegativeoption(retry, "tolerance")) return { allowed: false, reason: "The reviewed retry movement tolerance must be zero or a positive number of pixels." };
363
+ }
95
364
  return { allowed: true };
96
365
  }
97
366
  function sessiongate(input) {
@@ -107,11 +376,12 @@ function canexecute(input) {
107
376
  if (!gate.allowed) return gate;
108
377
  if (!input.plan || input.plan.state !== "approved") return { allowed: false, reason: "The plan has not received explicit approval." };
109
378
  if (input.plan.expiresat <= now) return { allowed: false, reason: "The approved plan has expired." };
379
+ if ((input.step.kind === "pierceshadow" || input.step.kind === "enterframe") && !origingranted(input.session, input.origin)) return { allowed: false, reason: "The shadow or frame step is outside the session origin grants." };
110
380
  return validatestep(input.step, input.origin);
111
381
  }
112
382
 
113
383
  // version.ts
114
- var packageversion = "1.1.31";
384
+ var packageversion = "1.1.33";
115
385
 
116
386
  // types.ts
117
387
  var protocolversion = packageversion;
@@ -130,7 +400,7 @@ function parseproposal(value, origin) {
130
400
  if (root.version !== protocolversion) throw new Error("Unsupported protocol version.");
131
401
  const planinput = record(root.plan);
132
402
  const stepsinput = planinput.steps;
133
- if (!Array.isArray(stepsinput) || stepsinput.length === 0 || stepsinput.length > 50) throw new Error("A plan needs between one and fifty steps.");
403
+ if (!Array.isArray(stepsinput) || stepsinput.length === 0) throw new Error("A plan needs at least one step.");
134
404
  const steps = stepsinput.map((input, index) => {
135
405
  const candidate = record(input);
136
406
  const kind = text(candidate.kind, `step ${index + 1} kind`);
@@ -140,38 +410,59 @@ function parseproposal(value, origin) {
140
410
  summary: text(candidate.summary, `step ${index + 1} summary`),
141
411
  risk: actionrisk(kind),
142
412
  ...typeof candidate.target === "string" ? { target: candidate.target } : {},
143
- ...typeof candidate.value === "string" ? { value: candidate.value } : {}
413
+ ...typeof candidate.value === "string" ? { value: candidate.value } : {},
414
+ ...typeof candidate.options === "string" ? { options: candidate.options } : {}
144
415
  };
145
416
  const evaluation = validatestep(step, origin);
146
417
  if (!evaluation.allowed) throw new Error(evaluation.reason);
147
418
  return step;
148
419
  });
420
+ for (const step of steps) {
421
+ if (step.kind !== "retryaction" && step.kind !== "enterframe") continue;
422
+ const options = parseoptions(step);
423
+ if (typeof options.stepid === "string" && !steps.some((candidate) => candidate.id === options.stepid)) throw new Error("A retry or frame wrapper references an unknown step id.");
424
+ }
149
425
  const createdat = Date.now();
426
+ const expiresat = typeof planinput.expiresat === "number" ? planinput.expiresat : createdat + 10 * 60 * 1e3;
150
427
  const plan = {
151
428
  id: typeof planinput.id === "string" ? planinput.id : crypto.randomUUID(),
152
429
  objective: text(planinput.objective, "objective"),
153
430
  origin,
154
431
  steps,
155
432
  createdat,
156
- expiresat: Math.min(typeof planinput.expiresat === "number" ? planinput.expiresat : createdat + 10 * 60 * 1e3, createdat + 30 * 60 * 1e3),
433
+ expiresat,
157
434
  state: "pending"
158
435
  };
159
436
  if (plan.expiresat <= createdat) throw new Error("Plan expiry must be in the future.");
160
437
  return { version: protocolversion, plan };
161
438
  }
162
439
  function requestbody(input) {
163
- return JSON.stringify({ version: protocolversion, objective: input.objective, session: input.session, observation: input.observation });
440
+ return JSON.stringify({ version: protocolversion, objective: input.objective, session: input.session, observation: input.observation, capabilities: input.capabilities });
441
+ }
442
+ function outcomeresponse(input) {
443
+ return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, outcome: input.outcome, ...input.resolvedtarget ? { resolvedtarget: input.resolvedtarget } : {} });
444
+ }
445
+ function mapresponse(input) {
446
+ return JSON.stringify({ version: protocolversion, planid: input.plan.id, planstate: input.plan.state, map: input.map });
447
+ }
448
+ function heldkeysreport(input) {
449
+ return { version: protocolversion, tabid: input.tabid, heldkeys: input.holds };
164
450
  }
165
451
  export {
166
452
  canexecute,
167
453
  actionrisk as deriveactionrisk,
454
+ heldkeysreport,
168
455
  hostpattern,
456
+ mapresponse,
169
457
  normalizeendpoint,
458
+ outcomeresponse,
170
459
  parseproposal,
171
460
  protocolversion,
172
461
  randomid,
173
462
  requestbody,
463
+ resolutionverdict,
174
464
  sessionmemory,
175
- validatestep
465
+ validatestep,
466
+ validatetargetref
176
467
  };
177
468
  //# sourceMappingURL=index.js.map