@effect-agent/platform-cloudflare 0.1.0-beta.122 → 0.1.0-beta.124

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.
Files changed (34) hide show
  1. package/dist/BrowserCredentials.d.mts +91 -0
  2. package/dist/BrowserCredentials.mjs +120 -0
  3. package/dist/BrowserCredentials.mjs.map +1 -0
  4. package/dist/BrowserSession-DuOUcC6G.mjs +442 -0
  5. package/dist/BrowserSession-DuOUcC6G.mjs.map +1 -0
  6. package/dist/BrowserSession-DxCN-5F9.d.mts +109 -0
  7. package/dist/BrowserSession.d.mts +3 -0
  8. package/dist/BrowserSession.mjs +4 -0
  9. package/dist/CloudflareThreadClient.d.mts +48 -48
  10. package/dist/{InteractiveBrowser-Cm4xgiGy.d.mts → InteractiveBrowser-BnqY_Y-F.d.mts} +2 -2
  11. package/dist/{InteractiveBrowser-DXwmvWdA.mjs → InteractiveBrowser-DeOlcu-2.mjs} +4 -5
  12. package/dist/InteractiveBrowser-DeOlcu-2.mjs.map +1 -0
  13. package/dist/InteractiveBrowser.d.mts +1 -1
  14. package/dist/InteractiveBrowser.mjs +1 -1
  15. package/dist/{ThreadObject-mt0uxvZZ.d.mts → ThreadObject-QBlTByAB.d.mts} +32 -32
  16. package/dist/ThreadObject.d.mts +1 -1
  17. package/dist/index.d.mts +4 -2
  18. package/dist/index.mjs +3 -1
  19. package/package.json +1 -1
  20. package/src/BrowserCredentials.ts +146 -0
  21. package/src/BrowserSession.ts +589 -0
  22. package/src/index.ts +2 -0
  23. package/src/internal/browser-binding.ts +3 -3
  24. package/src/internal/browser-credentials.ts +362 -0
  25. package/dist/InteractiveBrowser-DXwmvWdA.mjs.map +0 -1
  26. package/dist/ProtectedBrowser.d.mts +0 -142
  27. package/dist/ProtectedBrowser.mjs +0 -1242
  28. package/dist/ProtectedBrowser.mjs.map +0 -1
  29. package/src/ProtectedBrowser.ts +0 -9
  30. package/src/protected-browser/binding.ts +0 -445
  31. package/src/protected-browser/host.ts +0 -398
  32. package/src/protected-browser/inspect-frame.ts +0 -140
  33. package/src/protected-browser/native.ts +0 -483
  34. package/src/protected-browser/policy.ts +0 -872
@@ -1,445 +0,0 @@
1
- /// <reference types="@cloudflare/workers-types" />
2
-
3
- import { Cause, Context, Crypto, Effect, Layer, Redacted, Schema, type Scope } from "effect";
4
- import { type InteractiveBrowserPolicy } from "effect-agent/interactive-browser";
5
- import { ProtectedBrowserError } from "effect-agent/protected-browser";
6
- import {
7
- type Browser,
8
- type Page,
9
- type CDPSession,
10
- } from "puppeteer-core/lib/esm/puppeteer/puppeteer-core-browser.js";
11
-
12
- import { BrowserRunBinding, type BrowserRunAttachment } from "../internal/browser-binding.ts";
13
- import {
14
- browserFailure,
15
- BrowserRunFailure,
16
- reportBrowserCause,
17
- reportedBrowserError,
18
- } from "../internal/browser-failure.ts";
19
- import { BrowserRunSessionLifecycle } from "../internal/browser-session-lifecycle.ts";
20
- import { makeProtectedNativeTransport, ProtectedNativeSession } from "./native.ts";
21
- import {
22
- BrowserRunProtectedTransport,
23
- ProtectedTransportError,
24
- type ProtectedBrowserTransport,
25
- } from "./policy.ts";
26
-
27
- /** Host-private exact page identity; never include it in model-visible results. */
28
- export const ProtectedProviderIdentity = Schema.Struct({
29
- sessionId: Schema.Redacted(Schema.String.check(Schema.isUUID())),
30
- contextId: Schema.Redacted(Schema.NonEmptyString.check(Schema.isMaxLength(256))),
31
- targetId: Schema.Redacted(Schema.NonEmptyString.check(Schema.isMaxLength(256))),
32
- });
33
-
34
- export type ProtectedProviderIdentity = typeof ProtectedProviderIdentity.Type;
35
-
36
- export interface ProtectedProviderSession {
37
- readonly identity: ProtectedProviderIdentity;
38
- readonly driver: ProtectedBrowserTransport;
39
- readonly command: (
40
- method: "Cloudflare.getLiveView" | "Cloudflare.handoff" | "Cloudflare.getHandoffState",
41
- parameters: Readonly<Record<string, string | number>>,
42
- ) => Effect.Effect<unknown, ProtectedBrowserError>;
43
- readonly detach: Effect.Effect<void, ProtectedBrowserError>;
44
- }
45
-
46
- export class BrowserRunProtectedBinding extends Context.Service<
47
- BrowserRunProtectedBinding,
48
- {
49
- readonly open: (
50
- policy: InteractiveBrowserPolicy,
51
- identity?: ProtectedProviderIdentity,
52
- ) => Effect.Effect<ProtectedProviderSession, ProtectedBrowserError, Scope.Scope>;
53
- readonly keepAlive: (
54
- sessionId: Redacted.Redacted<string>,
55
- ) => Effect.Effect<void, ProtectedBrowserError>;
56
- }
57
- >()("@effect-agent/platform-cloudflare/BrowserRunProtectedBinding") {}
58
-
59
- /**
60
- * Acquires through BROWSER with recording=false explicitly on the wire. The trusted provider's
61
- * documented opt-in semantics are the recording guarantee; no recording-status API exists.
62
- * Account operators remain trusted, and must not attach external observers to private passes.
63
- */
64
- const protectedBindingLayer = (options: { readonly browser: Pick<BrowserRun, "fetch"> }) =>
65
- Layer.effect(BrowserRunProtectedBinding)(
66
- Effect.gen(function* () {
67
- const lifecycle = yield* BrowserRunSessionLifecycle;
68
- const binding = yield* BrowserRunBinding;
69
- const crypto = yield* Crypto.Crypto;
70
-
71
- const open = Effect.fn("BrowserRunProtectedTransport.open")(function* (
72
- policy: InteractiveBrowserPolicy,
73
- identity?: ProtectedProviderIdentity,
74
- ): Effect.fn.Return<ProtectedProviderSession, ProtectedBrowserError, Scope.Scope> {
75
- const failure = () =>
76
- new ProtectedBrowserError({
77
- reason: "provider",
78
- dispatch: "not-dispatched",
79
- milestone: "none",
80
- observation: "closed",
81
- cleanup: "unconfirmed",
82
- });
83
-
84
- let sessionId: Redacted.Redacted<string> | undefined = identity?.sessionId;
85
- let detached = false;
86
- let control: CDPSession | undefined;
87
- let providerIdentity: ProtectedProviderIdentity | undefined;
88
- let browser: Browser | undefined;
89
- let attachment: BrowserRunAttachment | undefined;
90
- let driver: ProtectedBrowserTransport | undefined;
91
- let invalid = false;
92
- // SDK acquisition may finish after interruption. Its late-reply callback must await cleanup,
93
- // retaining this pass's clock and private reporting scope.
94
- const runCleanup = Effect.runPromiseWith(yield* Effect.context<never>());
95
-
96
- const terminate = Effect.gen(function* () {
97
- invalid = true;
98
- driver?.invalidate();
99
- if (sessionId === undefined) return "unconfirmed" as const;
100
-
101
- // A failed resume has not acquired the host's retained provider. Release only
102
- // this connection; successful attachments and new allocations own termination.
103
- if (identity !== undefined && driver === undefined) {
104
- if (attachment !== undefined)
105
- yield* attachment.retire.pipe(
106
- Effect.interruptible,
107
- Effect.timeoutOrElse({
108
- duration: "1 second",
109
- orElse: () =>
110
- Effect.fail(
111
- new BrowserRunFailure({
112
- operation: "protected.disconnect",
113
- reason: "timeout",
114
- }),
115
- ),
116
- }),
117
- Effect.catch((error) =>
118
- reportBrowserCause("protected.disconnect", Cause.fail(error)).pipe(
119
- Effect.andThen(() => Effect.die(reportedBrowserError(error))),
120
- ),
121
- ),
122
- );
123
-
124
- return "unconfirmed" as const;
125
- }
126
-
127
- const cleanup = yield* lifecycle.close(sessionId).pipe(
128
- Effect.as("confirmed" as const),
129
- Effect.catchCause((cause) =>
130
- reportBrowserCause("protected.close", cause).pipe(Effect.as("unconfirmed" as const)),
131
- ),
132
- Effect.interruptible,
133
- Effect.timeoutOrElse({
134
- duration: "10 seconds",
135
- orElse: () =>
136
- reportBrowserCause(
137
- "protected.close",
138
- Cause.fail(
139
- new BrowserRunFailure({ operation: "protected.close", reason: "timeout" }),
140
- ),
141
- ).pipe(Effect.as("unconfirmed" as const)),
142
- }),
143
- );
144
-
145
- // Local disconnect is not remote-closure evidence. Do it even when confirmation fails.
146
- const connected = browser;
147
-
148
- if (connected !== undefined)
149
- yield* Effect.promise(() => connected.disconnect()).pipe(
150
- Effect.catchCause((cause) => reportBrowserCause("protected.disconnect", cause)),
151
- Effect.interruptible,
152
- Effect.timeoutOrElse({
153
- duration: "1 second",
154
- orElse: () =>
155
- reportBrowserCause(
156
- "protected.disconnect",
157
- Cause.fail(
158
- new BrowserRunFailure({
159
- operation: "protected.disconnect",
160
- reason: "timeout",
161
- }),
162
- ),
163
- ),
164
- }),
165
- );
166
-
167
- return cleanup;
168
- });
169
-
170
- const close = yield* Effect.cached(terminate);
171
-
172
- yield* Effect.addFinalizer(() => (detached ? Effect.void : close));
173
- let stage = identity === undefined ? "protected.acquire" : "protected.connect";
174
-
175
- const acquired = yield* Effect.tryPromise({
176
- try: async (signal) => {
177
- try {
178
- sessionId =
179
- identity === undefined
180
- ? Redacted.make(
181
- await binding.acquire(
182
- Math.min(600_000, Math.max(10_000, policy.maxElapsedMillis)),
183
- "protected.acquire",
184
- ),
185
- )
186
- : identity.sessionId;
187
- if (signal.aborted || invalid) {
188
- await runCleanup(terminate);
189
- throw new ProtectedTransportError({ reason: "stale-reference" });
190
- }
191
- // Resume only a host-persisted exact page. Never open a replacement page or context.
192
- stage = "protected.connect";
193
- attachment = binding.connect(Redacted.value(sessionId), stage, signal);
194
- browser = await attachment.browser;
195
- if (signal.aborted || invalid) {
196
- await runCleanup(terminate);
197
- throw new ProtectedTransportError({ reason: "stale-reference" });
198
- }
199
- let page: Page;
200
-
201
- if (identity === undefined) {
202
- stage = "protected.context";
203
- const context = await browser.createBrowserContext();
204
-
205
- stage = "protected.page";
206
- page = await context.newPage();
207
- } else {
208
- stage = "protected.resume";
209
-
210
- const context = browser
211
- .browserContexts()
212
- .find((candidate) => candidate.id === Redacted.value(identity.contextId));
213
-
214
- if (context === undefined)
215
- throw new ProtectedTransportError({ reason: "stale-reference" });
216
- const pages = await context.pages();
217
- let found: Page | undefined;
218
-
219
- for (const candidate of pages) {
220
- const client = await candidate.createCDPSession();
221
- const info = await client.send("Target.getTargetInfo");
222
-
223
- await client.detach();
224
- if (info.targetInfo.targetId === Redacted.value(identity.targetId))
225
- found = candidate;
226
- }
227
- if (found === undefined || pages.length !== 1)
228
- throw new ProtectedTransportError({ reason: "stale-reference" });
229
- page = found;
230
- }
231
- stage = "protected.control";
232
- control = await page.createCDPSession();
233
- const info = await control.send("Target.getTargetInfo");
234
-
235
- stage = "protected.identity";
236
- providerIdentity = Schema.decodeSync(ProtectedProviderIdentity)({
237
- sessionId,
238
- contextId: Redacted.make(page.browserContext().id ?? ""),
239
- targetId: Redacted.make(info.targetInfo.targetId),
240
- });
241
-
242
- // Unrestricted passes need no attachment-local network enforcement. Leave their
243
- // service workers and requests alone, including across host-owned detach/resume.
244
- if (policy.network._tag !== "Unrestricted") {
245
- stage = "protected.interception";
246
- await page.setBypassServiceWorker(true);
247
- await page.setRequestInterception(true);
248
- page.on("request", (request) => {
249
- let allowed = false;
250
-
251
- try {
252
- const url = new URL(request.url());
253
-
254
- allowed =
255
- policy.network._tag === "ExactHosts" &&
256
- url.protocol === "https:" &&
257
- !url.username &&
258
- !url.password &&
259
- policy.network.allowedHosts.includes(url.host);
260
- } catch {
261
- /* Refuse malformed destinations. */
262
- }
263
- void (
264
- allowed && !invalid ? request.continue() : request.abort("blockedbyclient")
265
- ).catch(async (cause) => {
266
- invalid = true;
267
- driver?.invalidate();
268
- await runCleanup(
269
- reportBrowserCause("protected.interception", Cause.fail(cause)),
270
- );
271
- });
272
- });
273
- }
274
- if (signal.aborted || invalid) {
275
- await runCleanup(terminate);
276
- throw new ProtectedTransportError({ reason: "stale-reference" });
277
- }
278
-
279
- return ProtectedNativeSession.of({
280
- browser,
281
- page,
282
- close,
283
- release: Effect.suspend(() => (detached ? Effect.void : close.pipe(Effect.asVoid))),
284
- });
285
- } catch (cause) {
286
- if (signal.aborted && !(cause instanceof ProtectedTransportError))
287
- await runCleanup(reportBrowserCause(stage, Cause.fail(cause)));
288
- throw cause;
289
- }
290
- },
291
- catch: (cause) => browserFailure(stage, cause),
292
- }).pipe(
293
- Effect.timeoutOrElse({
294
- duration: Math.min(policy.maxElapsedMillis, 30_000),
295
- orElse: () =>
296
- Effect.fail(new BrowserRunFailure({ operation: stage, reason: "timeout" })),
297
- }),
298
- Effect.catch((error) => {
299
- if (identity !== undefined && driver === undefined)
300
- return reportBrowserCause(stage, Cause.fail(error)).pipe(
301
- Effect.andThen(
302
- Effect.fail(
303
- reportedBrowserError(
304
- new ProtectedBrowserError({
305
- ...failure(),
306
- reason: error.reason === "timeout" ? "timeout" : "provider",
307
- cleanup: "not-requested",
308
- }),
309
- ),
310
- ),
311
- ),
312
- // A disposal defect stays alongside the original failure. Only a pure typed
313
- // failure proves that this pre-handle attachment is safe to relinquish.
314
- Effect.ensuring(close),
315
- );
316
-
317
- return reportBrowserCause(stage, Cause.fail(error)).pipe(
318
- Effect.andThen(close),
319
- Effect.flatMap((cleanup) =>
320
- Effect.fail(
321
- reportedBrowserError(
322
- new ProtectedBrowserError({
323
- ...failure(),
324
- reason: error.reason === "timeout" ? "timeout" : "provider",
325
- cleanup,
326
- }),
327
- ),
328
- ),
329
- ),
330
- );
331
- }),
332
- );
333
-
334
- driver = yield* makeProtectedNativeTransport(policy).pipe(
335
- Effect.provideService(ProtectedNativeSession, acquired),
336
- Effect.provideService(Crypto.Crypto, crypto),
337
- );
338
-
339
- const exact = providerIdentity;
340
-
341
- if (exact === undefined) return yield* failure();
342
-
343
- return {
344
- identity: exact,
345
- driver,
346
- command: (method, parameters) =>
347
- Effect.tryPromise({
348
- try: async () => {
349
- if (invalid || detached || control === undefined) throw undefined;
350
-
351
- // Cloudflare extends CDP with host-only methods absent from upstream ProtocolMapping.
352
- const send = Reflect.get(control, "send");
353
-
354
- return await Reflect.apply(send, control, [method, parameters]);
355
- },
356
- catch: (cause) => browserFailure("protected.command", cause),
357
- }).pipe(
358
- Effect.timeoutOrElse({
359
- duration: "10 seconds",
360
- orElse: () =>
361
- Effect.fail(
362
- new BrowserRunFailure({ operation: "protected.command", reason: "timeout" }),
363
- ),
364
- }),
365
- Effect.catch((error) =>
366
- reportBrowserCause("protected.command", Cause.fail(error)).pipe(
367
- Effect.andThen(Effect.fail(reportedBrowserError(failure()))),
368
- ),
369
- ),
370
- ),
371
- detach: Effect.tryPromise({
372
- try: async () => {
373
- if (invalid || detached || browser === undefined) throw undefined;
374
- // Stop this attachment before another controller can attach. The browser remains live.
375
- await browser.disconnect();
376
- detached = true;
377
- driver?.invalidate();
378
- },
379
- catch: (cause) => browserFailure("protected.detach", cause),
380
- }).pipe(
381
- Effect.timeoutOrElse({
382
- duration: "10 seconds",
383
- orElse: () =>
384
- Effect.fail(
385
- new BrowserRunFailure({ operation: "protected.detach", reason: "timeout" }),
386
- ),
387
- }),
388
- Effect.catch((error) =>
389
- reportBrowserCause("protected.detach", Cause.fail(error)).pipe(
390
- Effect.andThen(Effect.fail(reportedBrowserError(failure()))),
391
- ),
392
- ),
393
- ),
394
- };
395
- }, Effect.withTracerEnabled(false));
396
-
397
- const keepAlive = Effect.fn("BrowserRunProtectedBinding.keepAlive")(function* (
398
- sessionId: Redacted.Redacted<string>,
399
- ) {
400
- const failure = (reason: ProtectedBrowserError["reason"]) =>
401
- new ProtectedBrowserError({
402
- reason,
403
- dispatch: "not-dispatched",
404
- milestone: "none",
405
- observation: "protected",
406
- cleanup: "not-requested",
407
- });
408
-
409
- const id = yield* Schema.decodeEffect(ProtectedProviderIdentity.fields.sessionId)(
410
- sessionId,
411
- ).pipe(Effect.mapError(() => failure("denied")));
412
-
413
- yield* binding.keepAlive(Redacted.value(id)).pipe(
414
- Effect.timeoutOrElse({
415
- duration: "10 seconds",
416
- orElse: () =>
417
- Effect.fail(
418
- new BrowserRunFailure({ operation: "protected.keepAlive", reason: "timeout" }),
419
- ),
420
- }),
421
- Effect.tapError((error) => reportBrowserCause("protected.keepAlive", Cause.fail(error))),
422
- Effect.mapError((error) =>
423
- reportedBrowserError(failure(error.reason === "timeout" ? "timeout" : "provider")),
424
- ),
425
- );
426
- }, Effect.withTracerEnabled(false));
427
-
428
- return { open, keepAlive };
429
- }),
430
- ).pipe(Layer.provide(BrowserRunBinding.layer(options.browser)));
431
-
432
- /** One binding implementation serves scoped tools and host-managed protected handoffs. */
433
- export const browserRunProtectedBindingLayer = (options: {
434
- readonly browser: Pick<BrowserRun, "fetch">;
435
- }) =>
436
- Layer.effect(BrowserRunProtectedTransport)(
437
- Effect.gen(function* () {
438
- const binding = yield* BrowserRunProtectedBinding;
439
-
440
- return {
441
- open: (policy: InteractiveBrowserPolicy) =>
442
- binding.open(policy).pipe(Effect.map((session) => session.driver)),
443
- };
444
- }),
445
- ).pipe(Layer.provideMerge(protectedBindingLayer(options)));