cdp-toolkit 1.6.0 → 1.8.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 +439 -0
- package/CONTRACT.md +11 -2
- package/README.md +89 -23
- package/dist/cli.js +1150 -111
- package/dist/index.js +1011 -99
- package/dist/mcp.js +1447 -167
- package/package.json +5 -3
- package/src/activity.ts +533 -0
- package/src/bidi/driver.ts +231 -8
- package/src/capabilities.ts +1 -1
- package/src/cdp/driver.ts +472 -25
- package/src/driver.ts +161 -3
- package/src/index.ts +26 -4
- package/src/leases-tools.ts +269 -27
- package/src/leases.ts +84 -4
- package/src/manifest.ts +295 -56
- package/src/mcp.ts +10 -2
- package/src/reap.ts +165 -0
- package/src/shared-tools.ts +303 -30
- package/src/tools/browser-session.ts +246 -0
- package/src/tools/dispatch-mouse.ts +108 -0
- package/src/tools/downloads.ts +143 -0
- package/src/tools/permissions.ts +130 -0
package/src/leases.ts
CHANGED
|
@@ -49,6 +49,15 @@ export interface LeaseRecord {
|
|
|
49
49
|
createdAt: number;
|
|
50
50
|
lastUsedAt: number;
|
|
51
51
|
ttlMs: number;
|
|
52
|
+
/**
|
|
53
|
+
* How this lease was taken, and therefore who may use it without a token.
|
|
54
|
+
* true - the gate acquired it implicitly; passes for any call from `pid`.
|
|
55
|
+
* false - claim_page or new_page{claim:true}; the token is required, even
|
|
56
|
+
* from the owning process.
|
|
57
|
+
* Absent on records written by <=1.4.0 and read as false, so an upgrade never
|
|
58
|
+
* downgrades a held lease from token-required to pid-only.
|
|
59
|
+
*/
|
|
60
|
+
auto?: boolean;
|
|
52
61
|
}
|
|
53
62
|
|
|
54
63
|
/** 15 minutes. Refresh-on-use means an active agent never expires, so this only
|
|
@@ -65,6 +74,43 @@ export function leaseTtlMs(): number {
|
|
|
65
74
|
return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_LEASE_TTL_MS;
|
|
66
75
|
}
|
|
67
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Whether THIS process is the long-lived MCP server rather than a one-shot CLI
|
|
79
|
+
* invocation. Module state, which this file's header otherwise forbids: that
|
|
80
|
+
* prohibition is about state describing OTHER processes' ownership, which must
|
|
81
|
+
* live on disk because two MCP servers drive one browser. This describes this
|
|
82
|
+
* process's own role, which no other process can observe or contend for, so a
|
|
83
|
+
* module flag is the correct scope rather than a violation of the rule.
|
|
84
|
+
*/
|
|
85
|
+
let longLived = false;
|
|
86
|
+
|
|
87
|
+
/** Called once by mcp.ts at startup. cli.ts never calls it. The parameter
|
|
88
|
+
* exists so a test can put the module back into CLI state. */
|
|
89
|
+
export function markLongLivedProcess(value = true): void {
|
|
90
|
+
longLived = value;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Strict mode: every tool call acquires a lease for the tab it resolves, and
|
|
95
|
+
* stale agent tabs are reaped on read. Off by default, so 1.4.0 consumers are
|
|
96
|
+
* unaffected.
|
|
97
|
+
*
|
|
98
|
+
* ALWAYS FALSE UNDER THE CLI, regardless of the env var, and that is not a
|
|
99
|
+
* convenience. cli.ts already refuses claim_page because a CLI invocation is
|
|
100
|
+
* one process per call, so its lease is reclaimable by the dead-pid rule the
|
|
101
|
+
* moment the process exits, and "a lease that is reclaimable on arrival is
|
|
102
|
+
* worse than no lease". Auto-acquire would mint exactly such a lease on every
|
|
103
|
+
* call. The sharper danger is reap: those dead-pid records would read as
|
|
104
|
+
* orphaned agent tabs, so a CLI user running list_pages twice could watch the
|
|
105
|
+
* second run close the tabs the first run leased. Read per call, like
|
|
106
|
+
* leaseDir(), so a test can flip it.
|
|
107
|
+
*/
|
|
108
|
+
export function requireLease(): boolean {
|
|
109
|
+
if (!longLived) return false;
|
|
110
|
+
const raw = (process.env.CDP_REQUIRE_LEASE ?? "").trim().toLowerCase();
|
|
111
|
+
return raw === "1" || raw === "true" || raw === "yes" || raw === "on";
|
|
112
|
+
}
|
|
113
|
+
|
|
68
114
|
/** The label recorded when a caller supplies none. ONE definition, because it
|
|
69
115
|
* is now written by three call sites (claim_page, new_page, and the origin
|
|
70
116
|
* ledger) and a second rule would make the same tab describe itself two
|
|
@@ -158,6 +204,9 @@ export interface ClaimOptions {
|
|
|
158
204
|
now?: number;
|
|
159
205
|
/** Live target ids from the browser, enabling the "target-gone" reclamation. */
|
|
160
206
|
liveIds?: readonly string[];
|
|
207
|
+
/** Marks the lease gate-acquired. See LeaseRecord.auto. Defaults to false:
|
|
208
|
+
* an unmarked claim is an explicit one, which is the stricter reading. */
|
|
209
|
+
auto?: boolean;
|
|
161
210
|
}
|
|
162
211
|
|
|
163
212
|
/** Why an existing lease may be taken over. All three are sufficient alone. */
|
|
@@ -214,6 +263,7 @@ export async function claimLease(
|
|
|
214
263
|
createdAt: now,
|
|
215
264
|
lastUsedAt: now,
|
|
216
265
|
ttlMs: opts.ttlMs ?? leaseTtlMs(),
|
|
266
|
+
auto: opts.auto === true,
|
|
217
267
|
};
|
|
218
268
|
try {
|
|
219
269
|
await writeFile(leaseFile(backend, targetId), JSON.stringify(record), { flag: "wx" });
|
|
@@ -265,7 +315,7 @@ export async function releaseLeaseFor(backend: LeaseBackend, targetId: string):
|
|
|
265
315
|
* This exists so enforcement can stay at ONE point per resolution path while
|
|
266
316
|
* the token still reaches it. The token arrives as a key on a tool's args
|
|
267
317
|
* object, many frames above resolveTarget, and threading a parameter through
|
|
268
|
-
* all
|
|
318
|
+
* all 45 tools would put the burden on every future contributor to remember
|
|
269
319
|
* the check exists: one missed tool would silently defeat the feature for that
|
|
270
320
|
* tool. mcp.ts and cli.ts open this scope once per dispatch instead, so a tool
|
|
271
321
|
* added tomorrow is covered with no action from whoever writes it.
|
|
@@ -370,7 +420,29 @@ export async function assertLeaseOk(
|
|
|
370
420
|
const where = describeTarget(targetId, ctx);
|
|
371
421
|
|
|
372
422
|
if (token === undefined) {
|
|
373
|
-
if (!held)
|
|
423
|
+
if (!held) {
|
|
424
|
+
// STRICT MODE: acquire rather than wave through. This is the change that
|
|
425
|
+
// makes "you cannot drive a tab you do not hold" true, and it is done
|
|
426
|
+
// here rather than by refusing because refusing would force every caller
|
|
427
|
+
// to learn a claim/release protocol to do what used to just work.
|
|
428
|
+
// Marked auto:true, which is what lets a later call from this same
|
|
429
|
+
// process pass with no token. A LeaseConflictError from claimLease (two
|
|
430
|
+
// processes racing to acquire the same tab, resolved by the "wx"
|
|
431
|
+
// exclusive create) propagates: the loser is genuinely refused.
|
|
432
|
+
if (requireLease()) {
|
|
433
|
+
await claimLease(backend, targetId, { label: defaultLabel(), ttlMs: leaseTtlMs(), auto: true, now });
|
|
434
|
+
}
|
|
435
|
+
return; // unleased, or leased-but-reclaimable: today's behavior
|
|
436
|
+
}
|
|
437
|
+
// An auto lease is owned by a PROCESS, not by a token, so any call from the
|
|
438
|
+
// owning pid passes. NOT gated on requireLease(): an auto:true record can
|
|
439
|
+
// only exist because strict mode created it, and gating this too would mean
|
|
440
|
+
// turning the flag off locks this process out of leases it still holds. It
|
|
441
|
+
// cannot regress 1.4.0, where no auto:true record can exist at all.
|
|
442
|
+
if (held.auto === true && held.pid === process.pid) {
|
|
443
|
+
await touchLease(held, now);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
374
446
|
throw new LeaseConflictError(
|
|
375
447
|
`${where} is leased by '${held.label}' (pid ${held.pid}). Pass that lease's token as the 'lease' argument, or call release_page to free it. list_leases shows every active lease.`,
|
|
376
448
|
targetId,
|
|
@@ -396,8 +468,16 @@ export async function assertLeaseOk(
|
|
|
396
468
|
// call arrives here, select_page via activatePage included.
|
|
397
469
|
if (parts.backend !== backend || parts.targetId !== targetId) {
|
|
398
470
|
// The token is about a different tab, so it says nothing about this one.
|
|
399
|
-
//
|
|
400
|
-
|
|
471
|
+
// Under strict mode this tab still has to be acquired: an agent holding
|
|
472
|
+
// tab A that reaches into unleased tab C is exactly the case the feature
|
|
473
|
+
// exists to cover, and it is the COMMON case under the ambient dispatch
|
|
474
|
+
// scope, not an edge one.
|
|
475
|
+
if (!held) {
|
|
476
|
+
if (requireLease()) {
|
|
477
|
+
await claimLease(backend, targetId, { label: defaultLabel(), ttlMs: leaseTtlMs(), auto: true, now });
|
|
478
|
+
}
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
401
481
|
throw new LeaseConflictError(
|
|
402
482
|
`the lease token you passed is for ${parts.backend} target '${parts.targetId}', not ${where}, which is leased by '${held.label}'. Resolve the tab you actually hold, or claim this one.`,
|
|
403
483
|
targetId,
|