@webhands/core 0.1.0 → 0.2.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/LICENSE +661 -0
- package/README.md +112 -0
- package/dist/cookies-export.d.ts +5 -5
- package/dist/cookies-export.d.ts.map +1 -1
- package/dist/cookies-export.js +4 -4
- package/dist/hand-host.d.ts +217 -0
- package/dist/hand-host.d.ts.map +1 -0
- package/dist/hand-host.js +351 -0
- package/dist/hand-host.js.map +1 -0
- package/dist/hand-loading.d.ts +128 -0
- package/dist/hand-loading.d.ts.map +1 -0
- package/dist/hand-loading.js +143 -0
- package/dist/hand-loading.js.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/playwright-attach-transport.d.ts +9 -0
- package/dist/playwright-attach-transport.d.ts.map +1 -1
- package/dist/playwright-attach-transport.js +53 -91
- package/dist/playwright-attach-transport.js.map +1 -1
- package/dist/playwright-launch-transport.d.ts +7 -62
- package/dist/playwright-launch-transport.d.ts.map +1 -1
- package/dist/playwright-launch-transport.js +51 -204
- package/dist/playwright-launch-transport.js.map +1 -1
- package/dist/remote-session.d.ts +12 -2
- package/dist/remote-session.d.ts.map +1 -1
- package/dist/remote-session.js +37 -6
- package/dist/remote-session.js.map +1 -1
- package/dist/seam.d.ts +13 -5
- package/dist/seam.d.ts.map +1 -1
- package/dist/session-rpc.d.ts +76 -12
- package/dist/session-rpc.d.ts.map +1 -1
- package/dist/session-rpc.js +76 -8
- package/dist/session-rpc.js.map +1 -1
- package/dist/stub-transport.d.ts +2 -2
- package/dist/stub-transport.d.ts.map +1 -1
- package/dist/stub-transport.js +11 -0
- package/dist/stub-transport.js.map +1 -1
- package/package.json +21 -2
- package/src/cookies-export.ts +5 -5
- package/src/hand-host.ts +511 -0
- package/src/hand-loading.ts +254 -0
- package/src/index.ts +18 -1
- package/src/playwright-attach-transport.ts +65 -119
- package/src/playwright-launch-transport.ts +63 -244
- package/src/remote-session.ts +43 -5
- package/src/seam.ts +13 -5
- package/src/session-rpc.ts +121 -11
- package/src/stub-transport.ts +15 -3
package/src/session-rpc.ts
CHANGED
|
@@ -4,12 +4,12 @@ import {
|
|
|
4
4
|
type Snapshot,
|
|
5
5
|
type WaitCondition,
|
|
6
6
|
} from './seam.js';
|
|
7
|
-
import type {
|
|
7
|
+
import type {WebHandsPage} from './seam.js';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* The wire protocol for driving the long-lived session over HTTP (ADR-0005).
|
|
11
11
|
*
|
|
12
|
-
* The served process holds ONE live {@link
|
|
12
|
+
* The served process holds ONE live {@link WebHandsPage} in memory; a thin client verb
|
|
13
13
|
* cannot hold a JS reference to it across the process boundary, so each verb
|
|
14
14
|
* call is sent as a small JSON request to the server, which runs it against its
|
|
15
15
|
* live page and returns the result. This module is the SINGLE source of truth
|
|
@@ -17,11 +17,35 @@ import type {Page} from './seam.js';
|
|
|
17
17
|
* client proxy so they cannot drift (mirrors how `serializeCookies` is shared
|
|
18
18
|
* by the cookies verb and its test).
|
|
19
19
|
*
|
|
20
|
-
* It is a thin transport detail, NOT a second verb surface: every
|
|
21
|
-
* 1:1 to a {@link
|
|
22
|
-
* are unchanged. The {@link LocatorString} brand and the
|
|
23
|
-
* {@link WaitCondition} cross as plain JSON and are re-branded on the
|
|
24
|
-
* with {@link locator}; no Playwright/CDP type is ever named here.
|
|
20
|
+
* It is a thin transport detail, NOT a second verb surface: every built-in
|
|
21
|
+
* request maps 1:1 to a {@link WebHandsPage} method, and the seam's verb semantics
|
|
22
|
+
* (ADR-0003/0004) are unchanged. The {@link LocatorString} brand and the
|
|
23
|
+
* structured {@link WaitCondition} cross as plain JSON and are re-branded on the
|
|
24
|
+
* server with {@link locator}; no Playwright/CDP type is ever named here.
|
|
25
|
+
*
|
|
26
|
+
* THIRD-PARTY HAND VERBS (Phase 2, Model B of the "hands" prd; ADR-0007). The
|
|
27
|
+
* eight built-in verbs stay a CLOSED union (the 1:1 source of truth above). A
|
|
28
|
+
* dynamically-loaded hand contributes a verb whose name `core` does NOT know at
|
|
29
|
+
* compile time, so it cannot be a named member of that closed union without
|
|
30
|
+
* re-meaning "closed". Instead it crosses as a SINGLE generic
|
|
31
|
+
* {@link SessionRpcHandRequest} variant (`{verb: 'hand', name, args}`) that
|
|
32
|
+
* names the contributed verb at runtime and carries its arguments. This is the
|
|
33
|
+
* exact wire parallel of how a hand verb composes into the page object: by name,
|
|
34
|
+
* dynamically, alongside the typed built-ins. The agent thereby gains a new tool
|
|
35
|
+
* over the wire WITHOUT ever holding a live page handle.
|
|
36
|
+
*
|
|
37
|
+
* SERIALIZATION BOUNDARY (the load-bearing rule; prd's resolved Q3). A hand
|
|
38
|
+
* verb's result crosses this RPC, so it MUST be serializable under the same
|
|
39
|
+
* structured-clone contract `eval` documents (see {@link WebHandsPage.eval}): richer
|
|
40
|
+
* than JSON, but a value with no transferable form does not round-trip. This is
|
|
41
|
+
* enforced by CONVENTION + TYPES (a hand author returns serializable values),
|
|
42
|
+
* NOT a blanket runtime clone here — a blanket clone would corrupt legitimate
|
|
43
|
+
* in-process (Model A) returns, where a hand may pass/return live Playwright
|
|
44
|
+
* handles within a single in-process call chain. A host-side runtime clone of
|
|
45
|
+
* agent-verb results is available HARDENING for untrusted hands, not built here.
|
|
46
|
+
* A page/in-hand throw REJECTS faithfully on the client exactly as the `eval`
|
|
47
|
+
* path already does (the server maps it to an `ok: false` reply carrying the
|
|
48
|
+
* message; the client re-throws a faithful `Error`).
|
|
25
49
|
*/
|
|
26
50
|
|
|
27
51
|
/** The path the session RPC is served under, below the server's base URL. */
|
|
@@ -29,6 +53,15 @@ export const SESSION_RPC_PATH = '/session/call';
|
|
|
29
53
|
|
|
30
54
|
/** A single verb call to run against the served live page. */
|
|
31
55
|
export type SessionRpcRequest =
|
|
56
|
+
| SessionRpcBuiltInRequest
|
|
57
|
+
| SessionRpcHandRequest;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The CLOSED union of webhands' eight built-in verbs. Each variant maps 1:1 to a
|
|
61
|
+
* {@link WebHandsPage} method in {@link applySessionRpc}; this is the single source of
|
|
62
|
+
* truth for the built-in verb surface, shared verbatim by server and client.
|
|
63
|
+
*/
|
|
64
|
+
export type SessionRpcBuiltInRequest =
|
|
32
65
|
| {readonly verb: 'navigate'; readonly url: string}
|
|
33
66
|
| {readonly verb: 'snapshot'; readonly full?: boolean}
|
|
34
67
|
| {readonly verb: 'click'; readonly locator: string}
|
|
@@ -38,6 +71,25 @@ export type SessionRpcRequest =
|
|
|
38
71
|
| {readonly verb: 'cookies'}
|
|
39
72
|
| {readonly verb: 'setCookies'; readonly cookies: readonly Cookie[]};
|
|
40
73
|
|
|
74
|
+
/**
|
|
75
|
+
* The OPEN escape for a dynamically-loaded third-party hand verb (Phase 2,
|
|
76
|
+
* Model B; ADR-0007). Unlike the closed built-in union, `core` does not know the
|
|
77
|
+
* verb's name at compile time, so it is carried at runtime: `name` is the
|
|
78
|
+
* contributed verb's plain name (exactly as it composed into the page object,
|
|
79
|
+
* not namespaced — a hand may even deliberately override a built-in, the
|
|
80
|
+
* operator's choice per ADR-0007) and `args` are its JSON arguments.
|
|
81
|
+
*
|
|
82
|
+
* The returned value and any thrown error obey the same serialization +
|
|
83
|
+
* rejection contract as `eval` (see this module's overview and {@link WebHandsPage.eval}).
|
|
84
|
+
*/
|
|
85
|
+
export interface SessionRpcHandRequest {
|
|
86
|
+
readonly verb: 'hand';
|
|
87
|
+
/** The hand-contributed verb's name (as it composed into the page object). */
|
|
88
|
+
readonly name: string;
|
|
89
|
+
/** The verb's arguments, carried as plain JSON (must be serializable). */
|
|
90
|
+
readonly args: readonly unknown[];
|
|
91
|
+
}
|
|
92
|
+
|
|
41
93
|
/**
|
|
42
94
|
* The server's reply to a {@link SessionRpcRequest}. `ok: true` carries the
|
|
43
95
|
* verb's return value (for verbs that return data); `ok: false` carries the
|
|
@@ -49,7 +101,7 @@ export type SessionRpcResponse =
|
|
|
49
101
|
| {readonly ok: false; readonly error: string};
|
|
50
102
|
|
|
51
103
|
/**
|
|
52
|
-
* Run one {@link SessionRpcRequest} against a live {@link
|
|
104
|
+
* Run one {@link SessionRpcRequest} against a live {@link WebHandsPage}, returning the
|
|
53
105
|
* value the wire should carry back. The server's HTTP handler is just this plus
|
|
54
106
|
* JSON framing; keeping the dispatch here (not inline in the handler) means the
|
|
55
107
|
* verb-to-page mapping is in one place and unit-testable without HTTP.
|
|
@@ -59,7 +111,7 @@ export type SessionRpcResponse =
|
|
|
59
111
|
* branded-string contract holds.
|
|
60
112
|
*/
|
|
61
113
|
export async function applySessionRpc(
|
|
62
|
-
page:
|
|
114
|
+
page: WebHandsPage,
|
|
63
115
|
request: SessionRpcRequest,
|
|
64
116
|
): Promise<unknown> {
|
|
65
117
|
switch (request.verb) {
|
|
@@ -84,11 +136,46 @@ export async function applySessionRpc(
|
|
|
84
136
|
case 'setCookies':
|
|
85
137
|
await page.setCookies(request.cookies);
|
|
86
138
|
return undefined;
|
|
139
|
+
case 'hand':
|
|
140
|
+
return applyHandVerb(page, request);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Invoke a dynamically-loaded hand verb by name against the live composed page.
|
|
146
|
+
*
|
|
147
|
+
* The composed {@link WebHandsPage} carries the hand's verbs at runtime (the host merged
|
|
148
|
+
* them in by name alongside the built-ins, see `composePage`), even though the
|
|
149
|
+
* seam `WebHandsPage` TYPE only names the eight built-ins. We therefore look the verb up
|
|
150
|
+
* on the page object as a runtime method and invoke it with the request's args.
|
|
151
|
+
* An unknown name is a faithful error (the hand was not loaded / named that
|
|
152
|
+
* verb), surfaced the same way a page-side throw is so the client rejects.
|
|
153
|
+
*
|
|
154
|
+
* The result is returned as-is: the serializable-only boundary is enforced by
|
|
155
|
+
* convention + types on the hand author, NOT a runtime clone here (a blanket
|
|
156
|
+
* clone would corrupt legitimate in-process Model A returns; see this module's
|
|
157
|
+
* overview). What the hand returns is what the wire carries back; the JSON
|
|
158
|
+
* framing in the server handler is the only encoding applied.
|
|
159
|
+
*/
|
|
160
|
+
async function applyHandVerb(
|
|
161
|
+
page: WebHandsPage,
|
|
162
|
+
request: SessionRpcHandRequest,
|
|
163
|
+
): Promise<unknown> {
|
|
164
|
+
const verb = (page as unknown as Record<string, unknown>)[request.name];
|
|
165
|
+
if (typeof verb !== 'function') {
|
|
166
|
+
throw new Error(
|
|
167
|
+
`no such hand verb '${request.name}' on the live page ` +
|
|
168
|
+
`(is the hand loaded and named in config?)`,
|
|
169
|
+
);
|
|
87
170
|
}
|
|
171
|
+
return (verb as (...args: readonly unknown[]) => unknown).call(
|
|
172
|
+
page,
|
|
173
|
+
...request.args,
|
|
174
|
+
);
|
|
88
175
|
}
|
|
89
176
|
|
|
90
177
|
/**
|
|
91
|
-
* A {@link
|
|
178
|
+
* A {@link WebHandsPage} whose verbs forward to a server via the supplied transport.
|
|
92
179
|
*
|
|
93
180
|
* Used by the client-side proxy (see `remote-session.ts`): each verb builds a
|
|
94
181
|
* {@link SessionRpcRequest}, hands it to `send`, and shapes the reply back into
|
|
@@ -98,7 +185,7 @@ export async function applySessionRpc(
|
|
|
98
185
|
*/
|
|
99
186
|
export function makeRpcPage(
|
|
100
187
|
send: (request: SessionRpcRequest) => Promise<unknown>,
|
|
101
|
-
):
|
|
188
|
+
): WebHandsPage {
|
|
102
189
|
return {
|
|
103
190
|
async navigate(url) {
|
|
104
191
|
await send({verb: 'navigate', url});
|
|
@@ -130,6 +217,29 @@ export function makeRpcPage(
|
|
|
130
217
|
};
|
|
131
218
|
}
|
|
132
219
|
|
|
220
|
+
/**
|
|
221
|
+
* Invoke a dynamically-loaded hand verb over the session RPC by name (Phase 2,
|
|
222
|
+
* Model B; ADR-0007). The client-side mirror of {@link applyHandVerb}: it builds
|
|
223
|
+
* the single generic {@link SessionRpcHandRequest} and hands it to the SAME
|
|
224
|
+
* `send` the built-in verbs use, so request/response shapes cannot drift.
|
|
225
|
+
*
|
|
226
|
+
* This is how the agent gains a new tool over the wire WITHOUT holding a live
|
|
227
|
+
* page handle: it names the contributed verb and passes serializable args; the
|
|
228
|
+
* server runs the hand against its own live page and returns a serializable
|
|
229
|
+
* result. A page/in-hand throw rejects faithfully (the `send` re-throws the
|
|
230
|
+
* server's error message), exactly as the `eval` path does.
|
|
231
|
+
*
|
|
232
|
+
* The result type is `unknown` because the hand decides the shape; callers
|
|
233
|
+
* narrow it (mirrors {@link WebHandsPage.eval}).
|
|
234
|
+
*/
|
|
235
|
+
export async function callHandVerb(
|
|
236
|
+
send: (request: SessionRpcRequest) => Promise<unknown>,
|
|
237
|
+
name: string,
|
|
238
|
+
...args: readonly unknown[]
|
|
239
|
+
): Promise<unknown> {
|
|
240
|
+
return send({verb: 'hand', name, args});
|
|
241
|
+
}
|
|
242
|
+
|
|
133
243
|
/**
|
|
134
244
|
* Re-brand a {@link WaitCondition} that arrived as plain JSON: only the
|
|
135
245
|
* `locator` form carries a branded string, which JSON flattens to a plain
|
package/src/stub-transport.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
Cookie,
|
|
3
3
|
OpenTarget,
|
|
4
|
-
|
|
4
|
+
WebHandsPage,
|
|
5
5
|
Session,
|
|
6
6
|
Snapshot,
|
|
7
7
|
SnapshotOptions,
|
|
@@ -13,7 +13,7 @@ import type {
|
|
|
13
13
|
* A record of one verb call against the stub, for assertions in seam tests.
|
|
14
14
|
*/
|
|
15
15
|
export interface StubCall {
|
|
16
|
-
readonly verb: keyof
|
|
16
|
+
readonly verb: keyof WebHandsPage;
|
|
17
17
|
readonly args: readonly unknown[];
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -37,6 +37,11 @@ export class StubTransport implements Transport {
|
|
|
37
37
|
const calls = this.calls;
|
|
38
38
|
let closed = false;
|
|
39
39
|
|
|
40
|
+
let resolveClosed!: () => void;
|
|
41
|
+
const closedSignal = new Promise<void>((resolve) => {
|
|
42
|
+
resolveClosed = resolve;
|
|
43
|
+
});
|
|
44
|
+
|
|
40
45
|
const ensureOpen = () => {
|
|
41
46
|
if (closed) {
|
|
42
47
|
throw new Error('session is closed');
|
|
@@ -48,7 +53,7 @@ export class StubTransport implements Transport {
|
|
|
48
53
|
? `stub://launch/${target.profile}`
|
|
49
54
|
: `stub://attach/${target.endpoint}`;
|
|
50
55
|
|
|
51
|
-
const page:
|
|
56
|
+
const page: WebHandsPage = {
|
|
52
57
|
async navigate(to: string): Promise<void> {
|
|
53
58
|
ensureOpen();
|
|
54
59
|
calls.push({verb: 'navigate', args: [to]});
|
|
@@ -93,7 +98,14 @@ export class StubTransport implements Transport {
|
|
|
93
98
|
return {
|
|
94
99
|
page,
|
|
95
100
|
async close(): Promise<void> {
|
|
101
|
+
if (closed) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
96
104
|
closed = true;
|
|
105
|
+
resolveClosed();
|
|
106
|
+
},
|
|
107
|
+
waitForClose(): Promise<void> {
|
|
108
|
+
return closedSignal;
|
|
97
109
|
},
|
|
98
110
|
};
|
|
99
111
|
}
|