cdp-toolkit 1.6.0 → 1.7.0
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/CHANGELOG.md +155 -0
- package/CONTRACT.md +7 -2
- package/README.md +57 -13
- package/dist/cli.js +184 -62
- package/dist/index.js +184 -62
- package/dist/mcp.js +245 -112
- package/package.json +1 -1
- package/src/leases-tools.ts +146 -25
- package/src/leases.ts +83 -3
- package/src/manifest.ts +59 -49
- package/src/mcp.ts +5 -2
- package/src/reap.ts +114 -0
- package/src/shared-tools.ts +39 -7
package/src/shared-tools.ts
CHANGED
|
@@ -39,8 +39,9 @@ import type {
|
|
|
39
39
|
BrowserCookie, BrowserDriver, DriverUid, ElementLocator, NavigateResult, PageDriver, PageInfo, SnapshotNode,
|
|
40
40
|
} from "./driver.ts";
|
|
41
41
|
import type { TargetSelector } from "./types.ts";
|
|
42
|
-
import { assertLeaseOk, claimLease, defaultLabel, leaseTtlMs, releaseLeaseFor, type LeaseBackend, type LeaseToken } from "./leases.ts";
|
|
42
|
+
import { assertLeaseOk, claimLease, defaultLabel, leaseTtlMs, releaseLeaseFor, requireLease, type LeaseBackend, type LeaseToken } from "./leases.ts";
|
|
43
43
|
import { newTrackedPage, originIndex, type PageOrigin } from "./origins.ts";
|
|
44
|
+
import { reapStaleAgentTabs, type ReapedTab } from "./reap.ts";
|
|
44
45
|
|
|
45
46
|
const ARTIFACT_DIR = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
46
47
|
const STATE_DIR = process.env.CDP_STATE_DIR ?? "/tmp/cdp-toolkit";
|
|
@@ -70,7 +71,9 @@ async function withPage<T>(driver: BrowserDriver, target: TargetSelector | undef
|
|
|
70
71
|
}
|
|
71
72
|
}
|
|
72
73
|
|
|
73
|
-
|
|
74
|
+
/** Exported for leases-tools.ts's release_page{target}, which needs exactly this
|
|
75
|
+
* gate: resolving through here is what authorizes the release. */
|
|
76
|
+
export async function resolvePage(driver: BrowserDriver, selector?: TargetSelector): Promise<PageInfo> {
|
|
74
77
|
const pages = await driver.listPages();
|
|
75
78
|
const hit = await pickPage(driver, pages, selector);
|
|
76
79
|
// THE third choke point. close_page and select_page resolve here and nowhere
|
|
@@ -93,8 +96,17 @@ async function resolvePage(driver: BrowserDriver, selector?: TargetSelector): Pr
|
|
|
93
96
|
* every branch resolves against page-type targets only EXCEPT a bare-id lookup, which (like
|
|
94
97
|
* legacy resolveTarget's `targets.find`) searches the FULL unfiltered listing, so passing an
|
|
95
98
|
* exact non-page targetId (a worker, an iframe) still resolves.
|
|
99
|
+
*
|
|
100
|
+
* THE GATE-FREE HALF of resolvePage, and exported for the one caller that needs
|
|
101
|
+
* resolution WITHOUT authorization: claim_page{target}. Everything else must go
|
|
102
|
+
* through resolvePage. Claiming cannot run assertLeaseOk, because under
|
|
103
|
+
* CDP_REQUIRE_LEASE the gate AUTO-ACQUIRES a lease on an unleased tab, so a
|
|
104
|
+
* claim routed through it would take the tab twice — once implicitly with an
|
|
105
|
+
* auto lease, then again explicitly, and the second claim would collide with
|
|
106
|
+
* the first. Refusing a tab another process holds is still enforced, one layer
|
|
107
|
+
* down, by claimLease's own conflict check.
|
|
96
108
|
*/
|
|
97
|
-
async function pickPage(driver: BrowserDriver, pages: PageInfo[], selector?: TargetSelector): Promise<PageInfo> {
|
|
109
|
+
export async function pickPage(driver: BrowserDriver, pages: PageInfo[], selector?: TargetSelector): Promise<PageInfo> {
|
|
98
110
|
if (selector === undefined || selector === "active") {
|
|
99
111
|
if (!pages.length) throw new SharedToolError("no page targets open");
|
|
100
112
|
return pages[0]!;
|
|
@@ -176,8 +188,15 @@ async function reapSet(driver: BrowserDriver, pages: PageInfo[], all?: boolean):
|
|
|
176
188
|
}
|
|
177
189
|
}
|
|
178
190
|
|
|
179
|
-
export async function listPages(
|
|
180
|
-
|
|
191
|
+
export async function listPages(
|
|
192
|
+
driver: BrowserDriver,
|
|
193
|
+
args: { all?: boolean } = {},
|
|
194
|
+
): Promise<{ pages: ListedPage[]; count: number; reaped?: ReapedTab[] }> {
|
|
195
|
+
// Reap FIRST, so a tab this call is about to close never appears in the
|
|
196
|
+
// listing it returns. Under strict mode only: see requireLease.
|
|
197
|
+
const reaped = requireLease() ? await reapStaleAgentTabs(driver, backendOf(driver)) : [];
|
|
198
|
+
const reapedIds = new Set(reaped.map((r) => r.targetId));
|
|
199
|
+
const pages = (await driver.listPages({ all: args.all })).filter((p) => !reapedIds.has(p.id));
|
|
181
200
|
// Never let provenance break the listing: originIndex does not throw, and a
|
|
182
201
|
// missing or unreadable ledger yields an empty map, so every page falls back
|
|
183
202
|
// to origin "unknown" exactly as a pre-ledger consumer always saw.
|
|
@@ -188,7 +207,10 @@ export async function listPages(driver: BrowserDriver, args: { all?: boolean } =
|
|
|
188
207
|
if (rec.unreadable !== undefined) return { ...p, origin: "unknown", originUnreadable: rec.unreadable };
|
|
189
208
|
return { ...p, origin: "agent", label: rec.label, createdAt: rec.createdAt };
|
|
190
209
|
});
|
|
191
|
-
|
|
210
|
+
// `reaped` is present only when it has something to say, so the common-case
|
|
211
|
+
// shape is byte-identical to 1.4.0. Silently closing a tab is not acceptable:
|
|
212
|
+
// if a read closed something, the read says so.
|
|
213
|
+
return { pages: annotated, count: annotated.length, ...(reaped.length ? { reaped } : {}) };
|
|
192
214
|
}
|
|
193
215
|
|
|
194
216
|
export async function newPage(
|
|
@@ -199,12 +221,22 @@ export async function newPage(
|
|
|
199
221
|
// Creates the tab AND writes its creation record. The record outlives any
|
|
200
222
|
// lease taken below, which is the point: a released tab is still an agent's.
|
|
201
223
|
const p = await newTrackedPage(driver, backendOf(driver), { url: args.url, label });
|
|
202
|
-
|
|
224
|
+
// Under strict mode a tab nobody holds is a tab nobody may drive, so creating
|
|
225
|
+
// one without claiming it would hand back a target the very next call has to
|
|
226
|
+
// auto-acquire anyway. Doing it here means the tab is never briefly unowned
|
|
227
|
+
// AND the caller is handed a token it can release with, which matters because
|
|
228
|
+
// an auto-acquired lease never returns one.
|
|
229
|
+
const explicit = args.claim === true;
|
|
230
|
+
if (!explicit && !requireLease()) return { targetId: p.id, url: p.url };
|
|
203
231
|
// Atomic in the sense that matters: the tab is claimed before this call
|
|
204
232
|
// returns, so no other agent can see it unclaimed and take it first.
|
|
205
233
|
const { record, token } = await claimLease(backendOf(driver), p.id, {
|
|
206
234
|
label,
|
|
207
235
|
ttlMs: typeof args.ttlMs === "number" && args.ttlMs > 0 ? args.ttlMs : leaseTtlMs(),
|
|
236
|
+
// An explicit claim:true stays explicit under strict mode: the caller asked
|
|
237
|
+
// for the strong tier and must keep getting it, or turning strict on would
|
|
238
|
+
// silently WEAKEN every existing claim:true call site to pid-only.
|
|
239
|
+
auto: !explicit,
|
|
208
240
|
});
|
|
209
241
|
return { targetId: p.id, url: p.url, lease: token, label: record.label, expiresAt: record.lastUsedAt + record.ttlMs };
|
|
210
242
|
}
|