@effect-agent/platform-cloudflare 0.1.0-beta.52 → 0.1.0-beta.53

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.
@@ -182,13 +182,13 @@ declare const encodeUnknownResolutionCommand: (input: UnknownResolutionCommand,
182
182
  readonly author: string;
183
183
  readonly reason: string;
184
184
  readonly resolution: {
185
- readonly _tag: "SafeToRetry";
186
- } | {
187
185
  readonly _tag: "CompletedWithResult";
188
186
  readonly result: Schema.Json;
189
187
  readonly isFailure: boolean;
190
188
  } | {
191
189
  readonly _tag: "NeverHappened";
190
+ } | {
191
+ readonly _tag: "SafeToRetry";
192
192
  } | {
193
193
  readonly _tag: "AbortSubmission";
194
194
  };
@@ -316,19 +316,6 @@ declare const encodeHostResponse: (input: AbortRecorded | ApprovalRecorded | Hos
316
316
  readonly createdAt: string;
317
317
  readonly deploymentId: string;
318
318
  readonly payload: {
319
- readonly _tag: "SubagentStarted";
320
- readonly runId: string;
321
- readonly toolCallId: string;
322
- readonly childThreadId: string;
323
- readonly childSubmissionId: string;
324
- readonly childReceiptId: string;
325
- readonly childRunId: string;
326
- } | {
327
- readonly _tag: "AbortRequested";
328
- readonly submissionId: string;
329
- readonly author: string;
330
- readonly reason: string;
331
- } | {
332
319
  readonly _tag: "ThreadCreated";
333
320
  readonly agentId: string;
334
321
  readonly definitions: {
@@ -466,6 +453,11 @@ declare const encodeHostResponse: (input: AbortRecorded | ApprovalRecorded | Hos
466
453
  readonly runDisposition?: Schema.Json | undefined;
467
454
  readonly finishReason?: "budget-exhausted" | undefined;
468
455
  readonly exhausted?: "tokens" | "tool-calls" | "turns" | undefined;
456
+ } | {
457
+ readonly _tag: "AbortRequested";
458
+ readonly submissionId: string;
459
+ readonly author: string;
460
+ readonly reason: string;
469
461
  } | {
470
462
  readonly _tag: "SubmissionSettled";
471
463
  readonly submissionId: string;
@@ -557,6 +549,14 @@ declare const encodeHostResponse: (input: AbortRecorded | ApprovalRecorded | Hos
557
549
  readonly resultBytes: number;
558
550
  };
559
551
  } | undefined;
552
+ } | {
553
+ readonly _tag: "SubagentStarted";
554
+ readonly runId: string;
555
+ readonly toolCallId: string;
556
+ readonly childThreadId: string;
557
+ readonly childSubmissionId: string;
558
+ readonly childReceiptId: string;
559
+ readonly childRunId: string;
560
560
  } | {
561
561
  readonly _tag: "SubagentJoined";
562
562
  readonly runId: string;
@@ -628,13 +628,13 @@ declare const encodeHostResponse: (input: AbortRecorded | ApprovalRecorded | Hos
628
628
  readonly author: string;
629
629
  readonly reason: string;
630
630
  readonly resolution: {
631
- readonly _tag: "SafeToRetry";
632
- } | {
633
631
  readonly _tag: "CompletedWithResult";
634
632
  readonly result: Schema.Json;
635
633
  readonly isFailure: boolean;
636
634
  } | {
637
635
  readonly _tag: "NeverHappened";
636
+ } | {
637
+ readonly _tag: "SafeToRetry";
638
638
  } | {
639
639
  readonly _tag: "AbortSubmission";
640
640
  };
@@ -644,6 +644,16 @@ declare const encodeHostResponse: (input: AbortRecorded | ApprovalRecorded | Hos
644
644
  } | {
645
645
  readonly _tag: "HostFailed";
646
646
  readonly failure: {
647
+ readonly _tag: "HostProtocolError";
648
+ readonly message: string;
649
+ } | {
650
+ readonly _tag: "AgentInputError";
651
+ readonly message: string;
652
+ } | {
653
+ readonly _tag: "DigestError";
654
+ readonly message: string;
655
+ readonly cause?: Schema.Json | undefined;
656
+ } | {
647
657
  readonly _tag: "AdmissionConflict";
648
658
  readonly threadId: string;
649
659
  readonly principal: string;
@@ -658,6 +668,15 @@ declare const encodeHostResponse: (input: AbortRecorded | ApprovalRecorded | Hos
658
668
  readonly _tag: "SettlementConflict";
659
669
  readonly submissionId: string;
660
670
  readonly existingOutcome: "aborted" | "completed" | "failed";
671
+ } | {
672
+ readonly _tag: "ApprovalConflict";
673
+ readonly submissionId: string;
674
+ readonly toolCallId: string;
675
+ readonly existingDecision: "approved" | "denied";
676
+ } | {
677
+ readonly _tag: "UnknownResolutionConflict";
678
+ readonly submissionId: string;
679
+ readonly toolCallId: string;
661
680
  } | {
662
681
  readonly _tag: "JoinedToHost";
663
682
  readonly submissionId: string;
@@ -687,30 +706,6 @@ declare const encodeHostResponse: (input: AbortRecorded | ApprovalRecorded | Hos
687
706
  readonly threadId: string;
688
707
  readonly actualEpoch: number;
689
708
  readonly attemptedEpoch: number;
690
- } | {
691
- readonly _tag: "DurableAlarmError";
692
- readonly operation: string;
693
- readonly message: string;
694
- readonly cause?: Schema.Json | undefined;
695
- } | {
696
- readonly _tag: "HostProtocolError";
697
- readonly message: string;
698
- } | {
699
- readonly _tag: "AgentInputError";
700
- readonly message: string;
701
- } | {
702
- readonly _tag: "DigestError";
703
- readonly message: string;
704
- readonly cause?: Schema.Json | undefined;
705
- } | {
706
- readonly _tag: "ApprovalConflict";
707
- readonly submissionId: string;
708
- readonly toolCallId: string;
709
- readonly existingDecision: "approved" | "denied";
710
- } | {
711
- readonly _tag: "UnknownResolutionConflict";
712
- readonly submissionId: string;
713
- readonly toolCallId: string;
714
709
  } | {
715
710
  readonly _tag: "DurableRuntimeFailpointError";
716
711
  readonly location: "abort:after-intent" | "approval:after-request-append" | "approval:after-suspend" | "claim:after-claim" | "compaction:after-canonical-append" | "compaction:before-canonical-append" | "input:after-canonical-append" | "join:after-canonical-append" | "join:after-claim" | "policy:after-reservation-append" | "policy:before-reservation-append" | "resolve:after-intent" | "run:after-start-append" | "run:before-start-append" | "step:after-step-append" | "subagent:after-admit" | "subagent:after-child-abort-intent" | "subagent:after-child-ready" | "subagent:after-join-append" | "subagent:after-release" | "subagent:after-release-pending" | "subagent:after-request-append" | "subagent:after-reserve" | "subagent:after-sibling-settle" | "subagent:after-start-append" | "subagent:after-suspend" | "submit:after-admit" | "submit:after-materialize" | "terminalize:after-canonical-append" | "terminalize:after-reserve" | "tools:after-prepared-append" | "tools:before-prepared-append" | "turn:after-canonical-append" | "turn:after-response-append" | "turn:after-results-append";
@@ -719,6 +714,11 @@ declare const encodeHostResponse: (input: AbortRecorded | ApprovalRecorded | Hos
719
714
  readonly limit: "database-bytes" | "input-bytes" | "queue-depth";
720
715
  readonly actual: number;
721
716
  readonly maximum: number;
717
+ } | {
718
+ readonly _tag: "DurableAlarmError";
719
+ readonly operation: string;
720
+ readonly message: string;
721
+ readonly cause?: Schema.Json | undefined;
722
722
  } | {
723
723
  readonly _tag: "OperationDenied";
724
724
  readonly operation: "abort" | "awaitSettlement" | "explain" | "observe" | "resolveApproval" | "resolveUnknown" | "retry" | "scanObligations" | "verify" | "wake";
@@ -1,7 +1,7 @@
1
1
  import { r as BrowserRunSessionLifecycle } from "./browser-session-lifecycle-DJpqLXy_.mjs";
2
2
  import { Context, Crypto, Effect, Layer, Redacted, Schema, Scope } from "effect";
3
3
  import { InteractiveBrowserPolicy } from "@effect-agent/sandbox/InteractiveBrowser";
4
- import { CredentialFieldRole, ProtectedBrowser, ProtectedBrowserControl, ProtectedBrowserError, ProtectedCleanup } from "@effect-agent/sandbox/ProtectedBrowser";
4
+ import { CredentialFieldRole, CredentialOrigin, ProtectedBrowser, ProtectedBrowserControl, ProtectedBrowserError, ProtectedCleanup } from "@effect-agent/sandbox/ProtectedBrowser";
5
5
  //#region src/protected-browser/policy.d.ts
6
6
  declare const ProtectedTransportError_base: Schema.Class<ProtectedTransportError, Schema.TaggedStruct<"ProtectedTransportError", {
7
7
  readonly reason: Schema.Literals<readonly ["stale-reference", "needs-attention", "unsupported", "provider"]>;
@@ -14,12 +14,14 @@ declare const ProtectedBrowserDispatch_base: Context.ServiceClass<ProtectedBrows
14
14
  declare class ProtectedBrowserDispatch extends ProtectedBrowserDispatch_base {}
15
15
  /** Decoded adapter boundary. SDK exceptions and page diagnostics never cross this port. */
16
16
  interface ProtectedBrowserTransport {
17
+ /** Select observation/target origins; undefined restores all network-permitted HTTPS frames. */
18
+ readonly restrictObservation: (origins: ReadonlyArray<typeof CredentialOrigin.Type> | undefined) => Effect.Effect<void, ProtectedTransportError>;
17
19
  readonly context: Effect.Effect<typeof ProtectedPageContext.Type, ProtectedTransportError>;
18
20
  readonly discover: Effect.Effect<typeof ProtectedDiscovery.Type, ProtectedTransportError>;
19
21
  readonly target: (ref: string) => Effect.Effect<ProtectedBrowserControl, ProtectedTransportError>;
20
22
  readonly navigate: (url: string) => Effect.Effect<void, ProtectedTransportError>;
21
23
  readonly click: (ref: string) => Effect.Effect<void, ProtectedTransportError>;
22
- readonly fill: (ref: string, role: typeof CredentialFieldRole.Type, value: Redacted.Redacted<string>) => Effect.Effect<void, ProtectedTransportError, ProtectedBrowserDispatch>;
24
+ readonly fill: (ref: string, role: typeof CredentialFieldRole.Type | "text" | "select", value: Redacted.Redacted<string>) => Effect.Effect<void, ProtectedTransportError, ProtectedBrowserDispatch>;
23
25
  /** Invalidates local references synchronously before bounded exact-session cleanup. */
24
26
  readonly invalidate: () => void;
25
27
  readonly close: Effect.Effect<typeof ProtectedCleanup.Type>;
@@ -44,7 +46,7 @@ declare const ProtectedDiscovery: Schema.Struct<{
44
46
  }>;
45
47
  /**
46
48
  * Fresh private passes only. Account administrators and Browser Rendering token holders are
47
- * trusted operators. No viewer, handoff, raw JavaScript, screenshot or plaintext-fill API exists.
49
+ * trusted operators. No viewer, handoff, raw JavaScript, screenshot or plaintext credential API exists.
48
50
  * Hosts explicitly authorize post-exposure observations for recipients they trust not to echo.
49
51
  */
50
52
  declare const browserRunProtectedLayer: () => Layer.Layer<ProtectedBrowser, never, BrowserRunProtectedTransport | Crypto.Crypto>;
@@ -2,7 +2,7 @@ import { n as BrowserRunSessionLifecycle } from "./browser-session-lifecycle-CUb
2
2
  import { Clock, Context, Crypto, Effect, Layer, Option, Redacted, Schema, Semaphore } from "effect";
3
3
  import puppeteer from "@cloudflare/puppeteer";
4
4
  import { InteractiveBrowserPolicy } from "@effect-agent/sandbox/InteractiveBrowser";
5
- import { BrowserCredentialAccess, CardCredential, CredentialOffer, CredentialOfferMetadata, CredentialOrigin, CredentialTarget, CredentialUseResult, ListCredentialOffers, LoginCredential, ProtectedBrowser, ProtectedBrowserClick, ProtectedBrowserControl, ProtectedBrowserError, ProtectedBrowserNavigate, ProtectedBrowserObservation, UseCredential } from "@effect-agent/sandbox/ProtectedBrowser";
5
+ import { BrowserCredentialAccess, CardCredential, CredentialObservationDecision, CredentialOffer, CredentialOfferMetadata, CredentialOrigin, CredentialTarget, CredentialUseResult, ListCredentialOffers, LoginCredential, ProtectedBrowser, ProtectedBrowserClick, ProtectedBrowserControl, ProtectedBrowserError, ProtectedBrowserFill, ProtectedBrowserNavigate, ProtectedBrowserObservation, UseCredential } from "@effect-agent/sandbox/ProtectedBrowser";
6
6
  //#region src/protected-browser/policy.ts
7
7
  var ProtectedTransportError = class extends Schema.TaggedError()("ProtectedTransportError", { reason: Schema.Literals([
8
8
  "stale-reference",
@@ -42,7 +42,7 @@ const secretFor = (material, role) => {
42
42
  };
43
43
  /**
44
44
  * Fresh private passes only. Account administrators and Browser Rendering token holders are
45
- * trusted operators. No viewer, handoff, raw JavaScript, screenshot or plaintext-fill API exists.
45
+ * trusted operators. No viewer, handoff, raw JavaScript, screenshot or plaintext credential API exists.
46
46
  * Hosts explicitly authorize post-exposure observations for recipients they trust not to echo.
47
47
  */
48
48
  const browserRunProtectedLayer = () => Layer.effect(ProtectedBrowser)(Effect.gen(function* () {
@@ -100,18 +100,48 @@ const browserRunProtectedLayer = () => Layer.effect(ProtectedBrowser)(Effect.gen
100
100
  const pageContext = remote(driver.context);
101
101
  const permitObservation = Effect.gen(function* () {
102
102
  const context = yield* pageContext;
103
+ let origins;
103
104
  if (exposures.length > 0) {
104
105
  observation = "protected";
105
- if ((yield* access.observation({
106
+ const principal = yield* caller;
107
+ const rawDecision = yield* access.observation({
106
108
  ...context,
107
- caller: yield* caller,
109
+ caller: principal,
108
110
  exposures: [...exposures]
109
- }).pipe(Effect.mapError((error) => fail(error.reason)))) !== "trust-recipient-no-credential-echo") return yield* fail("observation-blocked");
111
+ }).pipe(Effect.mapError((error) => fail(error.reason)));
112
+ if (Redacted.value(yield* caller) !== Redacted.value(principal)) return yield* fail("denied");
113
+ const decision = yield* Schema.decodeUnknownEffect(CredentialObservationDecision)(rawDecision).pipe(Effect.mapError(() => fail("observation-blocked")));
114
+ if (decision === "deny") return yield* fail("observation-blocked");
115
+ if (typeof decision !== "string") {
116
+ origins = [...decision.origins];
117
+ if (!origins.includes(context.topOrigin)) return yield* fail("observation-blocked");
118
+ }
110
119
  observation = "approved-after-exposure";
111
120
  }
112
- return context;
121
+ yield* remote(driver.restrictObservation(origins));
122
+ return {
123
+ ...context,
124
+ frameOrigins: context.frameOrigins.filter((origin) => origins === void 0 || origins.includes(origin))
125
+ };
113
126
  });
114
127
  const target = (ref) => remote(driver.target(ref));
128
+ const authorizeAction = Effect.fn("ProtectedBrowser.authorizeAction")(function* (action) {
129
+ if (access.authorizeAction === void 0) {
130
+ if (action._tag === "Submit") return yield* fail("unsupported");
131
+ return;
132
+ }
133
+ const principal = yield* caller;
134
+ yield* access.authorizeAction({
135
+ caller: principal,
136
+ action,
137
+ exposures: [...exposures]
138
+ }).pipe(Effect.mapError((error) => fail(error.reason)));
139
+ if (Redacted.value(yield* caller) !== Redacted.value(principal)) return yield* fail("denied");
140
+ if (action._tag !== "Navigate") {
141
+ const current = yield* target(action.ref);
142
+ if (!sameTarget(current.target, action.target) || current.role !== (action._tag === "Submit" ? "submit" : action.role) || action._tag === "Click" && action.role === "link" && current.url !== action.url) return yield* fail("stale-reference");
143
+ }
144
+ }, Effect.withTracerEnabled(false));
115
145
  const bounded = (result) => {
116
146
  return new TextEncoder().encode(JSON.stringify(result)).byteLength <= policy.maxReturnedBytes ? Effect.succeed(result) : Effect.fail(fail("limit"));
117
147
  };
@@ -155,6 +185,10 @@ const browserRunProtectedLayer = () => Layer.effect(ProtectedBrowser)(Effect.gen
155
185
  }
156
186
  if (url.protocol !== "https:" || url.username || url.password || policy.network._tag === "ExactHosts" && !policy.network.allowedHosts.includes(url.host)) return yield* fail("denied");
157
187
  if (exposures.length > 0) yield* permitObservation;
188
+ yield* authorizeAction({
189
+ _tag: "Navigate",
190
+ url: decoded.url
191
+ });
158
192
  offers.clear();
159
193
  dispatch = "possibly-dispatched";
160
194
  yield* remote(driver.navigate(decoded.url));
@@ -165,7 +199,8 @@ const browserRunProtectedLayer = () => Layer.effect(ProtectedBrowser)(Effect.gen
165
199
  const before = yield* permitObservation;
166
200
  const result = yield* remote(driver.discover);
167
201
  const after = yield* permitObservation;
168
- if (before.document !== after.document || result.document !== after.document) return yield* fail("stale-reference");
202
+ if (before.document !== after.document || result.document !== after.document || result.topOrigin !== after.topOrigin) return yield* fail("stale-reference");
203
+ if (result.frameOrigins.some((origin) => !after.frameOrigins.includes(origin))) return yield* fail("observation-blocked");
169
204
  return yield* bounded(ProtectedBrowserObservation.make({
170
205
  ...result,
171
206
  observation: exposures.length > 0 ? "approved-after-exposure" : "before-exposure"
@@ -175,10 +210,54 @@ const browserRunProtectedLayer = () => Layer.effect(ProtectedBrowser)(Effect.gen
175
210
  const decoded = yield* Schema.decodeUnknownEffect(ProtectedBrowserClick)(request).pipe(Effect.mapError(() => fail("denied")));
176
211
  yield* permitObservation;
177
212
  const control = yield* target(decoded.ref);
178
- if (control.role !== "link" && control.role !== "button") return yield* fail("unsupported");
213
+ if (control.role !== "link" && control.role !== "button" && control.role !== "radio" && control.role !== "checkbox" && control.role !== "submit") return yield* fail("unsupported");
214
+ let action;
215
+ if (control.role === "link") {
216
+ if (control.url === void 0) return yield* fail("unsupported");
217
+ action = {
218
+ _tag: "Click",
219
+ ref: decoded.ref,
220
+ target: control.target,
221
+ role: "link",
222
+ url: control.url
223
+ };
224
+ } else if (control.role === "submit") action = {
225
+ _tag: "Submit",
226
+ ref: decoded.ref,
227
+ target: control.target
228
+ };
229
+ else action = {
230
+ _tag: "Click",
231
+ ref: decoded.ref,
232
+ target: control.target,
233
+ role: control.role
234
+ };
235
+ yield* authorizeAction(action);
179
236
  dispatch = "possibly-dispatched";
180
- yield* remote(driver.click(decoded.ref));
237
+ yield* remote(driver.click(decoded.ref)).pipe(Effect.catch((error) => {
238
+ if (error.reason === "needs-attention") dispatch = "not-dispatched";
239
+ return Effect.fail(error);
240
+ }));
241
+ dispatch = "dispatched";
242
+ if (control.role === "submit") milestone = "submission-dispatched";
243
+ yield* permitObservation;
244
+ })),
245
+ fill: (request) => run(Effect.gen(function* () {
246
+ const decoded = yield* Schema.decodeUnknownEffect(ProtectedBrowserFill)(request).pipe(Effect.mapError(() => fail("denied")));
247
+ yield* permitObservation;
248
+ const control = yield* target(decoded.ref);
249
+ if (control.role !== "text" && control.role !== "select") return yield* fail("unsupported");
250
+ yield* authorizeAction({
251
+ _tag: "Fill",
252
+ ref: decoded.ref,
253
+ target: control.target,
254
+ role: control.role
255
+ });
256
+ yield* remote(driver.fill(decoded.ref, control.role, Redacted.make(decoded.value))).pipe(Effect.provideService(ProtectedBrowserDispatch, { mark: Effect.sync(() => {
257
+ dispatch = "possibly-dispatched";
258
+ }) }));
181
259
  dispatch = "dispatched";
260
+ milestone = "filled";
182
261
  yield* permitObservation;
183
262
  })),
184
263
  listCredentialOffers: (request) => run(Effect.gen(function* () {
@@ -226,7 +305,6 @@ const browserRunProtectedLayer = () => Layer.effect(ProtectedBrowser)(Effect.gen
226
305
  if (offer === void 0 || offer.expires <= (yield* Clock.currentTimeMillis)) return yield* fail("stale-reference");
227
306
  const principal = yield* caller;
228
307
  if (Redacted.value(principal) !== Redacted.value(offer.caller)) return yield* fail("denied");
229
- if (offer.kind === "card" && decoded.submit !== void 0) return yield* fail("unsupported");
230
308
  if (new Set(decoded.fields.map((field) => field.ref)).size !== decoded.fields.length || new Set(decoded.fields.map((field) => field.role)).size !== decoded.fields.length) return yield* fail("denied");
231
309
  const validate = Effect.gen(function* () {
232
310
  if (!sameTarget(offer.target, (yield* target(offer.targetRef)).target)) return yield* fail("stale-reference");
@@ -251,6 +329,7 @@ const browserRunProtectedLayer = () => Layer.effect(ProtectedBrowser)(Effect.gen
251
329
  const authorize = Effect.gen(function* () {
252
330
  if (Redacted.value(yield* caller) !== Redacted.value(principal)) return yield* fail("denied");
253
331
  yield* access.authorize(authorization).pipe(Effect.mapError((error) => fail(error.reason)));
332
+ if (Redacted.value(yield* caller) !== Redacted.value(principal)) return yield* fail("denied");
254
333
  });
255
334
  yield* authorize;
256
335
  yield* validate;
@@ -302,41 +381,66 @@ const maxAttributeLength = 2048;
302
381
  const inspectFrame = `(() => {
303
382
  const doc = document;
304
383
  const forms = [];
305
- const elements = [...doc.querySelectorAll('input,select,button,a[href]')].slice(0, 65);
384
+ const isChoice = el => el instanceof HTMLInputElement && ['radio','checkbox'].includes(el.type);
385
+ const presented = el => {
386
+ if (el.closest('[hidden],[aria-hidden="true"],[inert]') ||
387
+ ['hidden','collapse'].includes(getComputedStyle(el).visibility)) return false;
388
+ for (let parent = el; parent; parent = parent.parentElement) {
389
+ if (getComputedStyle(parent).opacity === '0') return false;
390
+ }
391
+ return true;
392
+ };
393
+ const hasLayout = el => presented(el) &&
394
+ [...el.getClientRects()].some(rect => rect.width > 0 && rect.height > 0);
395
+ const available = el => el.type !== 'hidden' && !el.closest('[hidden],[aria-hidden="true"],[inert]') &&
396
+ (hasLayout(el) || (isChoice(el) && [...(el.labels ?? [])].some(hasLayout)));
397
+ const elements = [...doc.querySelectorAll('input,textarea,select,button,a[href]')].filter(available).slice(0, 65);
398
+ const labelText = el => {
399
+ const label = el.labels?.[0]?.cloneNode(true);
400
+ label?.querySelectorAll('input,textarea,select,script,style,noscript').forEach(child => child.remove());
401
+ return label?.textContent ?? el.getAttribute('aria-label');
402
+ };
306
403
  const describe = (el) => {
307
- if (doc !== document || !el.isConnected || el.ownerDocument !== doc) return null;
404
+ if (doc !== document || !el.isConnected || el.ownerDocument !== doc || !available(el)) return null;
308
405
  const form = el.form ?? null;
309
406
  let formIndex = forms.indexOf(form);
310
407
  if (formIndex < 0) { formIndex = forms.length; forms.push(form); }
311
- const action = form ? (el.hasAttribute('formaction') ? el.formAction : form.action || doc.URL) : doc.URL;
408
+ // Resolved href (including path/query/fragment and base-URL changes) is part of the fingerprint.
409
+ const action = el instanceof HTMLAnchorElement ? el.href : form ? (el.hasAttribute('formaction') ? el.formAction : form.action || doc.URL) : doc.URL;
312
410
  const method = form ? (el.hasAttribute('formmethod') ? el.formMethod : form.method) : '';
313
411
  const enctype = form?.enctype ?? '';
314
412
  const name = el.name ?? '';
315
413
  const completion = el.getAttribute('autocomplete') ?? '';
316
414
  const inputType = el.type ?? '';
415
+ const choiceValue = isChoice(el) ? el.value : '';
317
416
  // Reject before parsing, fingerprinting or CDP transfer. Truncation could hide a target change.
318
- if ([action, method, enctype, name, completion, inputType].some(value => value.length > ${maxAttributeLength})) return null;
417
+ if ([action, method, enctype, name, completion, inputType, choiceValue].some(value => value.length > ${maxAttributeLength})) return null;
319
418
  const type = inputType.toLowerCase();
320
419
  const autocomplete = completion.trim().toLowerCase().split(/\\s+/).at(-1);
321
420
  const cardRoles = { 'cc-name':'card-name', 'cc-number':'card-number', 'cc-exp':'card-expiry',
322
421
  'cc-exp-month':'card-expiry-month', 'cc-exp-year':'card-expiry-year', 'cc-csc':'card-security-code' };
323
422
  let role = 'unsupported';
324
- const nativeField = el instanceof HTMLInputElement || el instanceof HTMLSelectElement;
325
- if (nativeField && form && !['submit','button'].includes(type) && !el.disabled && !el.readOnly && el.getClientRects().length > 0) {
326
- if (el instanceof HTMLInputElement && type === 'password') role = 'password';
327
- else if (['text','email','tel','number','month',''].includes(type) || el instanceof HTMLSelectElement) {
328
- if (cardRoles[autocomplete]) role = cardRoles[autocomplete];
329
- else if (autocomplete === 'username' || autocomplete === 'email') role = 'username';
423
+ const nativeField = el instanceof HTMLInputElement || el instanceof HTMLSelectElement || el instanceof HTMLTextAreaElement;
424
+ if (nativeField && !['submit','button'].includes(type) && !el.matches(':disabled') && !el.readOnly && !(el instanceof HTMLSelectElement && el.multiple)) {
425
+ if (isChoice(el)) role = type;
426
+ else if (type === 'password' || ['current-password','new-password','one-time-code'].includes(autocomplete)) {
427
+ if (type === 'password' && form) role = 'password';
428
+ } else if (['text','email','tel','number','month','search','url','date','time','week','datetime-local','textarea',''].includes(type) || el instanceof HTMLSelectElement) {
429
+ if (cardRoles[autocomplete]) { if (form) role = cardRoles[autocomplete]; }
430
+ else if (autocomplete === 'username' || autocomplete === 'email') { if (form) role = 'username'; }
330
431
  else if (['text','email'].includes(type) && form && form.querySelector('input[type="password"]')) role = 'username';
432
+ else if (el instanceof HTMLSelectElement) { if (!el.multiple) role = 'select'; }
433
+ else role = 'text';
331
434
  }
332
435
  } else if (el instanceof HTMLButtonElement || (el instanceof HTMLInputElement && ['submit','button'].includes(type))) {
333
- if (!el.disabled) role = type === 'submit' && form ? 'submit' : type === 'button' ? 'button' : 'unsupported';
436
+ if (!el.matches(':disabled')) role = type === 'submit' && form ? 'submit' : type === 'button' ? 'button' : 'unsupported';
334
437
  } else if (el instanceof HTMLAnchorElement) role = 'link';
335
- const fingerprint = JSON.stringify([role, action, method, enctype, name, completion, type]);
336
- return { role, formIndex, action, fingerprint,
337
- label: (el.labels?.[0]?.textContent ?? el.getAttribute('aria-label') ?? el.textContent ?? '').slice(0,200) };
438
+ const fingerprint = JSON.stringify([role, action, method, enctype, name, completion, type, choiceValue]);
439
+ return { role, formIndex, action, fingerprint, ...(isChoice(el) ? {checked: el.checked} : {}),
440
+ label: (labelText(el) ?? (nativeField ? '' : el.textContent) ?? '').slice(0,200) };
338
441
  };
339
- const expose = ({role, formIndex, action, label}) => ({role, formIndex, action, label});
442
+ const expose = ({role, formIndex, action, label, checked}) =>
443
+ ({role, formIndex, action, label, ...(checked === undefined ? {} : {checked})});
340
444
  const original = elements.map(describe);
341
445
  const validate = (index) => {
342
446
  const current = describe(elements[index]);
@@ -348,14 +452,32 @@ const inspectFrame = `(() => {
348
452
  doc, elements, original: original.map(current => current && expose(current)), validate,
349
453
  text: () => {
350
454
  if (doc !== document) return null;
351
- const clone = doc.body?.cloneNode(true);
352
- clone?.querySelectorAll('input,textarea,select,script,style,noscript,iframe,object,embed').forEach(el => el.remove());
353
- return (clone?.textContent ?? '').slice(0,65536);
455
+ if (!doc.body) return '';
456
+ const walker = doc.createTreeWalker(doc.body, NodeFilter.SHOW_TEXT);
457
+ const range = doc.createRange();
458
+ let text = '';
459
+ for (let node = walker.nextNode(); node && text.length < 65536; node = walker.nextNode()) {
460
+ const parent = node.parentElement;
461
+ if (!parent || parent.closest('input,textarea,select,script,style,noscript,iframe,object,embed')) continue;
462
+ if (!presented(parent)) continue;
463
+ range.selectNodeContents(node);
464
+ if (![...range.getClientRects()].some(rect => rect.width > 0 && rect.height > 0)) continue;
465
+ text += (node.textContent ?? '').replace(/\\s+/g, ' ') + ' ';
466
+ }
467
+ return text.slice(0,65536);
354
468
  },
355
469
  fill: (index, role, value) => {
356
470
  const current = validate(index);
357
471
  if (!current || current.role !== role) return false;
358
472
  const el = elements[index];
473
+ if (role === 'select') {
474
+ const options = [...el.options].filter(option => !option.matches(':disabled'));
475
+ const values = options.filter(option => option.value === value);
476
+ const matches = values.length > 0 ? values : options.filter(option => option.textContent?.trim() === value);
477
+ if (matches.length !== 1) return 'unsupported';
478
+ value = matches[0].value;
479
+ if ([...el.options].filter(option => option.value === value).length !== 1) return 'unsupported';
480
+ }
359
481
  let prototype = Object.getPrototypeOf(el);
360
482
  let setter;
361
483
  while (prototype && !setter) { setter = Object.getOwnPropertyDescriptor(prototype,'value')?.set; prototype = Object.getPrototypeOf(prototype); }
@@ -370,7 +492,7 @@ const inspectFrame = `(() => {
370
492
  },
371
493
  click: (index) => {
372
494
  const current = validate(index);
373
- if (!current || !['button','submit','link'].includes(current.role)) return false;
495
+ if (!current || !['button','submit','link','radio','checkbox'].includes(current.role)) return false;
374
496
  if (current.role === 'submit' && elements[index].form && !elements[index].form.matches(':valid')) return 'needs-attention';
375
497
  elements[index].click();
376
498
  return true;
@@ -383,7 +505,8 @@ const Description = Schema.Struct({
383
505
  role: ProtectedBrowserControl.fields.role,
384
506
  formIndex: Schema.Natural,
385
507
  action: Schema.String.check(Schema.isMaxLength(maxAttributeLength)),
386
- label: Schema.String.check(Schema.isMaxLength(200))
508
+ label: Schema.String.check(Schema.isMaxLength(200)),
509
+ checked: Schema.optionalKey(Schema.Boolean)
387
510
  });
388
511
  const Descriptions = Schema.Array(Schema.NullOr(Description)).check(Schema.isMaxLength(65));
389
512
  /** One host-private acquired session. The SDK handles and exact-session cleanup have one owner. */
@@ -413,6 +536,7 @@ const makeProtectedNativeTransport = Effect.fn("ProtectedNativeTransport.make")(
413
536
  let closed = false;
414
537
  let violation = false;
415
538
  let documentRef;
539
+ let observationOrigins;
416
540
  const frames = /* @__PURE__ */ new Map();
417
541
  const controls = /* @__PURE__ */ new Map();
418
542
  const origin = (value) => Effect.try({
@@ -442,12 +566,20 @@ const makeProtectedNativeTransport = Effect.fn("ProtectedNativeTransport.make")(
442
566
  closed = true;
443
567
  clear();
444
568
  };
569
+ const observationFrames = Effect.gen(function* () {
570
+ const list = page.frames();
571
+ if (list.length > 32) return yield* transportError("unsupported");
572
+ return yield* Effect.filter(list, (frame) => frame === page.mainFrame() ? Effect.succeed(true) : Effect.try({
573
+ try: () => frame.url() !== "" && new URL(frame.url()).protocol === "https:",
574
+ catch: () => transportError("provider")
575
+ }));
576
+ });
445
577
  const context = Effect.gen(function* () {
446
578
  yield* check;
447
- const list = page.frames();
448
- if (list.length > 16) return yield* transportError("unsupported");
579
+ const list = yield* observationFrames;
449
580
  const topOrigin = yield* origin(page.url());
450
- const frameOrigins = yield* Effect.forEach(list, (frame) => origin(frame.url()));
581
+ const frameOrigins = [...new Set(yield* Effect.forEach(list, (frame) => origin(frame.url())))];
582
+ if (frameOrigins.length > 16) return yield* transportError("unsupported");
451
583
  if (documentRef === void 0) documentRef = yield* uuid;
452
584
  return yield* decode(ProtectedPageContext, {
453
585
  document: documentRef,
@@ -465,7 +597,7 @@ const makeProtectedNativeTransport = Effect.fn("ProtectedNativeTransport.make")(
465
597
  const get = Effect.fn("ProtectedNativeTransport.get")(function* (ref) {
466
598
  yield* check;
467
599
  const state = controls.get(ref);
468
- if (!state || state.expires <= (yield* clock.currentTimeMillis) || !(yield* isCurrent(state.frame))) return yield* transportError("stale-reference");
600
+ if (!state || observationOrigins !== void 0 && !observationOrigins.has(state.control.target.frameOrigin) || state.expires <= (yield* clock.currentTimeMillis) || !(yield* isCurrent(state.frame))) return yield* transportError("stale-reference");
469
601
  const current = yield* remote(() => state.frame.handle.evaluate((held, index) => {
470
602
  if (typeof held !== "object" || held === null) return null;
471
603
  return Reflect.apply(Reflect.get(held, "validate"), held, [index]);
@@ -490,6 +622,11 @@ const makeProtectedNativeTransport = Effect.fn("ProtectedNativeTransport.make")(
490
622
  browser.on("targetcreated", onTarget);
491
623
  }), () => close);
492
624
  return {
625
+ restrictObservation: Effect.fn("ProtectedNativeTransport.restrictObservation")(function* (origins) {
626
+ yield* check;
627
+ observationOrigins = origins === void 0 ? void 0 : new Set(yield* decode(Schema.Array(CredentialOrigin), origins));
628
+ for (const [ref, state] of controls) if (observationOrigins !== void 0 && !observationOrigins.has(state.control.target.frameOrigin)) controls.delete(ref);
629
+ }),
493
630
  context,
494
631
  invalidate,
495
632
  close,
@@ -509,7 +646,11 @@ const makeProtectedNativeTransport = Effect.fn("ProtectedNativeTransport.make")(
509
646
  const discovered = [];
510
647
  let text = "";
511
648
  let truncated = false;
512
- for (const frame of page.frames()) {
649
+ const frameOrigins = /* @__PURE__ */ new Set();
650
+ for (const frame of yield* observationFrames) {
651
+ const frameOrigin = yield* origin(frame.url());
652
+ if (observationOrigins !== void 0 && !observationOrigins.has(frameOrigin)) continue;
653
+ frameOrigins.add(frameOrigin);
513
654
  if (typeof frame.isolatedRealm !== "function") return yield* transportError("unsupported");
514
655
  const handle = yield* remote(() => frame.isolatedRealm().evaluateHandle(inspectFrame));
515
656
  const state = {
@@ -543,6 +684,8 @@ const makeProtectedNativeTransport = Effect.fn("ProtectedNativeTransport.make")(
543
684
  ref: yield* uuid,
544
685
  role: desc.role,
545
686
  label: desc.label,
687
+ ...desc.checked === void 0 ? {} : { checked: desc.checked },
688
+ ...desc.role === "link" ? { url: desc.action } : {},
546
689
  target: CredentialTarget.make({
547
690
  topOrigin: before.topOrigin,
548
691
  frameOrigin: yield* origin(frame.url()),
@@ -573,6 +716,7 @@ const makeProtectedNativeTransport = Effect.fn("ProtectedNativeTransport.make")(
573
716
  if ((yield* context).document !== before.document) return yield* transportError("stale-reference");
574
717
  return yield* decode(ProtectedDiscovery, {
575
718
  ...before,
719
+ frameOrigins: [...frameOrigins],
576
720
  text,
577
721
  controls: discovered,
578
722
  truncated
@@ -658,7 +802,7 @@ const browserRunProtectedBindingLayer = (options) => Layer.effect(BrowserRunProt
658
802
  return options.browser.fetch(request);
659
803
  } }, {
660
804
  recording: false,
661
- keep_alive: Math.max(1e4, policy.maxElapsedMillis)
805
+ keep_alive: Math.min(6e5, Math.max(1e4, policy.maxElapsedMillis))
662
806
  });
663
807
  sessionId = Redacted.make(Schema.decodeUnknownSync(Schema.String.check(Schema.isUUID()))(acquired.sessionId));
664
808
  if (!recordingDisabled || signal.aborted || invalid) {